From acec3202220a98c70a5b19057033aeee65862236 Mon Sep 17 00:00:00 2001 From: Padrino Date: Sun, 17 May 2026 01:09:01 +0200 Subject: [PATCH] Implement basic UI with QML (MailListPage) and batching DbChangeProcessor (Step 3-4 of transition plan) --- CMakeLists.txt | 11 +- resources.qrc | 1 + resources/qml/MailListPage.qml | 136 ++++++++++++ resources/qml/Shell.qml | 174 +++++++++++++++ resources/qml/main.qml | 26 +-- resources/qml/models/FolderModel.qml | 24 +++ src/core/events.h | 12 +- src/core/translator.h | 4 +- src/db/dao/accountdao.cpp | 50 ++--- src/db/dao/folderdao.cpp | 3 + src/db/dao/mailitemdao.cpp | 2 + src/db/dbchangeprocessor.cpp | 214 +++++++++++++++++++ src/db/dbchangeprocessor.h | 51 +++++ src/main.cpp | 6 +- src/services/gmail/gmailsynchronizer.cpp | 18 +- src/services/imap/imapsynchronizer.cpp | 78 ++++++- src/services/imap/imapsynchronizer.h | 7 + src/services/outlook/outlooksynchronizer.cpp | 14 +- src/ui/models/FolderListModel.cpp | 46 ++++ src/ui/models/FolderListModel.h | 34 +++ 20 files changed, 829 insertions(+), 82 deletions(-) create mode 100644 resources/qml/MailListPage.qml create mode 100644 resources/qml/Shell.qml create mode 100644 resources/qml/models/FolderModel.qml create mode 100644 src/db/dbchangeprocessor.cpp create mode 100644 src/db/dbchangeprocessor.h create mode 100644 src/ui/models/FolderListModel.cpp create mode 100644 src/ui/models/FolderListModel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a2d2258..164a06f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,10 +17,9 @@ include_directories(${PROJECT_SOURCE_DIR}/src) # Resources qt6_add_resources(WinoMailQtResources - PREFIX - / FILES resources/qml/main.qml + resources/qml/Shell.qml ) # Source files @@ -32,10 +31,18 @@ set(SRC_FILES src/core/models/folder.cpp src/services/synchronizer.cpp src/services/imap/imapsynchronizer.cpp + src/services/outlook/outlooksynchronizer.cpp + src/services/gmail/gmailsynchronizer.cpp + src/services/request.cpp + src/services/concreterequests.cpp + src/services/requestprocessor.cpp + src/services/changetype.cpp + src/services/changprocessor.cpp src/db/databasemanager.cpp src/db/dao/accountdao.cpp src/db/dao/folderdao.cpp src/db/dao/mailitemdao.cpp + src/db/dbchangeprocessor.cpp ) # Executable diff --git a/resources.qrc b/resources.qrc index 3d86720..1d39c46 100644 --- a/resources.qrc +++ b/resources.qrc @@ -1,5 +1,6 @@ resources/qml/main.qml + resources/qml/Shell.qml \ No newline at end of file diff --git a/resources/qml/MailListPage.qml b/resources/qml/MailListPage.qml new file mode 100644 index 0000000..9f2d976 --- /dev/null +++ b/resources/qml/MailListPage.qml @@ -0,0 +1,136 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Item { + id: mailListPage + anchors.fill: parent + + // Layout: SplitView for resizable panes + SplitView { + anchors.fill: parent + + // Left pane: Folder list + Rectangle { + width: 200 + color: "#fafafa" + ListView { + id: folderListView + anchors.fill: parent + clip: true + model: FolderModel {} + delegate: Item { + height: 40 + Layout.fillWidth: true + Rectangle { + anchors.fill: parent + color: ListView.isCurrentItem ? "#e3f2fd" : "transparent" + Text { + text: folderName + anchors.left: parent.left + anchors.leftMargin: 16 + anchors.verticalCenter: parent.verticalCenter + font.pointSize: 14 + color: ListView.isCurrentItem ? "#1976d2" : "#666" + } + } + MouseArea { + anchors.fill: parent + onClicked: { + folderListView.currentIndex = index + // TODO: Load emails for selected folder + } + } + } + } + } + + // Right pane: Email list and preview + Rectangle { + color: "#fafafa" + SplitView { + orientation: Qt.Vertical + anchors.fill: parent + + // Email list (top) + Rectangle { + Layout.minimumHeight: 200 + color: "#fafafa" + ListView { + id: emailListView + anchors.fill: parent + clip: true + model: EmailModel {} + delegate: Item { + height: 60 + Layout.fillWidth: true + Rectangle { + anchors.fill: parent + color: ListView.isCurrentItem ? "#e3f2fd" : "transparent" + RowLayout { + anchors.fill: parent + anchors.margins: 8 + // Sender avatar/initial + Rectangle { + width: 40 + height: 40 + color: "#cccccc" + Text { + text: senderInitial + anchors.centerIn: parent + font.pointSize: 16 + color: "#666" + } + } + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + Text { + text: senderName + font.pointSize: 14 + color: "#333" + elide: Text.ElideRight + Layout.fillWidth: true + } + Text { + text: subject + font.pointSize: 13 + color: "#666" + elide: Text.ElideRight + Layout.fillWidth: true + } + Text { + text: time + font.pointSize: 12 + color: "#999" + } + } + } + } + MouseArea { + anchors.fill: parent + onClicked: { + emailListView.currentIndex = index + // TODO: Show email preview in the bottom pane + } + } + } + } + } + + // Email preview (bottom) + Rectangle { + Layout.minimumHeight: 200 + color: "#ffffff" + Text { + id: emailPreview + text: "Select an email to preview" + anchors.centerIn: parent + font.pointSize: 14 + color: "#666" + } + } + } + } + } +} \ No newline at end of file diff --git a/resources/qml/Shell.qml b/resources/qml/Shell.qml new file mode 100644 index 0000000..24946d1 --- /dev/null +++ b/resources/qml/Shell.qml @@ -0,0 +1,174 @@ +import QtQuick 2.15 +import QtQuick.Window 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +ApplicationWindow { + id: shell + visible: true + width: 1024 + height: 768 + title: qsTr("Wino Mail Qt") + + // Set the title from translator in Component.onCompleted + Component.onCompleted: { + title = translator.tr("appName") + } + + // Drawer for navigation + Drawer { + id: drawer + width: 200 + height: shell.height + Container { + anchors.fill: parent + Layout.alignment: Qt.AlignTop + ScrollView { + anchors.fill: parent + ColumnLayout { + spacing: 0 + // App logo/name + Rectangle { + Layout.fillWidth: true + height: 60 + color: "#1976D2" + Text { + text: qsTr("Wino Mail") + color: "white" + font.pointSize: 18 + anchors.centerIn: parent + } + } + // Navigation items + NavigationItem { + text: qsTr("Mail") + icon: "mail" + isChecked: stackView.currentIndex === 0 + onClicked: { + drawer.close() + stackView.currentIndex = 0 + } + } + NavigationItem { + text: qsTr("Calendar") + icon: "calendar" + isChecked: stackView.currentIndex === 1 + onClicked: { + drawer.close() + stackView.currentIndex = 1 + } + } + NavigationItem { + text: qsTr("Contacts") + icon: "contacts" + isChecked: stackView.currentIndex === 2 + onClicked: { + drawer.close() + stackView.currentIndex = 2 + } + } + NavigationItem { + text: qsTr("Settings") + icon: "settings" + isChecked: stackView.currentIndex === 3 + onClicked: { + drawer.close() + stackView.currentIndex = 3 + } + } + } + } + } + } + + // Main content area with StackView for pages + StackView { + id: stackView + anchors.fill: parent + initialItem: mailListPage + } + + // Define pages + MailListPage { + id: mailListPage + } + // Placeholder for other pages + Rectangle { + color: "#f0f0f0" + Text { + text: qsTr("Calendar Page") + anchors.centerIn: parent + } + } + Rectangle { + color: "#f0f0f0" + Text { + text: qsTr("Contacts Page") + anchors.centerIn: parent + } + } + Rectangle { + color: "#f0f0f0" + Text { + text: qsTr("Settings Page") + anchors.centerIn: parent + } + } + + // Hamburger button to open drawer + MenuButton { + icon: "menu" + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: 10 + onClicked: drawer.open() + } +} + +// Helper components +component NavigationItem: Button { + Layout.fillWidth: true + height: 48 + padding: 0 + contentItem: RowLayout { + spacing: 16 + anchors.fill: parent + anchors.margins: 24 + Icon { + source: icon + width: 24 + height: 24 + color: isChecked ? "#1976D2" : "#666" + } + Text { + text: text + font.pointSize: 14 + verticalAlignment: Text.AlignVCenter + color: isChecked ? "#1976D2" : "#666" + } + } + background: Rectangle { + color: isChecked ? "#E3F2FD" : "transparent" + radius: 0 + } +} + +component MenuButton: IconButton { + iconSource: icon +} + +// Placeholder for MailListPage - we'll replace this with a real component later +component MailListPage: Item { + id: mailListPage + anchors.fill: parent + Rectangle { + anchors.fill: parent + color: "#fafafa" + Text { + text: qsTr("Mail List Placeholder") + anchors.centerIn: parent + font.pointSize: 16 + color: "#666" + } + } +} \ No newline at end of file diff --git a/resources/qml/main.qml b/resources/qml/main.qml index 8f0654d..e765464 100644 --- a/resources/qml/main.qml +++ b/resources/qml/main.qml @@ -2,28 +2,6 @@ import QtQuick 2.15 import QtQuick.Window 2.15 import QtQuick.Controls 2.15 -Window { - width: 1024 - height: 768 - visible: true - title: qsTr("Wino Mail Qt") - - // Use the translator to set the title - // Note: we can't directly call translator.tr here because it's a C++ object. - // We'll set the title in Component.onCompleted by accessing the context property. - Component.onCompleted: { - title = translator.tr("appName") - } - - Rectangle { - anchors.fill: parent - color: "#f0f0f0" - - Text { - text: qsTr("Welcome to Wino Mail Qt!") - anchors.centerIn: parent - font.pointSize: 24 - color: "#333" - } - } +// We'll load the Shell component from Shell.qml +Shell { } \ No newline at end of file diff --git a/resources/qml/models/FolderModel.qml b/resources/qml/models/FolderModel.qml new file mode 100644 index 0000000..06f3b0e --- /dev/null +++ b/resources/qml/models/FolderModel.qml @@ -0,0 +1,24 @@ +import QtQuick 2.15 + +ListModel { + ListElement { + folderName: "Inbox" + unreadCount: 5 + } + ListElement { + folderName: "Sent" + unreadCount: 0 + } + ListElement { + folderName: "Drafts" + unreadCount: 0 + } + ListElement { + folderName: "Trash" + unreadCount: 0 + } + ListElement { + folderName: "Spam" + unreadCount: 0 + } +} \ No newline at end of file diff --git a/src/core/events.h b/src/core/events.h index f13cf5b..69a85ba 100644 --- a/src/core/events.h +++ b/src/core/events.h @@ -3,9 +3,9 @@ #include #include #include -#include "../models/account.h" -#include "../models/folder.h" -#include "../core/mailitem.h" +#include "models/account.h" +#include "models/folder.h" +#include "mailitem.h" namespace WinoMail { namespace Events { @@ -38,7 +38,7 @@ struct FolderAddedEvent : public BaseEvent { }; struct FolderRemovedEvent : public BaseEvent { - String folderId; + QString folderId; }; struct FolderUpdatedEvent : public BaseEvent { @@ -63,12 +63,12 @@ struct AccountUpdatedEvent : public BaseEvent { // Sync events struct SyncStartedEvent : public BaseEvent { int accountId; - String folderId; // Optional, empty for full account sync + QString folderId; // Optional, empty for full account sync }; struct SyncFinishedEvent : public BaseEvent { int accountId; - String folderId; // Optional, empty for full account sync + QString folderId; // Optional, empty for full account sync bool success; QString errorMessage; // If success is false }; diff --git a/src/core/translator.h b/src/core/translator.h index 508a254..b120cc9 100644 --- a/src/core/translator.h +++ b/src/core/translator.h @@ -3,9 +3,11 @@ #include #include #include +#include -class Translator +class Translator : public QObject { + Q_OBJECT public: static Translator& instance(); ~Translator() = default; diff --git a/src/db/dao/accountdao.cpp b/src/db/dao/accountdao.cpp index 7142f8a..6c15edb 100644 --- a/src/db/dao/accountdao.cpp +++ b/src/db/dao/accountdao.cpp @@ -1,5 +1,4 @@ #include "accountdao.h" -#include bool AccountDao::insert(const Account& account) { @@ -69,7 +68,7 @@ bool AccountDao::remove(int id) return true; } -std::optional AccountDao::findById(int id) +Account* AccountDao::findById(int id) { QSqlDatabase& db = DatabaseManager::instance().database(); QSqlQuery query(db); @@ -78,21 +77,22 @@ std::optional AccountDao::findById(int id) if (!query.exec()) { qWarning() << "Failed to find account by id:" << query.lastError().text(); - return std::nullopt; + return nullptr; } if (query.next()) { - Account acc; - acc.setId(query.value(0).toInt()); - acc.setEmail(query.value(1).toString()); - acc.setDisplayName(query.value(2).toString()); - acc.setType(static_cast(query.value(3).toInt())); - acc.setAccessToken(query.value(4).toString()); - acc.setRefreshToken(query.value(5).toString()); - acc.setTokenExpires(query.value(6).toDateTime()); - return acc; + Account* account = new Account(); + account->setId(query.value("id").toLongLong()); + account->setEmail(query.value("email").toString()); + account->setDisplayName(query.value("displayName").toString()); + account->setType(static_cast(query.value("type").toInt())); + account->setAccessToken(query.value("accessToken").toString()); + account->setRefreshToken(query.value("refreshToken").toString()); + account->setTokenExpires(query.value("tokenExpires").toDateTime()); + return account; } - return std::nullopt; + + return nullptr; } QVector AccountDao::findAll() @@ -119,7 +119,7 @@ QVector AccountDao::findAll() return accounts; } -std::optional AccountDao::findByEmail(const QString& email) +Account* AccountDao::findByEmail(const QString& email) { QSqlDatabase& db = DatabaseManager::instance().database(); QSqlQuery query(db); @@ -128,19 +128,19 @@ std::optional AccountDao::findByEmail(const QString& email) if (!query.exec()) { qWarning() << "Failed to find account by email:" << query.lastError().text(); - return std::nullopt; + return nullptr; } if (query.next()) { - Account acc; - acc.setId(query.value(0).toInt()); - acc.setEmail(query.value(1).toString()); - acc.setDisplayName(query.value(2).toString()); - acc.setType(static_cast(query.value(3).toInt())); - acc.setAccessToken(query.value(4).toString()); - acc.setRefreshToken(query.value(5).toString()); - acc.setTokenExpires(query.value(6).toDateTime()); + Account* acc = new Account(); + acc->setId(query.value(0).toLongLong()); + acc->setEmail(query.value(1).toString()); + acc->setDisplayName(query.value(2).toString()); + acc->setType(static_cast(query.value(3).toInt())); + acc->setAccessToken(query.value(4).toString()); + acc->setRefreshToken(query.value(5).toString()); + acc->setTokenExpires(query.value(6).toDateTime()); return acc; } - return std::nullopt; -} \ No newline at end of file + return nullptr; +} diff --git a/src/db/dao/folderdao.cpp b/src/db/dao/folderdao.cpp index 733f267..37108cb 100644 --- a/src/db/dao/folderdao.cpp +++ b/src/db/dao/folderdao.cpp @@ -1,4 +1,7 @@ +#include #include "folderdao.h" +#include +#include #include bool FolderDao::insert(const Folder& folder) diff --git a/src/db/dao/mailitemdao.cpp b/src/db/dao/mailitemdao.cpp index a526f08..9760790 100644 --- a/src/db/dao/mailitemdao.cpp +++ b/src/db/dao/mailitemdao.cpp @@ -1,3 +1,5 @@ +#include +#include #include "mailitemdao.h" #include diff --git a/src/db/dbchangeprocessor.cpp b/src/db/dbchangeprocessor.cpp new file mode 100644 index 0000000..8058e73 --- /dev/null +++ b/src/db/dbchangeprocessor.cpp @@ -0,0 +1,214 @@ +#include "dbchangeprocessor.h" +#include +#include + +DbChangeProcessor::DbChangeProcessor(QObject* parent) + : QObject(parent), + m_db(DatabaseManager::instance()), + m_mailItemDao(MailItemDao::instance()), + m_folderDao(FolderDao::instance()), + m_accountDao(AccountDao::instance()), + m_batchTimer(new QTimer(this)) +{ + // Configurar el timer para procesar batch cada 5 segundos + m_batchTimer->setInterval(5000); // 5 segundos + connect(m_batchTimer, &QTimer::timeout, this, &DbChangeProcessor::processBatch); + m_batchTimer->start(); + + // Suscribirse a todos los eventos relevantes + SUBSCRIBE(WinoMail::Events::MailItemAddedEvent, + [this](const WinoMail::Events::MailItemAddedEvent& event) { + handleMailItemAdded(event); + }); + + SUBSCRIBE(WinoMail::Events::MailItemRemovedEvent, + [this](const WinoMail::Events::MailItemRemovedEvent& event) { + handleMailItemRemoved(event); + }); + + SUBSCRIBE(WinoMail::Events::MailItemUpdatedEvent, + [this](const WinoMail::Events::MailItemUpdatedEvent& event) { + handleMailItemUpdated(event); + }); + + SUBSCRIBE(WinoMail::Events::FolderAddedEvent, + [this](const WinoMail::Events::FolderAddedEvent& event) { + handleFolderAdded(event); + }); + + SUBSCRIBE(WinoMail::Events::FolderRemovedEvent, + [this](const WinoMail::Events::FolderRemovedEvent& event) { + handleFolderRemoved(event); + }); + + SUBSCRIBE(WinoMail::Events::FolderUpdatedEvent, + [this](const WinoMail::Events::FolderUpdatedEvent& event) { + handleFolderUpdated(event); + }); + + SUBSCRIBE(WinoMail::Events::AccountAddedEvent, + [this](const WinoMail::Events::AccountAddedEvent& event) { + handleAccountAdded(event); + }); + + SUBSCRIBE(WinoMail::Events::AccountRemovedEvent, + [this](const WinoMail::Events::AccountRemovedEvent& event) { + handleAccountRemoved(event); + }); + + SUBSCRIBE(WinoMail::Events::AccountUpdatedEvent, + [this](const WinoMail::Events::AccountUpdatedEvent& event) { + handleAccountUpdated(event); + }); +} + +void DbChangeProcessor::handleMailItemAdded(const WinoMail::Events::MailItemAddedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing MailItemAddedEvent for UID:" << event.item.uid; + m_mailItemAddedQueue.append(event); +} + +void DbChangeProcessor::handleMailItemRemoved(const WinoMail::Events::MailItemRemovedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing MailItemRemovedEvent for UID:" << event.itemUid; + m_mailItemRemovedQueue.append(event); +} + +void DbChangeProcessor::handleMailItemUpdated(const WinoMail::Events::MailItemUpdatedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing MailItemUpdatedEvent for UID:" << event.item.uid; + m_mailItemUpdatedQueue.append(event); +} + +void DbChangeProcessor::handleFolderAdded(const WinoMail::Events::FolderAddedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing FolderAddedEvent for folder:" << event.folder.name; + m_folderAddedQueue.append(event); +} + +void DbChangeProcessor::handleFolderRemoved(const WinoMail::Events::FolderRemovedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing FolderRemovedEvent for folder ID:" << event.folderId; + m_folderRemovedQueue.append(event); +} + +void DbChangeProcessor::handleFolderUpdated(const WinoMail::Events::FolderUpdatedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing FolderUpdatedEvent for folder ID:" << event.folder.id(); + m_folderUpdatedQueue.append(event); +} + +void DbChangeProcessor::handleAccountAdded(const WinoMail::Events::AccountAddedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing AccountAddedEvent for account:" << event.account.email(); + m_accountAddedQueue.append(event); +} + +void DbChangeProcessor::handleAccountRemoved(const WinoMail::Events::AccountRemovedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing AccountRemovedEvent for account ID:" << event.accountId; + m_accountRemovedQueue.append(event); +} + +void DbChangeProcessor::handleAccountUpdated(const WinoMail::Events::AccountUpdatedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing AccountUpdatedEvent for account ID:" << event.account.id(); + m_accountUpdatedQueue.append(event); +} + +void DbChangeProcessor::processBatch() +{ + qDebug() << "DbChangeProcessor: Starting batch processing at" << QDateTime::currentDateTime().toString(); + + // Procesar eventos de MailItem añadidos + if (!m_mailItemAddedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_mailItemAddedQueue.size() << "MailItemAdded events"; + for (const auto& event : m_mailItemAddedQueue) { + m_mailItemDao.insert(event.item); + } + m_mailItemAddedQueue.clear(); + } + + // Procesar eventos de MailItem eliminados + if (!m_mailItemRemovedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_mailItemRemovedQueue.size() << "MailItemRemoved events"; + for (const auto& event : m_mailItemRemovedQueue) { + m_mailItemDao.removeByUid(event.itemUid, event.folderId); + } + m_mailItemRemovedQueue.clear(); + } + + // Procesar eventos de MailItem actualizados + if (!m_mailItemUpdatedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_mailItemUpdatedQueue.size() << "MailItemUpdated events"; + for (const auto& event : m_mailItemUpdatedQueue) { + m_mailItemDao.update(event.item, event.changedFields); + } + m_mailItemUpdatedQueue.clear(); + } + + // Procesar eventos de carpetas añadidas + if (!m_folderAddedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_folderAddedQueue.size() << "FolderAdded events"; + for (const auto& event : m_folderAddedQueue) { + m_folderDao.insert(event.folder); + } + m_folderAddedQueue.clear(); + } + + // Procesar eventos de carpetas eliminadas + if (!m_folderRemovedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_folderRemovedQueue.size() << "FolderRemoved events"; + for (const auto& event : m_folderRemovedQueue) { + m_folderDao.remove(event.folderId.toInt()); + } + m_folderRemovedQueue.clear(); + } + + // Procesar eventos de carpetas actualizadas + if (!m_folderUpdatedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_folderUpdatedQueue.size() << "FolderUpdated events"; + for (const auto& event : m_folderUpdatedQueue) { + m_folderDao.update(event.folder, event.changedFields); + } + m_folderUpdatedQueue.clear(); + } + + // Procesar eventos de cuentas añadidas + if (!m_accountAddedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_accountAddedQueue.size() << "AccountAdded events"; + for (const auto& event : m_accountAddedQueue) { + m_accountDao.insert(event.account); + } + m_accountAddedQueue.clear(); + } + + // Procesar eventos de cuentas eliminadas + if (!m_accountRemovedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_accountRemovedQueue.size() << "AccountRemoved events"; + for (const auto& event : m_accountRemovedQueue) { + m_accountDao.remove(event.accountId); + } + m_accountRemovedQueue.clear(); + } + + // Procesar eventos de cuentas actualizadas + if (!m_accountUpdatedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_accountUpdatedQueue.size() << "AccountUpdated events"; + for (const auto& event : m_accountUpdatedQueue) { + m_accountDao.update(event.account, event.changedFields); + } + m_accountUpdatedQueue.clear(); + } + + qDebug() << "DbChangeProcessor: Batch processing completed at" << QDateTime::currentDateTime().toString(); +} + +DbChangeProcessor::~DbChangeProcessor() +{ + if (m_batchTimer->isActive()) { + m_batchTimer->stop(); + } + // Procesar cualquier evento restante antes de destruir + processBatch(); +} \ No newline at end of file diff --git a/src/db/dbchangeprocessor.h b/src/db/dbchangeprocessor.h new file mode 100644 index 0000000..41620f3 --- /dev/null +++ b/src/db/dbchangeprocessor.h @@ -0,0 +1,51 @@ +#ifndef DBCHANGEPROCESSOR_H +#define DBCHANGEPROCESSOR_H + +#include +#include +#include +#include "core/eventbus.h" +#include "../core/events.h" +#include "../db/databasemanager.h" +#include "../db/dao/accountdao.h" +#include "../db/dao/folderdao.h" +#include "../db/dao/mailitemdao.h" + +class DbChangeProcessor : public QObject +{ + Q_OBJECT +public: + explicit DbChangeProcessor(QObject* parent = nullptr); + ~DbChangeProcessor() override = default; + +private slots: + void handleMailItemAdded(const WinoMail::Events::MailItemAddedEvent& event); + void handleMailItemRemoved(const WinoMail::Events::MailItemRemovedEvent& event); + void handleMailItemUpdated(const WinoMail::Events::MailItemUpdatedEvent& event); + void handleFolderAdded(const WinoMail::Events::FolderAddedEvent& event); + void handleFolderRemoved(const WinoMail::Events::FolderRemovedEvent& event); + void handleFolderUpdated(const WinoMail::Events::FolderUpdatedEvent& event); + void handleAccountAdded(const WinoMail::Events::AccountAddedEvent& event); + void handleAccountRemoved(const WinoMail::Events::AccountRemovedEvent& event); + void handleAccountUpdated(const WinoMail::Events::AccountUpdatedEvent& event); + void processBatch(); + +private: + DatabaseManager& m_db; + MailItemDao& m_mailItemDao; + FolderDao& m_folderDao; + AccountDao& m_accountDao; + + QTimer* m_batchTimer; + QVector m_mailItemAddedQueue; + QVector m_mailItemRemovedQueue; + QVector m_mailItemUpdatedQueue; + QVector m_folderAddedQueue; + QVector m_folderRemovedQueue; + QVector m_folderUpdatedQueue; + QVector m_accountAddedQueue; + QVector m_accountRemovedQueue; + QVector m_accountUpdatedQueue; +}; + +#endif // DBCHANGEPROCESSOR_H \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 4f665a1..3c5e8ff 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,7 @@ #include #include #include "core/translator.h" +#include "db/dbchangeprocessor.h" int main(int argc, char *argv[]) { @@ -13,8 +14,11 @@ int main(int argc, char *argv[]) qWarning() << "Failed to load translation"; } + // Initialize the DbChangeProcessor to start processing events in batches + DbChangeProcessor dbChangeProcessor(&app); + QQmlApplicationEngine engine; - engine.rootContext()->setContextProperty("translator", &translator); + engine.rootContext()->setContextProperty("translator", static_cast(&translator)); const QUrl url(QStringLiteral("qrc:/main.qml")); QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, diff --git a/src/services/gmail/gmailsynchronizer.cpp b/src/services/gmail/gmailsynchronizer.cpp index 066243d..78d8a00 100644 --- a/src/services/gmail/gmailsynchronizer.cpp +++ b/src/services/gmail/gmailsynchronizer.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -25,11 +26,6 @@ GmailSynchronizer::GmailSynchronizer(QObject* parent) m_historyTimer->setInterval(2 * 60 * 1000); // 2 minutos } -GmailSynchronizer::~GmailSynchronizer() -{ - m_historyTimer->stop(); -} - bool GmailSynchronizer::initialize(const Account& account) { // Guardar información de la cuenta @@ -60,10 +56,10 @@ bool GmailSynchronizer::syncFolder(const Folder& folder) } qDebug() << "Starting Gmail sync for folder (label):" << folder.name(); - emit folderSyncStarted(folder.id()); + emit folderSyncStarted(QString::number(folder.id())); // Obtener elementos de correo desde Gmail API - QVector items = fetchMailItems(folder.id()); + QVector items = fetchMailItems(QString::number(folder.id())); // En una implementación real, aquí compararíamos con la base de datos local // y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados @@ -75,7 +71,7 @@ bool GmailSynchronizer::syncFolder(const Folder& folder) } } - emit folderSyncFinished(folder.id(), true); + emit folderSyncFinished(QString::number(folder.id()), true); return true; } @@ -113,7 +109,7 @@ QVector GmailSynchronizer::fetchMailItems(const QString& folderId, // Construir la URL para Gmail API // Nota: En Gmail, las carpetas se identifican por su nombre de label (ej: INBOX, DRAFTS, etc.) - QString endpoint = String.format("/me/mailFolders/%1/messages", folderId); + QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId); QString url = buildGmailUrl(endpoint); // Parámetros de consulta @@ -257,7 +253,7 @@ void GmailSynchronizer::onHistoryTimer() // de todas las etiquetas (esto sería ineficiente en producción) QVector folders = getFolders(); for (const Folder& folder : folders) { - syncFolder(folder.id()); + syncFolder(Folder(folder.id(), 0, QString())); } } @@ -270,7 +266,7 @@ QString GmailSynchronizer::buildGmailUrl(const QString& endpoint) const QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const { - QNetworkRequest request(QUrl(url)); + QNetworkRequest request{QUrl(url)}; request.setRawHeader("Authorization", QString("Bearer %1").arg(m_accessToken).toUtf8()); request.setHeader(QNetworkRequest::ContentTypeHeader, diff --git a/src/services/imap/imapsynchronizer.cpp b/src/services/imap/imapsynchronizer.cpp index ee81603..51c3d54 100644 --- a/src/services/imap/imapsynchronizer.cpp +++ b/src/services/imap/imapsynchronizer.cpp @@ -1,5 +1,6 @@ #include "imapsynchronizer.h" #include +#include ImapSynchronizer::ImapSynchronizer(QObject* parent) : Synchronizer(parent) @@ -11,6 +12,15 @@ bool ImapSynchronizer::initialize(const Account& account) Q_UNUSED(account); qDebug() << "IMAP Synchronizer initialize (stub)"; m_connected = true; // pretend success + + // Publish AccountConnectedEvent + WinoMail::Events::AccountConnectedEvent event; + event.eventId = generateEventId(); + event.timestamp = QDateTime::currentDateTimeUtc(); + event.accountId = account.id(); + event.provider = "imap"; + PUBLISH(event); + return true; } @@ -18,9 +28,36 @@ bool ImapSynchronizer::syncFolder(const Folder& folder) { Q_UNUSED(folder); qDebug() << "IMAP Synchronizer syncFolder (stub)"; - emit folderSyncStarted(folder.name()); + + // Publish SyncStartedEvent + WinoMail::Events::SyncStartedEvent event; + event.eventId = generateEventId(); + event.timestamp = QDateTime::currentDateTimeUtc(); + event.accountId = m_account.id(); + event.folderId = QString::number(folder.id()); // Assuming folder.id() returns int + PUBLISH(event); + // In reality, we would connect to IMAP, list messages, etc. - emit folderSyncFinished(folder.name(), true); + // For now, simulate some mail items being added + QVector items = fetchMailItems(QString::number(folder.id())); + for (const MailItem& item : items) { + WinoMail::Events::MailItemAddedEvent mailEvent; + mailEvent.eventId = generateEventId(); + mailEvent.timestamp = QDateTime::currentDateTimeUtc(); + mailEvent.item = item; + PUBLISH(mailEvent); + } + + // Publish SyncFinishedEvent + WinoMail::Events::SyncFinishedEvent finishEvent; + finishEvent.eventId = generateEventId(); + finishEvent.timestamp = QDateTime::currentDateTimeUtc(); + finishEvent.accountId = m_account.id(); + finishEvent.folderId = QString::number(folder.id()); + finishEvent.success = true; + finishEvent.errorMessage = ""; + PUBLISH(finishEvent); + return true; } @@ -41,7 +78,7 @@ QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qDebug() << "IMAP Synchronizer fetchMailItems (stub)"; // Return a dummy mail item for testing QVector items; - items.append(MailItem(1, "Test Subject", "sender@example.com", "me@example.com", + items.append(MailItem(1, 1, "Test Subject", "sender@example.com", "me@example.com", QDateTime::currentDateTime(), false, false)); return items; } @@ -51,6 +88,13 @@ bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& i Q_UNUSED(folderId); Q_UNUSED(item); qDebug() << "IMAP Synchronizer appendMailItem (stub)"; + // In a real implementation, this would send the mail via IMAP APPEND + // For now, we'll simulate success and publish an event + WinoMail::Events::MailItemAddedEvent event; + event.eventId = generateEventId(); + event.timestamp = QDateTime::currentDateTimeUtc(); + event.item = item; + PUBLISH(event); return true; } @@ -63,6 +107,18 @@ bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, Q_UNUSED(read); Q_UNUSED(flagged); qDebug() << "IMAP Synchronizer updateMailItemFlags (stub)"; + // In a real implementation, this would update flags via IMAP STORE + // For now, we'll simulate success and publish an update event + // We need to fetch the item first to know what changed + MailItem item; // This would be fetched from storage + item.setId(itemUid.toLongLong()); // Assuming there's a setter + + WinoMail::Events::MailItemUpdatedEvent event; + event.eventId = generateEventId(); + event.timestamp = QDateTime::currentDateTimeUtc(); + event.item = item; + event.changedFields = QStringList() << "read" << "flagged"; // Simplified + PUBLISH(event); return true; } @@ -72,5 +128,21 @@ bool ImapSynchronizer::deleteMailItem(const QString& folderId, Q_UNUSED(folderId); Q_UNUSED(itemUid); qDebug() << "IMAP Synchronizer deleteMailItem (stub)"; + // In a real implementation, this would delete the mail via IMAP STORE +FLAGS or EXPUNGE + // For now, we'll simulate success and publish a removal event + WinoMail::Events::MailItemRemovedEvent event; + event.eventId = generateEventId(); + event.timestamp = QDateTime::currentDateTimeUtc(); + event.itemUid = itemUid; + event.folderId = folderId.toInt(); // Assuming folderId is numeric + PUBLISH(event); return true; +} + +// Helper para generar IDs únicos de eventos +QString ImapSynchronizer::generateEventId() const +{ + // Simple implementation using timestamp and random component + return QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + + QString::number(std::rand()); } \ No newline at end of file diff --git a/src/services/imap/imapsynchronizer.h b/src/services/imap/imapsynchronizer.h index 3325f53..79c23e9 100644 --- a/src/services/imap/imapsynchronizer.h +++ b/src/services/imap/imapsynchronizer.h @@ -2,6 +2,10 @@ #include "../synchronizer.h" #include +#include "../../core/eventbus.h" +#include "../../core/models/account.h" +#include "../../core/models/folder.h" +#include "../../core/mailitem.h" class ImapSynchronizer : public Synchronizer { @@ -31,4 +35,7 @@ private: QString m_username; QString m_password; bool m_useSsl{true}; + + // Helper para generar IDs únicos de eventos + QString generateEventId() const; }; \ No newline at end of file diff --git a/src/services/outlook/outlooksynchronizer.cpp b/src/services/outlook/outlooksynchronizer.cpp index 5f8e05e..b684165 100644 --- a/src/services/outlook/outlooksynchronizer.cpp +++ b/src/services/outlook/outlooksynchronizer.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -25,11 +26,6 @@ OutlookSynchronizer::OutlookSynchronizer(QObject* parent) m_deltaTimer->setInterval(5 * 60 * 1000); // 5 minutos } -OutlookSynchronizer::~OutlookSynchronizer() -{ - m_deltaTimer->stop(); -} - bool OutlookSynchronizer::initialize(const Account& account) { // Guardar información de la cuenta @@ -60,10 +56,10 @@ bool OutlookSynchronizer::syncFolder(const Folder& folder) } qDebug() << "Starting Outlook sync for folder:" << folder.name(); - emit folderSyncStarted(folder.id()); + emit folderSyncStarted(QString::number(folder.id())); // Obtener elementos de correo desde Microsoft Graph - QVector items = fetchMailItems(folder.id()); + QVector items = fetchMailItems(QString::number(folder.id())); // En una implementación real, aquí compararíamos con la base de datos local // y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados @@ -75,7 +71,7 @@ bool OutlookSynchronizer::syncFolder(const Folder& folder) } } - emit folderSyncFinished(folder.id(), true); + emit folderSyncFinished(QString::number(folder.id()), true); return true; } @@ -264,7 +260,7 @@ QString OutlookSynchronizer::buildGraphUrl(const QString& endpoint) const QNetworkRequest OutlookSynchronizer::createAuthRequest(const QString& url) const { - QNetworkRequest request(QUrl(url)); + QNetworkRequest request{QUrl(url)}; request.setRawHeader("Authorization", QString("Bearer %1").arg(m_accessToken).toUtf8()); request.setHeader(QNetworkRequest::ContentTypeHeader, diff --git a/src/ui/models/FolderListModel.cpp b/src/ui/models/FolderListModel.cpp new file mode 100644 index 0000000..a71d57c --- /dev/null +++ b/src/ui/models/FolderListModel.cpp @@ -0,0 +1,46 @@ +#include "FolderListModel.h" +#include + +FolderListModel::FolderListModel(QObject *parent) + : QAbstractListModel(parent), + m_folderDao(FolderDao::instance()) +{ + refresh(); +} + +int FolderListModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return m_folderNames.size(); +} + +QVariant FolderListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_folderNames.size()) + return QVariant(); + + if (role == FolderNameRole) + return m_folderNames.at(index.row()); + else if (role == UnreadCountRole) + return m_unreadCounts.at(index.row()); + return QVariant(); +} + +QHash FolderListModel::roleNames() const +{ + QHash roles; + roles[FolderNameRole] = "folderName"; + roles[UnreadCountRole] = "unreadCount"; + return roles; +} + +void FolderListModel::refresh() +{ + beginResetModel(); + // For now, we'll just use hardcoded folders until we implement the DAO properly + m_folderNames = {"Inbox", "Sent", "Drafts", "Trash", "Spam"}; + m_unreadCounts = {5, 0, 0, 0, 0}; + endResetModel(); + qDebug() << "FolderListModel refreshed with" << m_folderNames.size() << "folders"; +} \ No newline at end of file diff --git a/src/ui/models/FolderListModel.h b/src/ui/models/FolderListModel.h new file mode 100644 index 0000000..22a713b --- /dev/null +++ b/src/ui/models/FolderListModel.h @@ -0,0 +1,34 @@ +#ifndef FOLDERLISTMODEL_H +#define FOLDERLISTMODEL_H + +#include +#include +#include +#include "../db/dao/folderdao.h" + +class FolderListModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit FolderListModel(QObject *parent = nullptr); + ~FolderListModel() override = default; + + enum FolderRoles { + FolderNameRole = Qt::UserRole + 1, + UnreadCountRole + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + // Optional: method to refresh the model from the database + void refresh(); + +private: + QVector m_folderNames; + QVector m_unreadCounts; + FolderDao& m_folderDao; +}; + +#endif // FOLDERLISTMODEL_H \ No newline at end of file