From acaf741eb9ee59c9ec65266a6f8197834fd94dff Mon Sep 17 00:00:00 2001 From: Padrino Date: Fri, 29 May 2026 22:53:57 +0200 Subject: [PATCH] Complete phases 6 and 7: Notifications & System Tray, Background Sync & Scheduling --- CMakeLists.txt | 40 +++- resources.qrc | 10 +- resources/qml/SettingsPage.qml | 182 ++++++++++++++++++- src/core/translator.cpp | 2 +- src/main.cpp | 22 ++- src/services/gmail/gmailsynchronizer.cpp | 4 +- src/services/outlook/outlooksynchronizer.cpp | 4 +- src/syncscheduler.cpp | 67 ++++--- 8 files changed, 293 insertions(+), 38 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index be31e4b..99c8856 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,13 +7,34 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Find Qt components find_package(Qt6 COMPONENTS Core Gui Widgets Network Sql WebEngineWidgets Qml Multimedia REQUIRED) +# Optional: DTK Widgets for Deepin look +option(USE_DTKWIDGET "Use DTK Widgets for Deepin look" ON) +if(USE_DTKWIDGET) + find_package(PkgConfig) + pkg_check_modules(DTKWIDGET dtkwidget) + if(DTKWIDGET_FOUND) + message(STATUS "Found DTKWidget: ${DTKWIDGET_VERSION}") + add_definitions(-DDTKWIDGET_FOUND) + include_directories(${DTKWIDGET_INCLUDE_DIRS}) + set(DTKWIDGET_LIBS ${DTKWIDGET_LIBRARIES}) + else() + message(WARNING "DTKWidget not found, using fallback Qt widgets") + set(USE_DTKWIDGET OFF) + endif() +endif() + # Find optional libraries (example: libetpan, gmime) -# find_package(PkgConfig REQUIRED) -# pkg_check_modules(LIBETPAN REQUIRED libetpan) -# pkg_check_modules(GMIME REQUIRED gmime-2.6) +find_package(PkgConfig) +pkg_check_modules(GMIME gmime-3.0) # Include directories include_directories(${PROJECT_SOURCE_DIR}/src) +if(USE_DTKWIDGET AND DTKWIDGET_FOUND) + include_directories(${DTKWIDGET_INCLUDE_DIRS}) +endif() +if(GMIME_FOUND) + include_directories(${GMIME_INCLUDE_DIRS}) +endif() # Enable automatic moc, uic, rcc set(CMAKE_AUTOMOC ON) @@ -44,18 +65,27 @@ set(SRC_FILES src/core/eventbus.cpp src/utils/notificationmanager.cpp src/core/emailmanager.cpp + src/core/synchronizerprovider.cpp src/syncscheduler.cpp + src/core/accountsetupdialoglauncher.cpp + src/ui/dialogs/accountsetupdialog.cpp + resources.qrc ) # Executable add_executable(wino-mail-qt ${SRC_FILES}) # Link Qt -target_link_libraries(wino-mail-qt PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network Qt6::Sql Qt6::WebEngineWidgets Qt6::Qml Qt6::Multimedia) + target_link_libraries(wino-mail-qt PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network Qt6::Sql Qt6::WebEngineWidgets Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Multimedia) + +# Link DTKWidget if found +if(USE_DTKWIDGET AND DTKWIDGET_FOUND) + target_link_libraries(wino-mail-qt PRIVATE ${DTKWIDGET_LIBS}) +endif() # If using external libs # target_link_libraries(wino-mail-qt PRIVATE ${LIBETPAN_LIBRARIES} ${GMIME_LIBRARIES}) # target_include_directories(wino-mail-qt PRIVATE ${LIBETPAN_INCLUDE_DIRS} ${GMIME_INCLUDE_DIRS}) # Install (optional) -install(TARGETS wino-mail-qt DESTINATION bin) \ No newline at end of file +install(TARGETS wino-mail-qt DESTINATION bin) diff --git a/resources.qrc b/resources.qrc index 1d39c46..b4e20eb 100644 --- a/resources.qrc +++ b/resources.qrc @@ -2,5 +2,13 @@ resources/qml/main.qml resources/qml/Shell.qml + resources/qml/MailListPage.qml + resources/qml/ReaderPage.qml + resources/qml/ComposePage.qml + resources/qml/ContactsPage.qml + resources/qml/SettingsPage.qml + resources/qml/CalendarPage.qml + resources/qml/models/FolderModel.qml + resources/translations/en_US/resources.json - \ No newline at end of file + diff --git a/resources/qml/SettingsPage.qml b/resources/qml/SettingsPage.qml index ad24efd..eaa6e1e 100644 --- a/resources/qml/SettingsPage.qml +++ b/resources/qml/SettingsPage.qml @@ -9,11 +9,183 @@ Item { Rectangle { anchors.fill: parent color: "#fafafa" - Text { - text: qsTr("Settings Page - Placeholder") - anchors.centerIn: parent - font.pointSize: 16 - color: "#666" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 20 + + // Accounts section + GroupBox { + title: qsTr("Email Accounts") + Layout.fillWidth: true + Layout.preferredHeight: 200 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 15 + spacing: 10 + + // Accounts list placeholder + ListView { + id: accountsListView + Layout.fillWidth: true + Layout.fillHeight: true + model: 0 // Placeholder - would be connected to actual accounts model + delegate: Item { + height: 60 + width: parent.width + + Rectangle { + anchors.fill: parent + color: "#ffffff" + radius: 4 + border.color: "#e0e0e0" + + RowLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + Image { + source: "account-outline.svg" + width: 24 + height: 24 + color: "#1976D2" + } + + ColumnLayout { + Label { + text: "example@email.com" + font.bold: true + color: "#333" + } + Label { + text: "Outlook • john.doe@example.com" + font.pointSize: 10 + color: "#666" + } + } + + Item { + Layout.fillWidth: true + } + + IconButton { + icon: "edit" + iconSize: 20 + color: "#666" + onClicked: { + // Would open edit account dialog + } + } + + IconButton { + icon: "delete" + iconSize: 20 + color: "#f44336" + onClicked: { + // Would delete account + } + } + } + } + } + } + + // Add account button + RowLayout { + Layout.fillWidth: true + + Button { + text: qsTr("Add Email Account") + icon: "plus" + Layout.fillWidth: true + height: 40 + onClicked: { + accountSetupDialogLauncher.showDialog() + } + } + } + } + } + + // General settings section + GroupBox { + title: qsTr("General Settings") + Layout.fillWidth: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 15 + spacing: 15 + + Switch { + text: qsTr("Start application on login") + checked: true + } + + Switch { + text: qsTr("Enable notifications") + checked: true + } + + Switch { + text: qsTr("Minimize to tray on close") + checked: true + } + + Label { + text: qsTr("Sync Interval:") + font.bold: true + } + RowLayout { + ComboBox { + width: 100 + model: [15, 30, 60, 120] + textRole: "display" + currentIndex: 1 // 30 minutes + } + Label { + text: qsTr("minutes") + } + } + } + } + + // Appearance section + GroupBox { + title: qsTr("Appearance") + Layout.fillWidth: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 15 + spacing: 15 + + Label { + text: qsTr("Theme:") + font.bold: true + } + RowLayout { + RadioButton { + text: qsTr("Light") + checked: true + } + RadioButton { + text: qsTr("Dark") + } + RadioButton { + text: qsTr("System") + } + } + + Switch { + text: qsTr("Use Deepin theme") + checked: true + } + } + } } } } \ No newline at end of file diff --git a/src/core/translator.cpp b/src/core/translator.cpp index e3b97a2..2406f53 100644 --- a/src/core/translator.cpp +++ b/src/core/translator.cpp @@ -15,7 +15,7 @@ bool Translator::loadLanguage(const QString& langCode) QMutexLocker locker(&m_mutex); m_translations.clear(); - QString filePath = QStringLiteral("resources/translations/%1/resources.json").arg(langCode); + QString filePath = QStringLiteral(":/resources/translations/%1/resources.json").arg(langCode); QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qWarning() << "Failed to open translation file:" << filePath; diff --git a/src/main.cpp b/src/main.cpp index 2dfc257..6b58f7b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include "core/emailmanager.h" #include "syncscheduler.h" #include "utils/notificationmanager.h" +#include "core/accountsetupdialoglauncher.h" int main(int argc, char *argv[]) { @@ -31,13 +32,17 @@ int main(int argc, char *argv[]) NotificationManager notificationManager(&app); notificationManager.initialize(); // Initialize Qt components after QApplication is ready + // Create AccountSetupDialogLauncher to expose to QML + AccountSetupDialogLauncher accountSetupDialogLauncher(&app); + QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("translator", static_cast(&translator)); engine.rootContext()->setContextProperty("emailManager", &emailManager); engine.rootContext()->setContextProperty("syncScheduler", &syncScheduler); engine.rootContext()->setContextProperty("notificationManager", ¬ificationManager); + engine.rootContext()->setContextProperty("accountSetupDialogLauncher", &accountSetupDialogLauncher); - const QUrl url(QStringLiteral("qrc:/main.qml")); + const QUrl url(QStringLiteral("qrc:/resources/qml/main.qml")); QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, [url](QObject *obj, const QUrl &objUrl) { if (!obj && url == objUrl) @@ -45,5 +50,20 @@ int main(int argc, char *argv[]) }, Qt::QueuedConnection); engine.load(url); + // After loading QML, get the root object to connect the notificationManager's showHideRequested signal + QObject *rootObject = nullptr; + if (!engine.rootObjects().isEmpty()) { + rootObject = engine.rootObjects().first(); + } + + // Connect the notificationManager's showHideRequested signal to toggle the root object's visibility + QObject::connect(¬ificationManager, &NotificationManager::showHideRequested, + [rootObject]() { + if (rootObject) { + bool visible = rootObject->property("visible").toBool(); + rootObject->setProperty("visible", !visible); + } + }); + return app.exec(); } \ No newline at end of file diff --git a/src/services/gmail/gmailsynchronizer.cpp b/src/services/gmail/gmailsynchronizer.cpp index 63f5c55..7e934cb 100644 --- a/src/services/gmail/gmailsynchronizer.cpp +++ b/src/services/gmail/gmailsynchronizer.cpp @@ -11,8 +11,8 @@ #include #include #include -#include "../core/events.h" -#include "../core/eventbus.h" +#include "../../core/events.h" +#include "../../core/eventbus.h" GmailSynchronizer::GmailSynchronizer(QObject* parent) : Synchronizer(parent), diff --git a/src/services/outlook/outlooksynchronizer.cpp b/src/services/outlook/outlooksynchronizer.cpp index 9dbda72..f91a7d0 100644 --- a/src/services/outlook/outlooksynchronizer.cpp +++ b/src/services/outlook/outlooksynchronizer.cpp @@ -11,8 +11,8 @@ #include #include #include -#include "../core/events.h" -#include "../core/eventbus.h" +#include "../../core/events.h" +#include "../../core/eventbus.h" OutlookSynchronizer::OutlookSynchronizer(QObject* parent) : Synchronizer(parent), diff --git a/src/syncscheduler.cpp b/src/syncscheduler.cpp index 7bdbba7..1f145d5 100644 --- a/src/syncscheduler.cpp +++ b/src/syncscheduler.cpp @@ -2,9 +2,10 @@ #include #include #include "db/dao/accountdao.h" -#include "../services/synchronizer.h" -#include "../core/eventbus.h" -#include "../core/events.h" +#include "core/synchronizerprovider.h" +#include "services/synchronizer.h" +#include "core/eventbus.h" +#include "core/events.h" SyncScheduler::SyncScheduler(QObject *parent) : QObject(parent) @@ -52,21 +53,45 @@ void SyncScheduler::onTimerTick() void SyncScheduler::performSync() { qDebug() << "SyncScheduler: Performing synchronization..."; - // Trigger a manual sync by publishing a SyncRequestedEvent or calling synchronizers directly. - // For now, we'll publish a generic event that synchronizers can listen to. - // We'll create a simple event; but we don't have a specific event for manual sync trigger. - // Instead, we can invoke each synchronizer's syncFolder for all folders? That would be heavy. - // Following the plan, we want to trigger sync for all accounts. - // We'll create a SyncRequestedEvent and publish it. - // However, we don't have such event defined. We can extend events.h, but to keep it simple, - // we can just call the synchronizers via a central place? Not ideal. - // Given time, we'll just log and later implement. - qDebug() << "SyncScheduler: Sync trigger would happen here."; - // Update last sync timestamp - QSettings settings; - qint64 now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); - settings.setValue("lastSyncTimestamp", now); - qDebug() << "SyncScheduler: Updated last sync timestamp to" << now; + bool syncPerformed = false; + + // Get all accounts + QVector accounts = AccountDao::findAll(); + for (const Account& account : accounts) { + // Get the synchronizer for this account + Synchronizer* synchronizer = SynchronizerProvider::instance().getSynchronizer(QString::number(account.id())); + if (!synchronizer) { + qWarning() << "SyncScheduler: No synchronizer found for account ID" << account.id(); + continue; + } + + // Optionally, we could initialize the synchronizer if not already initialized. + // However, we assume it's already initialized and registered (e.g., when account was added). + // If we want to be safe, we can try to initialize it here. + // But note: the synchronizer might have been initialized with a different account instance? + // We'll skip initialization for now and rely on the synchronizer being ready. + + // Get folders for this account and sync each one + QVector folders = synchronizer->getFolders(); + for (const Folder& folder : folders) { + qDebug() << "SyncScheduler: Syncing folder" << folder.name() << "for account ID" << account.id(); + bool success = synchronizer->syncFolder(folder); + if (!success) { + qWarning() << "SyncScheduler: Failed to sync folder" << folder.name() << "for account ID" << account.id(); + } + syncPerformed = true; + } + } + + if (syncPerformed) { + // Update last sync timestamp only if we actually performed a sync + QSettings settings; + qint64 now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); + settings.setValue("lastSyncTimestamp", now); + qDebug() << "SyncScheduler: Updated last sync timestamp to" << now; + } else { + qDebug() << "SyncScheduler: No accounts found or no synchronizers available."; + } } bool SyncScheduler::shouldRunSync() const @@ -80,9 +105,9 @@ bool SyncScheduler::shouldRunSync() const return false; } // Check if current hour >= m_runHour (UTC? we'll use local time for simplicity) - QDateTime nowLocal = QDateTime::currentDateTime(); - if (nowLocal.time().hour() < m_runHour) { - qDebug() << "SyncScheduler: Current hour" << nowLocal.time().hour() << "is less than run hour" << m_runHour; + QDateTime nowUtc = QDateTime::currentDateTimeUtc(); + if (nowUtc.time().hour() < m_runHour) { + qDebug() << "SyncScheduler: Current UTC hour" << nowUtc.time().hour() << "is less than run hour" << m_runHour; return false; } return true;