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:
@@ -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
|
||||
|
||||
+19
-9
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+110
-1
@@ -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<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)
|
||||
{
|
||||
// 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()) {
|
||||
|
||||
@@ -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<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);
|
||||
// Online search
|
||||
void onOnlineSearchRequested(const QString& query);
|
||||
|
||||
@@ -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<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();
|
||||
|
||||
qDebug() << "EmailListModel refreshed: total=" << m_totalCount << "loaded=" << m_loadedCount << "for folderId" << m_folderId;
|
||||
|
||||
@@ -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<MailItem>& 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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -1,4 +1,5 @@
|
||||
#include "ui/readerview.h"
|
||||
#include "services/mimestorage.h"
|
||||
#include <QFont>
|
||||
#include <QDesktopServices>
|
||||
#include <QUrl>
|
||||
@@ -15,6 +16,9 @@
|
||||
#include <QDateTime>
|
||||
#include <QClipboard>
|
||||
#include <QMessageBox>
|
||||
#include <QDialog>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QScrollBar>
|
||||
#include <QShortcut>
|
||||
#include <QFileDialog>
|
||||
@@ -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<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->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; }"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user