feat: move-to-folder dialog, batch actions, reader source view, Focused/Other pivot

- 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
This commit is contained in:
2026-08-30 02:26:31 +02:00
parent 59692b5706
commit 6e4a588879
11 changed files with 292 additions and 15 deletions
+3
View File
@@ -108,6 +108,7 @@ set(SRC_FILES
src/core/synchronizerprovider.cpp src/core/synchronizerprovider.cpp
src/core/accountsetupdialoglauncher.cpp src/core/accountsetupdialoglauncher.cpp
src/core/oauthcallbackserver.cpp src/core/oauthcallbackserver.cpp
src/core/icloudauthenticator.cpp
src/ui/mainmainwindow.cpp src/ui/mainmainwindow.cpp
src/ui/mainmainwindow.h src/ui/mainmainwindow.h
src/ui/newmessagedialog.cpp src/ui/newmessagedialog.cpp
@@ -138,6 +139,8 @@ set(SRC_FILES
src/ui/categorytreewidget.h src/ui/categorytreewidget.h
src/ui/rulesmanagerdialog.cpp src/ui/rulesmanagerdialog.cpp
src/ui/rulesmanagerdialog.h src/ui/rulesmanagerdialog.h
src/ui/movetofolderdialog.cpp
src/ui/movetofolderdialog.h
src/ui/ruleditordialog.cpp src/ui/ruleditordialog.cpp
src/ui/ruleditordialog.h src/ui/ruleditordialog.h
src/ui/templateeditordialog.cpp src/ui/templateeditordialog.cpp
+19 -9
View File
@@ -660,9 +660,8 @@ void MailListView::onBatchMarkUnreadClicked()
void MailListView::onBatchMoveClicked() void MailListView::onBatchMoveClicked()
{ {
if (m_selectedMailIds.isEmpty()) return; if (m_selectedMailIds.isEmpty()) return;
// TODO: Show folder picker dialog // targetFolderId == -1 signals the UI layer to present a folder picker
// For now, just emit signal emit batchMoveRequested(m_selectedMailIds, -1);
// emit batchMoveRequested(m_selectedMailIds, targetFolderId);
exitMultiSelectMode(); exitMultiSelectMode();
} }
@@ -678,19 +677,30 @@ void MailListView::setFolderType(const QString& folderType)
m_pivotCombo->setVisible(folderType == "inbox"); 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) void MailListView::onPivotChanged(int index)
{ {
// 0 = Focused (Principal), 1 = Other (Otros) // 0 = Focused (Principal), 1 = Other (Otros)
// TODO: Implement pivot filtering logic
// For now, just update the model filter
if (!m_sourceModel) return; if (!m_sourceModel) return;
if (index == 0) { if (index == 0) {
// Focused: important emails (could be based on sender frequency, AI, etc.) // Focused: important emails (flagged, pinned, or frequent senders)
// m_sourceModel->setShowFocusedOnly(true); m_sourceModel->setShowFocusedOnly(true);
} else { } else {
// Other: promotional, notifications, etc. // Other: show everything (promotional, notifications, etc.)
// m_sourceModel->setShowFocusedOnly(false); m_sourceModel->setShowFocusedOnly(false);
} }
refreshViews(); refreshViews();
} }
+3 -1
View File
@@ -51,7 +51,9 @@ signals:
void onlineSearchRequested(const QString& query); void onlineSearchRequested(const QString& query);
public slots: 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: private slots:
void onRowSelected(const QModelIndex &current, const QModelIndex &previous); void onRowSelected(const QModelIndex &current, const QModelIndex &previous);
+110 -1
View File
@@ -8,6 +8,7 @@
#include "services/rulesengine.h" #include "services/rulesengine.h"
#include "ui/rulesmanagerdialog.h" #include "ui/rulesmanagerdialog.h"
#include "ui/accountsetupdialog.h" #include "ui/accountsetupdialog.h"
#include "ui/movetofolderdialog.h"
#include "ui/delegates/FolderTreeDelegate.h" #include "ui/delegates/FolderTreeDelegate.h"
#include "services/preferencesservice.h" #include "services/preferencesservice.h"
#include "services/startupbehaviorservice.h" #include "services/startupbehaviorservice.h"
@@ -268,6 +269,10 @@ void MainMainWindow::setupMailPage()
connect(m_mailListView, &MailListView::replyRequested, this, &MainMainWindow::onReplyRequested); connect(m_mailListView, &MailListView::replyRequested, this, &MainMainWindow::onReplyRequested);
connect(m_mailListView, &MailListView::forwardRequested, this, &MainMainWindow::onForwardRequested); connect(m_mailListView, &MailListView::forwardRequested, this, &MainMainWindow::onForwardRequested);
connect(m_mailListView, &MailListView::markUnreadRequested, this, &MainMainWindow::onMarkUnreadRequested); 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); m_folderSplitter->addWidget(m_mailListView);
// Viewer stack: placeholder, reader, compose // Viewer stack: placeholder, reader, compose
@@ -299,6 +304,7 @@ void MainMainWindow::setupMailPage()
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::forwardRequested, this, &MainMainWindow::onReaderForwardRequested); connect(m_emailViewer, &ReaderView::forwardRequested, this, &MainMainWindow::onReaderForwardRequested);
connect(m_emailViewer, &ReaderView::deleteRequested, this, &MainMainWindow::onReaderDeleteRequested); connect(m_emailViewer, &ReaderView::deleteRequested, this, &MainMainWindow::onReaderDeleteRequested);
connect(m_emailViewer, &ReaderView::markUnreadRequested, this, &MainMainWindow::onMarkUnreadRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) { if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId); openMailInIndependentWindow(m_currentMailId);
@@ -412,6 +418,8 @@ void MainMainWindow::onFolderSelected(const QModelIndex &index)
m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId)); m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId));
// Tell MailListView the folder type for Focused/Other pivot // Tell MailListView the folder type for Focused/Other pivot
m_mailListView->setFolderType(folder.type().toLower()); // "inbox", "sent", etc. m_mailListView->setFolderType(folder.type().toLower()); // "inbox", "sent", etc.
// Reset pivot to Focused for inbox folders
m_mailListView->resetPivot();
delete account; delete account;
} }
} }
@@ -525,6 +533,107 @@ void MainMainWindow::onDeleteRequested(int mailId)
statusBar()->showMessage(tr("Mensaje eliminado"), 3000); statusBar()->showMessage(tr("Mensaje eliminado"), 3000);
} }
void MainMainWindow::onMoveMailToFolderRequested(int mailId)
{
// Determine the account of the mail (via its current folder).
std::optional<MailItem> optMail = MailItemDao::findById(mailId);
if (!optMail) {
statusBar()->showMessage(tr("Mensaje no encontrado"), 3000);
return;
}
int currentFolderId = optMail->folderId();
std::optional<Folder> optFolder = FolderDao::findById(currentFolderId);
if (!optFolder) {
statusBar()->showMessage(tr("Carpeta no encontrada"), 3000);
return;
}
int accountId = optFolder->accountId();
QVector<Folder> 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<int> &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<MailItem> optMail = MailItemDao::findById(mailIds.first());
if (!optMail) return;
std::optional<Folder> optFolder = FolderDao::findById(optMail->folderId());
if (!optFolder) return;
QVector<Folder> 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<int> &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<int> &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<int> &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) void MainMainWindow::onCategoryRequested(int mailId)
{ {
// Show category context menu at cursor position // Show category context menu at cursor position
@@ -538,7 +647,7 @@ void MainMainWindow::onMoreRequested(int mailId, const QPoint &globalPos)
QMenu menu; QMenu menu;
menu.addAction("Responder", [this, mailId]() { onReaderReplyRequested(mailId); }); menu.addAction("Responder", [this, mailId]() { onReaderReplyRequested(mailId); });
menu.addAction("Reenviar", [this, mailId]() { onReaderForwardRequested(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]() { menu.addAction("Marcar como no leído", [this, mailId]() {
auto item = MailItemDao::findById(mailId); auto item = MailItemDao::findById(mailId);
if (item.has_value()) { if (item.has_value()) {
+5
View File
@@ -81,6 +81,11 @@ private slots:
void onMoreRequested(int mailId, const QPoint &globalPos); void onMoreRequested(int mailId, const QPoint &globalPos);
void onReplyRequested(int mailId); void onReplyRequested(int mailId);
void onForwardRequested(int mailId); void onForwardRequested(int mailId);
void onMoveMailToFolderRequested(int mailId);
void onBatchMoveRequested(const QVector<int> &mailIds, int targetFolderId);
void onBatchDeleteRequested(const QVector<int> &mailIds);
void onBatchMarkUnreadRequested(const QVector<int> &mailIds);
void onBatchFlagRequested(const QVector<int> &mailIds, bool flagged);
void onMarkUnreadRequested(int mailId); void onMarkUnreadRequested(int mailId);
// Online search // Online search
void onOnlineSearchRequested(const QString& query); void onOnlineSearchRequested(const QString& query);
+16
View File
@@ -173,6 +173,22 @@ void EmailListModel::refresh()
} }
m_emails = firstBatch; m_emails = firstBatch;
m_loadedCount = firstBatch.size(); m_loadedCount = firstBatch.size();
// Apply Focused pivot heuristic in memory (important/frequent senders first).
if (m_focusedOnly) {
QVector<MailItem> focused;
// Count occurrences of each sender.
QHash<QString, int> 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(); endResetModel();
qDebug() << "EmailListModel refreshed: total=" << m_totalCount << "loaded=" << m_loadedCount << "for folderId" << m_folderId; qDebug() << "EmailListModel refreshed: total=" << m_totalCount << "loaded=" << m_loadedCount << "for folderId" << m_folderId;
+3
View File
@@ -52,6 +52,8 @@ public:
void setShowHasAttachments(bool show); void setShowHasAttachments(bool show);
// Set filter for pinned emails // Set filter for pinned emails
void setShowPinnedOnly(bool show); 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) // Get all emails currently in the model (after filtering)
const QVector<MailItem>& emails() const { return m_emails; } const QVector<MailItem>& emails() const { return m_emails; }
@@ -70,6 +72,7 @@ private:
bool m_flaggedOnly{false}; bool m_flaggedOnly{false};
bool m_hasAttachments{false}; bool m_hasAttachments{false};
bool m_pinnedOnly{false}; bool m_pinnedOnly{false};
bool m_focusedOnly{false};
int m_batchSize{50}; // emails per fetch int m_batchSize{50}; // emails per fetch
int m_loadedCount{0}; // how many emails currently loaded int m_loadedCount{0}; // how many emails currently loaded
int m_totalCount{0}; // total matching emails in DB int m_totalCount{0}; // total matching emails in DB
+60
View File
@@ -0,0 +1,60 @@
#include "ui/movetofolderdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QListWidgetItem>
MoveToFolderDialog::MoveToFolderDialog(const QVector<Folder> &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();
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <QDialog>
#include <QListWidget>
#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<Folder> &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;
};
+47 -3
View File
@@ -1,4 +1,5 @@
#include "ui/readerview.h" #include "ui/readerview.h"
#include "services/mimestorage.h"
#include <QFont> #include <QFont>
#include <QDesktopServices> #include <QDesktopServices>
#include <QUrl> #include <QUrl>
@@ -15,6 +16,9 @@
#include <QDateTime> #include <QDateTime>
#include <QClipboard> #include <QClipboard>
#include <QMessageBox> #include <QMessageBox>
#include <QDialog>
#include <QPlainTextEdit>
#include <QDialogButtonBox>
#include <QScrollBar> #include <QScrollBar>
#include <QShortcut> #include <QShortcut>
#include <QFileDialog> #include <QFileDialog>
@@ -325,10 +329,50 @@ void ReaderView::setupUI() {
if (item.has_value()) QApplication::clipboard()->setText(item->sender()); 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<MailItem> 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->addSeparator();
moreMenu->addAction("Marcar como no leído", [this]() { /* TODO */ }); moreMenu->addAction("Marcar como no leído", [this]() {
moreMenu->addAction("Marcar como spam", [this]() { /* TODO */ }); 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->setMenu(moreMenu);
m_moreButton->setStyleSheet( m_moreButton->setStyleSheet(
"QToolButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; font-size: 16px; }" "QToolButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; font-size: 16px; }"
+1
View File
@@ -29,6 +29,7 @@ signals:
void forwardRequested(int mailId); void forwardRequested(int mailId);
void deleteRequested(int mailId); void deleteRequested(int mailId);
void detachRequested(); void detachRequested();
void markUnreadRequested(int mailId);
void openAttachmentRequested(const QString &path); void openAttachmentRequested(const QString &path);
private slots: private slots: