From 6e4a588879acfbb2a8235c49cca83ade004d1ce8 Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 30 Aug 2026 02:26:31 +0200 Subject: [PATCH] feat: move-to-folder dialog, batch actions, reader source view, Focused/Other pivot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MoveToFolderDialog: modal folder picker per account (excludes current) - Wire 'Mover a...' in reader ⋯ menu + batch move (emit -1 -> dialog) - Connect batch delete/mark-unread/flag handlers (were unconnected) - Reader: 'Ver codigo fuente' reads .eml via MimeStorageService::readEmlFile - Reader: 'Marcar como no leido' emits markUnreadRequested - Pivot Focused/Other: EmailListModel::setShowFocusedOnly heuristic (flagged/pinned/frequent senders), reset to Focused on inbox select --- CMakeLists.txt | 3 + src/ui/maillistview.cpp | 30 ++++++--- src/ui/maillistview.h | 4 +- src/ui/mainmainwindow.cpp | 111 ++++++++++++++++++++++++++++++- src/ui/mainmainwindow.h | 5 ++ src/ui/models/EmailListModel.cpp | 16 +++++ src/ui/models/EmailListModel.h | 3 + src/ui/movetofolderdialog.cpp | 60 +++++++++++++++++ src/ui/movetofolderdialog.h | 24 +++++++ src/ui/readerview.cpp | 50 +++++++++++++- src/ui/readerview.h | 1 + 11 files changed, 292 insertions(+), 15 deletions(-) create mode 100644 src/ui/movetofolderdialog.cpp create mode 100644 src/ui/movetofolderdialog.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3dcc731..aa69a81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,6 +108,7 @@ set(SRC_FILES src/core/synchronizerprovider.cpp src/core/accountsetupdialoglauncher.cpp src/core/oauthcallbackserver.cpp + src/core/icloudauthenticator.cpp src/ui/mainmainwindow.cpp src/ui/mainmainwindow.h src/ui/newmessagedialog.cpp @@ -138,6 +139,8 @@ set(SRC_FILES src/ui/categorytreewidget.h src/ui/rulesmanagerdialog.cpp src/ui/rulesmanagerdialog.h + src/ui/movetofolderdialog.cpp + src/ui/movetofolderdialog.h src/ui/ruleditordialog.cpp src/ui/ruleditordialog.h src/ui/templateeditordialog.cpp diff --git a/src/ui/maillistview.cpp b/src/ui/maillistview.cpp index 993759a..8d19f53 100644 --- a/src/ui/maillistview.cpp +++ b/src/ui/maillistview.cpp @@ -660,9 +660,8 @@ void MailListView::onBatchMarkUnreadClicked() void MailListView::onBatchMoveClicked() { if (m_selectedMailIds.isEmpty()) return; - // TODO: Show folder picker dialog - // For now, just emit signal - // emit batchMoveRequested(m_selectedMailIds, targetFolderId); + // targetFolderId == -1 signals the UI layer to present a folder picker + emit batchMoveRequested(m_selectedMailIds, -1); exitMultiSelectMode(); } @@ -678,19 +677,30 @@ void MailListView::setFolderType(const QString& folderType) m_pivotCombo->setVisible(folderType == "inbox"); } +void MailListView::resetPivot() +{ + // Default to Focused (index 0). blockSignals to avoid recursive refresh. + m_pivotCombo->blockSignals(true); + m_pivotCombo->setCurrentIndex(0); + m_pivotCombo->blockSignals(false); + if (m_sourceModel) { + m_sourceModel->setShowFocusedOnly(true); + m_sourceModel->refresh(); + } + refreshViews(); +} + void MailListView::onPivotChanged(int index) { // 0 = Focused (Principal), 1 = Other (Otros) - // TODO: Implement pivot filtering logic - // For now, just update the model filter if (!m_sourceModel) return; - + if (index == 0) { - // Focused: important emails (could be based on sender frequency, AI, etc.) - // m_sourceModel->setShowFocusedOnly(true); + // Focused: important emails (flagged, pinned, or frequent senders) + m_sourceModel->setShowFocusedOnly(true); } else { - // Other: promotional, notifications, etc. - // m_sourceModel->setShowFocusedOnly(false); + // Other: show everything (promotional, notifications, etc.) + m_sourceModel->setShowFocusedOnly(false); } refreshViews(); } diff --git a/src/ui/maillistview.h b/src/ui/maillistview.h index 7f13ce3..1e4f136 100644 --- a/src/ui/maillistview.h +++ b/src/ui/maillistview.h @@ -51,7 +51,9 @@ signals: void onlineSearchRequested(const QString& query); public slots: - void setFolderType(const QString& folderType); // "inbox", "sent", "drafts", etc. + void setFolderType(const QString& folderType); + /// Reset pivot to Focused and hide for non-inbox folders. + void resetPivot(); // "inbox", "sent", "drafts", etc. private slots: void onRowSelected(const QModelIndex ¤t, const QModelIndex &previous); diff --git a/src/ui/mainmainwindow.cpp b/src/ui/mainmainwindow.cpp index 4eebb08..af07dce 100644 --- a/src/ui/mainmainwindow.cpp +++ b/src/ui/mainmainwindow.cpp @@ -8,6 +8,7 @@ #include "services/rulesengine.h" #include "ui/rulesmanagerdialog.h" #include "ui/accountsetupdialog.h" +#include "ui/movetofolderdialog.h" #include "ui/delegates/FolderTreeDelegate.h" #include "services/preferencesservice.h" #include "services/startupbehaviorservice.h" @@ -268,6 +269,10 @@ void MainMainWindow::setupMailPage() connect(m_mailListView, &MailListView::replyRequested, this, &MainMainWindow::onReplyRequested); connect(m_mailListView, &MailListView::forwardRequested, this, &MainMainWindow::onForwardRequested); connect(m_mailListView, &MailListView::markUnreadRequested, this, &MainMainWindow::onMarkUnreadRequested); + connect(m_mailListView, &MailListView::batchMoveRequested, this, &MainMainWindow::onBatchMoveRequested); + connect(m_mailListView, &MailListView::batchDeleteRequested, this, &MainMainWindow::onBatchDeleteRequested); + connect(m_mailListView, &MailListView::batchMarkUnreadRequested, this, &MainMainWindow::onBatchMarkUnreadRequested); + connect(m_mailListView, &MailListView::batchFlagRequested, this, &MainMainWindow::onBatchFlagRequested); m_folderSplitter->addWidget(m_mailListView); // Viewer stack: placeholder, reader, compose @@ -299,6 +304,7 @@ void MainMainWindow::setupMailPage() connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); connect(m_emailViewer, &ReaderView::forwardRequested, this, &MainMainWindow::onReaderForwardRequested); connect(m_emailViewer, &ReaderView::deleteRequested, this, &MainMainWindow::onReaderDeleteRequested); + connect(m_emailViewer, &ReaderView::markUnreadRequested, this, &MainMainWindow::onMarkUnreadRequested); connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { if (m_currentMailId >= 0) { openMailInIndependentWindow(m_currentMailId); @@ -412,6 +418,8 @@ void MainMainWindow::onFolderSelected(const QModelIndex &index) m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId)); // Tell MailListView the folder type for Focused/Other pivot m_mailListView->setFolderType(folder.type().toLower()); // "inbox", "sent", etc. + // Reset pivot to Focused for inbox folders + m_mailListView->resetPivot(); delete account; } } @@ -525,6 +533,107 @@ void MainMainWindow::onDeleteRequested(int mailId) statusBar()->showMessage(tr("Mensaje eliminado"), 3000); } +void MainMainWindow::onMoveMailToFolderRequested(int mailId) +{ + // Determine the account of the mail (via its current folder). + std::optional optMail = MailItemDao::findById(mailId); + if (!optMail) { + statusBar()->showMessage(tr("Mensaje no encontrado"), 3000); + return; + } + int currentFolderId = optMail->folderId(); + std::optional optFolder = FolderDao::findById(currentFolderId); + if (!optFolder) { + statusBar()->showMessage(tr("Carpeta no encontrada"), 3000); + return; + } + int accountId = optFolder->accountId(); + + QVector folders = FolderDao::findByAccountId(accountId); + MoveToFolderDialog dlg(folders, currentFolderId, this); + if (dlg.exec() != QDialog::Accepted) return; + int targetFolderId = dlg.selectedFolderId(); + if (targetFolderId < 0) return; + + m_mailService->moveMail(QString::number(mailId), QString::number(targetFolderId)); + // MailService::moveMail emits mailMoved; here we also refresh locally. + if (m_currentMailId == mailId) m_currentMailId = -1; + m_emailModel->refresh(); + m_mailListView->treeView()->clearSelection(); + m_viewerStack->setCurrentIndex(0); + statusBar()->showMessage(tr("Correo movido"), 3000); +} + +void MainMainWindow::onBatchMoveRequested(const QVector &mailIds, int targetFolderId) +{ + if (mailIds.isEmpty()) return; + + // Resolve target folder. targetFolderId == -1 → ask the user via dialog. + int resolvedTarget = targetFolderId; + if (resolvedTarget < 0) { + // Use the account of the first mail. + std::optional optMail = MailItemDao::findById(mailIds.first()); + if (!optMail) return; + std::optional optFolder = FolderDao::findById(optMail->folderId()); + if (!optFolder) return; + QVector folders = FolderDao::findByAccountId(optFolder->accountId()); + MoveToFolderDialog dlg(folders, optMail->folderId(), this); + if (dlg.exec() != QDialog::Accepted) return; + resolvedTarget = dlg.selectedFolderId(); + if (resolvedTarget < 0) return; + } + + for (int mailId : mailIds) { + m_mailService->moveMail(QString::number(mailId), QString::number(resolvedTarget)); + if (m_currentMailId == mailId) m_currentMailId = -1; + } + m_emailModel->refresh(); + m_mailListView->treeView()->clearSelection(); + m_viewerStack->setCurrentIndex(0); + statusBar()->showMessage(tr("%1 correo(s) movidos").arg(mailIds.size()), 3000); +} + +void MainMainWindow::onBatchDeleteRequested(const QVector &mailIds) +{ + if (mailIds.isEmpty()) return; + for (int mailId : mailIds) { + MailItemDao::remove(mailId); + if (m_currentMailId == mailId) m_currentMailId = -1; + } + m_emailModel->refresh(); + m_mailListView->treeView()->clearSelection(); + m_viewerStack->setCurrentIndex(0); + statusBar()->showMessage(tr("%1 correo(s) eliminados").arg(mailIds.size()), 3000); +} + +void MainMainWindow::onBatchMarkUnreadRequested(const QVector &mailIds) +{ + if (mailIds.isEmpty()) return; + for (int mailId : mailIds) { + auto item = MailItemDao::findById(mailId); + if (item.has_value()) { + item->setRead(false); + MailItemDao::update(*item); + } + } + m_emailModel->refresh(); + statusBar()->showMessage(tr("%1 correo(s) marcados como no leídos").arg(mailIds.size()), 3000); +} + +void MainMainWindow::onBatchFlagRequested(const QVector &mailIds, bool flagged) +{ + if (mailIds.isEmpty()) return; + for (int mailId : mailIds) { + auto item = MailItemDao::findById(mailId); + if (item.has_value()) { + item->setFlagged(flagged); + MailItemDao::update(*item); + } + } + m_emailModel->refresh(); + statusBar()->showMessage(tr("%1 correo(s) actualizados").arg(mailIds.size()), 3000); +} + void MainMainWindow::onCategoryRequested(int mailId) { // Show category context menu at cursor position @@ -538,7 +647,7 @@ void MainMainWindow::onMoreRequested(int mailId, const QPoint &globalPos) QMenu menu; menu.addAction("Responder", [this, mailId]() { onReaderReplyRequested(mailId); }); menu.addAction("Reenviar", [this, mailId]() { onReaderForwardRequested(mailId); }); - menu.addAction("Mover a...", [this, mailId]() { /* TODO: move dialog */ }); + menu.addAction("Mover a...", [this, mailId]() { onMoveMailToFolderRequested(mailId); }); menu.addAction("Marcar como no leído", [this, mailId]() { auto item = MailItemDao::findById(mailId); if (item.has_value()) { diff --git a/src/ui/mainmainwindow.h b/src/ui/mainmainwindow.h index 67e5f51..569f4f1 100644 --- a/src/ui/mainmainwindow.h +++ b/src/ui/mainmainwindow.h @@ -81,6 +81,11 @@ private slots: void onMoreRequested(int mailId, const QPoint &globalPos); void onReplyRequested(int mailId); void onForwardRequested(int mailId); + void onMoveMailToFolderRequested(int mailId); + void onBatchMoveRequested(const QVector &mailIds, int targetFolderId); + void onBatchDeleteRequested(const QVector &mailIds); + void onBatchMarkUnreadRequested(const QVector &mailIds); + void onBatchFlagRequested(const QVector &mailIds, bool flagged); void onMarkUnreadRequested(int mailId); // Online search void onOnlineSearchRequested(const QString& query); diff --git a/src/ui/models/EmailListModel.cpp b/src/ui/models/EmailListModel.cpp index 6c341ba..1857e3e 100644 --- a/src/ui/models/EmailListModel.cpp +++ b/src/ui/models/EmailListModel.cpp @@ -173,6 +173,22 @@ void EmailListModel::refresh() } m_emails = firstBatch; m_loadedCount = firstBatch.size(); + + // Apply Focused pivot heuristic in memory (important/frequent senders first). + if (m_focusedOnly) { + QVector focused; + // Count occurrences of each sender. + QHash senderCount; + for (const MailItem &m : m_emails) + senderCount[m.sender()]++; + for (const MailItem &m : m_emails) { + if (m.isFlagged() || m.isPinned() || senderCount.value(m.sender()) >= 2) + focused.append(m); + } + m_emails = focused; + m_loadedCount = focused.size(); + } + endResetModel(); qDebug() << "EmailListModel refreshed: total=" << m_totalCount << "loaded=" << m_loadedCount << "for folderId" << m_folderId; diff --git a/src/ui/models/EmailListModel.h b/src/ui/models/EmailListModel.h index 6e17e94..cb317d5 100644 --- a/src/ui/models/EmailListModel.h +++ b/src/ui/models/EmailListModel.h @@ -52,6 +52,8 @@ public: void setShowHasAttachments(bool show); // Set filter for pinned emails void setShowPinnedOnly(bool show); + // Focused/Other pivot heuristic + void setShowFocusedOnly(bool show) { m_focusedOnly = show; refresh(); } // Get all emails currently in the model (after filtering) const QVector& emails() const { return m_emails; } @@ -70,6 +72,7 @@ private: bool m_flaggedOnly{false}; bool m_hasAttachments{false}; bool m_pinnedOnly{false}; + bool m_focusedOnly{false}; int m_batchSize{50}; // emails per fetch int m_loadedCount{0}; // how many emails currently loaded int m_totalCount{0}; // total matching emails in DB diff --git a/src/ui/movetofolderdialog.cpp b/src/ui/movetofolderdialog.cpp new file mode 100644 index 0000000..f5e76f6 --- /dev/null +++ b/src/ui/movetofolderdialog.cpp @@ -0,0 +1,60 @@ +#include "ui/movetofolderdialog.h" +#include +#include +#include +#include +#include + +MoveToFolderDialog::MoveToFolderDialog(const QVector &folders, int excludeFolderId, + QWidget *parent) + : QDialog(parent) + , m_list(new QListWidget(this)) +{ + setWindowTitle(tr("Mover a carpeta")); + resize(320, 400); + + QVBoxLayout *lay = new QVBoxLayout(this); + + QLabel *title = new QLabel(tr("Selecciona la carpeta de destino:"), this); + lay->addWidget(title); + + m_list->setStyleSheet( + "QListWidget { border: 1px solid #d1d1d6; border-radius: 8px; padding: 4px; font-size: 13px; }" + "QListWidget::item { padding: 7px 8px; }" + "QListWidget::item:selected { background: #0071e3; color: #ffffff; border-radius: 6px; }" + ); + connect(m_list, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *) { accept(); }); + + for (const Folder &folder : folders) { + if (folder.id() == excludeFolderId) continue; + QListWidgetItem *item = new QListWidgetItem(folder.name(), m_list); + item->setData(Qt::UserRole, folder.id()); + m_list->addItem(item); + } + if (m_list->count() == 0) { + new QListWidgetItem(tr("(sin carpetas)"), m_list); + } + lay->addWidget(m_list, 1); + + QHBoxLayout *btnLay = new QHBoxLayout(); + QPushButton *cancel = new QPushButton(tr("Cancelar"), this); + connect(cancel, &QPushButton::clicked, this, &QDialog::reject); + btnLay->addWidget(cancel); + + QPushButton *ok = new QPushButton(tr("Mover"), this); + ok->setDefault(true); + connect(ok, &QPushButton::clicked, this, &MoveToFolderDialog::accept); + btnLay->addWidget(ok); + lay->addLayout(btnLay); + + if (m_list->count() > 0) m_list->setCurrentRow(0); +} + +void MoveToFolderDialog::accept() +{ + QListWidgetItem *item = m_list->currentItem(); + if (item && !item->data(Qt::UserRole).isNull()) { + m_selectedId = item->data(Qt::UserRole).toInt(); + } + QDialog::accept(); +} \ No newline at end of file diff --git a/src/ui/movetofolderdialog.h b/src/ui/movetofolderdialog.h new file mode 100644 index 0000000..04bf1f9 --- /dev/null +++ b/src/ui/movetofolderdialog.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include "core/models/folder.h" + +/// Modal dialog to pick a target folder for moving one or more mails. +class MoveToFolderDialog : public QDialog +{ + Q_OBJECT +public: + explicit MoveToFolderDialog(const QVector &folders, int excludeFolderId = -1, + QWidget *parent = nullptr); + + /// Target folder id, or -1 if cancelled. + int selectedFolderId() const { return m_selectedId; } + +private slots: + void accept() override; + +private: + QListWidget *m_list; + int m_selectedId = -1; +}; \ No newline at end of file diff --git a/src/ui/readerview.cpp b/src/ui/readerview.cpp index 97e7155..1f48a13 100644 --- a/src/ui/readerview.cpp +++ b/src/ui/readerview.cpp @@ -1,4 +1,5 @@ #include "ui/readerview.h" +#include "services/mimestorage.h" #include #include #include @@ -15,6 +16,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -325,10 +329,50 @@ void ReaderView::setupUI() { if (item.has_value()) QApplication::clipboard()->setText(item->sender()); } }); - moreMenu->addAction("Ver código fuente", [this]() { /* TODO */ }); + moreMenu->addAction("Ver código fuente", [this]() { + if (m_currentMailId < 0) return; + std::optional item = MailItemDao::findById(m_currentMailId); + if (!item.has_value()) return; + QByteArray raw; + if (!item->fileId().isEmpty()) { + MimeStorageService storage; + raw = storage.readEmlFile(item->fileId()); + } + if (raw.isEmpty()) raw = item->rawMime(); + if (raw.isEmpty()) { + QMessageBox::information(this, tr("Código fuente"), + tr("No hay código fuente disponible para este correo.")); + return; + } + QDialog dlg(this); + dlg.setWindowTitle(tr("Código fuente")); + dlg.resize(760, 560); + QVBoxLayout *lay = new QVBoxLayout(&dlg); + QPlainTextEdit *view = new QPlainTextEdit(&dlg); + view->setReadOnly(true); + view->setPlainText(QString::fromUtf8(raw)); + view->setStyleSheet("font-family: 'Consolas','Menlo',monospace; font-size: 12px;"); + lay->addWidget(view); + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dlg); + connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); + lay->addWidget(buttons); + dlg.exec(); + }); moreMenu->addSeparator(); - moreMenu->addAction("Marcar como no leído", [this]() { /* TODO */ }); - moreMenu->addAction("Marcar como spam", [this]() { /* TODO */ }); + moreMenu->addAction("Marcar como no leído", [this]() { + if (m_currentMailId < 0) return; + auto item = MailItemDao::findById(m_currentMailId); + if (item.has_value()) { + item->setRead(false); + MailItemDao::update(*item); + emit markUnreadRequested(m_currentMailId); + } + }); + moreMenu->addAction("Marcar como spam", [this]() { + if (m_currentMailId < 0) return; + QMessageBox::information(this, tr("Marcar como spam"), + tr("Función: mover a carpeta «Spam». Disponible en una próxima versión.")); + }); m_moreButton->setMenu(moreMenu); m_moreButton->setStyleSheet( "QToolButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; font-size: 16px; }" diff --git a/src/ui/readerview.h b/src/ui/readerview.h index b1c086a..19d0665 100644 --- a/src/ui/readerview.h +++ b/src/ui/readerview.h @@ -29,6 +29,7 @@ signals: void forwardRequested(int mailId); void deleteRequested(int mailId); void detachRequested(); + void markUnreadRequested(int mailId); void openAttachmentRequested(const QString &path); private slots: