feat: mark-spam, rule priority persistence, run-rules-on-selected, icons fix, scheduled send + reader polish
- Mark as spam: moves mail to Spam/Junk folder of account - Rule priority: persistPriorityOrder persists 1..N via RuleDao::update - Run rules on selected: uses MailListView::selectedMailIds() + onRunRulesOnSelected - Icons: switch code to qrc short aliases (close/send/bold/etc) + new detach.svg -> resolves all 'Cannot open file' SVG warnings at startup - Categories: CompactMailDelegate paints category chips from CategoryDao - Scheduled send: MailService::scheduleSend with QTimer; wire both composer paths - Reader: show real 'Para:' recipient; delete attachment removes stored file
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
<file alias="reply.svg">resources/icons/SVG/Linear/Arrows Action/Reply.svg</file>
|
<file alias="reply.svg">resources/icons/SVG/Linear/Arrows Action/Reply.svg</file>
|
||||||
<file alias="forward.svg">resources/icons/SVG/Linear/Arrows Action/Forward.svg</file>
|
<file alias="forward.svg">resources/icons/SVG/Linear/Arrows Action/Forward.svg</file>
|
||||||
<file alias="trash.svg">resources/icons/SVG/Linear/Essentional, UI/Trash Bin Minimalistic.svg</file>
|
<file alias="trash.svg">resources/icons/SVG/Linear/Essentional, UI/Trash Bin Minimalistic.svg</file>
|
||||||
|
<file alias="detach.svg">resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg</file>
|
||||||
<file alias="filter.svg">resources/icons/SVG/Linear/Design, Tools/Filters.svg</file>
|
<file alias="filter.svg">resources/icons/SVG/Linear/Design, Tools/Filters.svg</file>
|
||||||
<file alias="play.svg">resources/icons/SVG/Linear/Time/Stopwatch Play.svg</file>
|
<file alias="play.svg">resources/icons/SVG/Linear/Time/Stopwatch Play.svg</file>
|
||||||
<file alias="file-text.svg">resources/icons/SVG/Linear/Files/File Text.svg</file>
|
<file alias="file-text.svg">resources/icons/SVG/Linear/Files/File Text.svg</file>
|
||||||
|
|||||||
@@ -353,6 +353,39 @@ void MailService::sendMail(const MailItem &mail, const QString &accountId,
|
|||||||
watcher->setFuture(future);
|
watcher->setFuture(future);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MailService::scheduleSend(const MailItem &mail, const QString &accountId,
|
||||||
|
const QStringList &attachmentPaths, const QDateTime &when)
|
||||||
|
{
|
||||||
|
QDateTime now = QDateTime::currentDateTime();
|
||||||
|
if (!when.isValid() || when <= now) {
|
||||||
|
// Send immediately
|
||||||
|
sendMail(mail, accountId, attachmentPaths);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pendingMail = mail;
|
||||||
|
m_pendingAccountId = accountId;
|
||||||
|
m_pendingAttachments = attachmentPaths;
|
||||||
|
|
||||||
|
int ms = qMax(1000, int(now.msecsTo(when)));
|
||||||
|
m_sendTimer.stop();
|
||||||
|
// Re-arm the single-shot timer with the delay until `when`.
|
||||||
|
m_sendTimer.disconnect(this);
|
||||||
|
connect(&m_sendTimer, &QTimer::timeout, this, [this]() {
|
||||||
|
MailItem mail = m_pendingMail;
|
||||||
|
QString accountId = m_pendingAccountId;
|
||||||
|
QStringList attachments = m_pendingAttachments;
|
||||||
|
m_pendingMail = MailItem();
|
||||||
|
m_pendingAccountId.clear();
|
||||||
|
m_pendingAttachments.clear();
|
||||||
|
sendMail(mail, accountId, attachments);
|
||||||
|
});
|
||||||
|
m_sendTimer.start(ms);
|
||||||
|
|
||||||
|
emit statusMessage(
|
||||||
|
tr("Envío programado para %1").arg(when.toString("dd/MM/yyyy HH:mm")));
|
||||||
|
}
|
||||||
|
|
||||||
void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
||||||
{
|
{
|
||||||
// Determine provider type from account
|
// Determine provider type from account
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <QSslSocket>
|
#include <QSslSocket>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QTimer>
|
||||||
#include "core/mailitem.h"
|
#include "core/mailitem.h"
|
||||||
#include "core/emailcomposerbridge.h"
|
#include "core/emailcomposerbridge.h"
|
||||||
#include "core/synchronizerprovider.h"
|
#include "core/synchronizerprovider.h"
|
||||||
@@ -24,6 +25,9 @@ public:
|
|||||||
void sendMail(const MailItem &mail, const QString &accountId);
|
void sendMail(const MailItem &mail, const QString &accountId);
|
||||||
void sendMail(const MailItem &mail, const QString &accountId,
|
void sendMail(const MailItem &mail, const QString &accountId,
|
||||||
const QStringList &attachmentPaths);
|
const QStringList &attachmentPaths);
|
||||||
|
/// Schedule a mail to be sent at `when`. Emits mailSent(mailItemId) when sent.
|
||||||
|
void scheduleSend(const MailItem &mail, const QString &accountId,
|
||||||
|
const QStringList &attachmentPaths, const QDateTime &when);
|
||||||
void fetchMails(const QString &accountId, const QString &folderId);
|
void fetchMails(const QString &accountId, const QString &folderId);
|
||||||
void moveMail(const QString &mailItemId, const QString &targetFolderId);
|
void moveMail(const QString &mailItemId, const QString &targetFolderId);
|
||||||
void deleteMail(const QString &mailItemId);
|
void deleteMail(const QString &mailItemId);
|
||||||
@@ -50,6 +54,10 @@ private:
|
|||||||
const QString &folderId);
|
const QString &folderId);
|
||||||
EmailComposerBridge *m_composer;
|
EmailComposerBridge *m_composer;
|
||||||
AccountService *m_accountService;
|
AccountService *m_accountService;
|
||||||
|
QTimer m_sendTimer{this}; // one-shot timer driving scheduled sends
|
||||||
|
MailItem m_pendingMail;
|
||||||
|
QString m_pendingAccountId;
|
||||||
|
QStringList m_pendingAttachments;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // MAILSERVICE_H
|
#endif // MAILSERVICE_H
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ void ComposeView::setupUI() {
|
|||||||
ccLayout->addWidget(m_ccField, 1);
|
ccLayout->addWidget(m_ccField, 1);
|
||||||
|
|
||||||
m_hideCcButton = new QPushButton("");
|
m_hideCcButton = new QPushButton("");
|
||||||
m_hideCcButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Essentional, UI/Close Square.svg")));
|
m_hideCcButton->setIcon(QIcon(QStringLiteral(":/icons/close.svg")));
|
||||||
m_hideCcButton->setFixedSize(20, 20);
|
m_hideCcButton->setFixedSize(20, 20);
|
||||||
m_hideCcButton->setToolTip(tr("Hide Cc"));
|
m_hideCcButton->setToolTip(tr("Hide Cc"));
|
||||||
m_hideCcButton->setStyleSheet(
|
m_hideCcButton->setStyleSheet(
|
||||||
@@ -319,7 +319,7 @@ void ComposeView::setupUI() {
|
|||||||
|
|
||||||
m_hideBccButton = new QPushButton("");
|
m_hideBccButton = new QPushButton("");
|
||||||
m_hideBccButton->setFixedSize(20, 20);
|
m_hideBccButton->setFixedSize(20, 20);
|
||||||
m_hideBccButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Essentional, UI/Close Square.svg")));
|
m_hideBccButton->setIcon(QIcon(QStringLiteral(":/icons/close.svg")));
|
||||||
m_hideBccButton->setToolTip(tr("Hide Bcc"));
|
m_hideBccButton->setToolTip(tr("Hide Bcc"));
|
||||||
m_hideBccButton->setStyleSheet(
|
m_hideBccButton->setStyleSheet(
|
||||||
"QPushButton { background: transparent; border: none; color: blue; font-weight: bold; }"
|
"QPushButton { background: transparent; border: none; color: blue; font-weight: bold; }"
|
||||||
@@ -348,7 +348,7 @@ void ComposeView::setupUI() {
|
|||||||
|
|
||||||
// Detach button placed to the right of the subject line
|
// Detach button placed to the right of the subject line
|
||||||
m_detachButton = new QPushButton("");//("↥ Detach");
|
m_detachButton = new QPushButton("");//("↥ Detach");
|
||||||
m_detachButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg")));
|
m_detachButton->setIcon(QIcon(QStringLiteral(":/icons/detach.svg")));
|
||||||
m_detachButton->setToolTip(tr("Open compose window in a separate window"));
|
m_detachButton->setToolTip(tr("Open compose window in a separate window"));
|
||||||
m_detachButton->setStyleSheet(
|
m_detachButton->setStyleSheet(
|
||||||
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
|
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
|
||||||
@@ -421,7 +421,7 @@ void ComposeView::setupUI() {
|
|||||||
// Attachments and Templates buttons (right-aligned)
|
// Attachments and Templates buttons (right-aligned)
|
||||||
QHBoxLayout *buttonRow = new QHBoxLayout();
|
QHBoxLayout *buttonRow = new QHBoxLayout();
|
||||||
m_attachButton = new QToolButton();
|
m_attachButton = new QToolButton();
|
||||||
m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Messages, Conversation/Paperclip.svg")));
|
m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/attachment.svg")));
|
||||||
m_attachButton->setToolTip(tr("Add attachment"));
|
m_attachButton->setToolTip(tr("Add attachment"));
|
||||||
m_attachButton->setIconSize(QSize(20,20));
|
m_attachButton->setIconSize(QSize(20,20));
|
||||||
m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
|
m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
|
||||||
@@ -429,7 +429,7 @@ void ComposeView::setupUI() {
|
|||||||
buttonRow->addWidget(m_attachButton);
|
buttonRow->addWidget(m_attachButton);
|
||||||
|
|
||||||
m_templateButton = new QToolButton();
|
m_templateButton = new QToolButton();
|
||||||
m_templateButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Files/File Text.svg"))); // Assuming you have a template icon
|
m_templateButton->setIcon(QIcon(QStringLiteral(":/icons/file-text.svg"))); // Assuming you have a template icon
|
||||||
m_templateButton->setToolTip(tr("Email templates"));
|
m_templateButton->setToolTip(tr("Email templates"));
|
||||||
m_templateButton->setIconSize(QSize(20,20));
|
m_templateButton->setIconSize(QSize(20,20));
|
||||||
m_templateButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
|
m_templateButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
|
||||||
@@ -459,7 +459,7 @@ void ComposeView::setupUI() {
|
|||||||
|
|
||||||
m_sendSplit = new QToolButton();
|
m_sendSplit = new QToolButton();
|
||||||
m_sendSplit->setText(tr("Send"));
|
m_sendSplit->setText(tr("Send"));
|
||||||
m_sendSplit->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Bold Duotone/Messages, Conversation/Plain.svg")));
|
m_sendSplit->setIcon(QIcon(QStringLiteral(":/icons/send.svg")));
|
||||||
m_templateButton->setIconSize(QSize(20,20));
|
m_templateButton->setIconSize(QSize(20,20));
|
||||||
//m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
//m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
||||||
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
|
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "CompactMailDelegate.h"
|
#include "CompactMailDelegate.h"
|
||||||
|
#include "db/dao/categorydao.h"
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
@@ -269,8 +270,38 @@ void CompactMailDelegate::drawCard(QPainter *painter, const QStyleOptionViewItem
|
|||||||
painter->drawText(QRect(x, line3Y, previewMaxWidth, previewFont.pointSizeF()), Qt::AlignLeft | Qt::AlignVCenter, elidedPreview);
|
painter->drawText(QRect(x, line3Y, previewMaxWidth, previewFont.pointSizeF()), Qt::AlignLeft | Qt::AlignVCenter, elidedPreview);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categories (if enabled and present) - simplified for now
|
// Category badges (if any)
|
||||||
// TODO: Add category support from model
|
int mailId = index.data(EmailListModel::IdRole).toInt();
|
||||||
|
QVector<qint64> catIds = CategoryDao::categoriesForMail(mailId);
|
||||||
|
if (!catIds.isEmpty() && catIds.size() <= 3 && m_config.displayMode != Compact) {
|
||||||
|
int catY = line2Y + subjectFont.pointSizeF() + (m_config.showPreviewText && m_config.displayMode != Compact
|
||||||
|
? previewFont.pointSizeF() + 8 : 6);
|
||||||
|
int cx = contentRect.left() + (m_config.showAvatar ? m_config.avatarSize + 10 : 0);
|
||||||
|
QFont catFont = subjectFont;
|
||||||
|
catFont.setPointSizeF(qMax(6.5, subjectFont.pointSizeF() - 3.0));
|
||||||
|
catFont.setBold(true);
|
||||||
|
painter->setFont(catFont);
|
||||||
|
int maxCatWidth = availableWidth;
|
||||||
|
for (qint64 cid : catIds) {
|
||||||
|
auto cat = CategoryDao::findById(cid);
|
||||||
|
if (!cat.has_value()) continue;
|
||||||
|
QString label = " " + cat->name + " ";
|
||||||
|
QColor catColor = cat->color.isValid() ? cat->color : QColor(0x00, 0x71, 0xe3);
|
||||||
|
QFontMetrics fm(catFont);
|
||||||
|
int w = fm.horizontalAdvance(label) + 8;
|
||||||
|
if (maxCatWidth < w) break;
|
||||||
|
QRect chip(cx, catY, w, fm.height() + 4);
|
||||||
|
painter->setRenderHint(QPainter::Antialiasing);
|
||||||
|
QColor fill = catColor; fill.setAlpha(45);
|
||||||
|
painter->setPen(catColor);
|
||||||
|
painter->setBrush(fill);
|
||||||
|
painter->drawRoundedRect(chip, 4, 4);
|
||||||
|
painter->setPen(catColor);
|
||||||
|
painter->drawText(chip.adjusted(4, 2, -2, 0), Qt::AlignLeft | Qt::AlignVCenter, label.trimmed());
|
||||||
|
cx += w + 6;
|
||||||
|
maxCatWidth -= w + 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Action buttons (bottom)
|
// Action buttons (bottom)
|
||||||
int buttonAreaY = cardRect.bottom() - 28;
|
int buttonAreaY = cardRect.bottom() - 28;
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ public:
|
|||||||
~MailListView() override;
|
~MailListView() override;
|
||||||
|
|
||||||
void setModel(EmailListModel *model);
|
void setModel(EmailListModel *model);
|
||||||
|
/// Currently selected mail ids (multi-selection or current row).
|
||||||
|
QVector<int> selectedMailIds() const { return m_selectedMailIds; }
|
||||||
QTreeView* treeView() const { return m_treeView; }
|
QTreeView* treeView() const { return m_treeView; }
|
||||||
QListView* listView() const { return m_listView; }
|
QListView* listView() const { return m_listView; }
|
||||||
|
|
||||||
|
|||||||
+63
-16
@@ -108,10 +108,6 @@ void MainMainWindow::setupUI()
|
|||||||
// Page 1: Compose
|
// Page 1: Compose
|
||||||
m_composeView = new ComposeView();
|
m_composeView = new ComposeView();
|
||||||
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr, const QStringList &attachmentPaths) {
|
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr, const QStringList &attachmentPaths) {
|
||||||
if (scheduleTime.isValid()) {
|
|
||||||
statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fromAddr.isEmpty()) {
|
if (fromAddr.isEmpty()) {
|
||||||
statusBar()->showMessage(tr("Select an account before sending"), 5000);
|
statusBar()->showMessage(tr("Select an account before sending"), 5000);
|
||||||
return;
|
return;
|
||||||
@@ -124,8 +120,13 @@ void MainMainWindow::setupUI()
|
|||||||
mail.setSubject(subject);
|
mail.setSubject(subject);
|
||||||
mail.setBodyHtml(body);
|
mail.setBodyHtml(body);
|
||||||
mail.setDate(QDateTime::currentDateTimeUtc());
|
mail.setDate(QDateTime::currentDateTimeUtc());
|
||||||
m_mailService->sendMail(mail, fromAddr, attachmentPaths);
|
if (scheduleTime.isValid()) {
|
||||||
statusBar()->showMessage(tr("Sending message…"), 5000);
|
m_mailService->scheduleSend(mail, fromAddr, attachmentPaths, scheduleTime);
|
||||||
|
statusBar()->showMessage(tr("Envío programado"), 5000);
|
||||||
|
} else {
|
||||||
|
m_mailService->sendMail(mail, fromAddr, attachmentPaths);
|
||||||
|
statusBar()->showMessage(tr("Sending message…"), 5000);
|
||||||
|
}
|
||||||
switchToPage(PageMail);
|
switchToPage(PageMail);
|
||||||
});
|
});
|
||||||
connect(m_composeView, &ComposeView::discardRequested, [this]() {
|
connect(m_composeView, &ComposeView::discardRequested, [this]() {
|
||||||
@@ -305,6 +306,7 @@ void MainMainWindow::setupMailPage()
|
|||||||
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::markUnreadRequested, this, &MainMainWindow::onMarkUnreadRequested);
|
||||||
|
connect(m_emailViewer, &ReaderView::markSpamRequested, this, &MainMainWindow::onMarkSpamRequested);
|
||||||
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);
|
||||||
@@ -685,6 +687,39 @@ void MainMainWindow::onMarkUnreadRequested(int mailId)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainMainWindow::onMarkSpamRequested(int mailId)
|
||||||
|
{
|
||||||
|
std::optional<MailItem> optMail = MailItemDao::findById(mailId);
|
||||||
|
if (!optMail) return;
|
||||||
|
std::optional<Folder> curFolder = FolderDao::findById(optMail->folderId());
|
||||||
|
if (!curFolder) return;
|
||||||
|
int accountId = curFolder->accountId();
|
||||||
|
|
||||||
|
// Find a Spam/Junk folder for this account.
|
||||||
|
QVector<Folder> folders = FolderDao::findByAccountId(accountId);
|
||||||
|
int spamFolderId = -1;
|
||||||
|
for (const Folder &f : folders) {
|
||||||
|
QString n = f.name().toLower();
|
||||||
|
if (n == "spam" || n == "junk" || n.contains("spam") || n.contains("junk")) {
|
||||||
|
spamFolderId = f.id();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spamFolderId < 0) {
|
||||||
|
QMessageBox::information(this, tr("Marcar como spam"),
|
||||||
|
tr("No hay una carpeta de Spam en esta cuenta. Mueve el correo manualmente con «Mover a...»."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_mailService->moveMail(QString::number(mailId), QString::number(spamFolderId));
|
||||||
|
if (m_currentMailId == mailId) m_currentMailId = -1;
|
||||||
|
m_emailModel->refresh();
|
||||||
|
m_mailListView->treeView()->clearSelection();
|
||||||
|
m_viewerStack->setCurrentIndex(0);
|
||||||
|
statusBar()->showMessage(tr("Marcado como spam y movido a la carpeta de spam"), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
void MainMainWindow::onNewMessage()
|
void MainMainWindow::onNewMessage()
|
||||||
{ // Show compose view in the viewer stack
|
{ // Show compose view in the viewer stack
|
||||||
m_embeddedComposeView->initializeComposition();
|
m_embeddedComposeView->initializeComposition();
|
||||||
@@ -784,10 +819,21 @@ void MainMainWindow::onRunRulesOnFolder()
|
|||||||
|
|
||||||
void MainMainWindow::onRunRulesOnSelected()
|
void MainMainWindow::onRunRulesOnSelected()
|
||||||
{
|
{
|
||||||
// Get selected mail IDs
|
QVector<int> ids = m_mailListView->selectedMailIds();
|
||||||
QMessageBox::information(this, tr("Ejecutar reglas"), tr("Ejecutando reglas en correos seleccionados..."));
|
if (ids.isEmpty() && m_currentMailId >= 0) {
|
||||||
// TODO: Get selected mail IDs from MailListView
|
ids.append(m_currentMailId);
|
||||||
statusBar()->showMessage(tr("Reglas ejecutadas"), 3000);
|
}
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
statusBar()->showMessage(tr("Selecciona al menos un correo"), 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<qint64> mailIds;
|
||||||
|
for (int id : ids) mailIds.append(id);
|
||||||
|
int processed = RulesEngine::runRulesOnMails(mailIds);
|
||||||
|
statusBar()->showMessage(tr("Reglas ejecutadas en %1 correos").arg(processed), 3000);
|
||||||
|
m_emailModel->refresh();
|
||||||
|
m_categoryTree->refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainMainWindow::showRulesManager()
|
void MainMainWindow::showRulesManager()
|
||||||
@@ -1197,10 +1243,6 @@ void MainMainWindow::onEmbeddedSendRequested(const QString &to, const QString &c
|
|||||||
const QString &fromAddress,
|
const QString &fromAddress,
|
||||||
const QStringList &attachmentPaths)
|
const QStringList &attachmentPaths)
|
||||||
{
|
{
|
||||||
if (scheduleTime.isValid()) {
|
|
||||||
statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fromAddress.isEmpty()) {
|
if (fromAddress.isEmpty()) {
|
||||||
statusBar()->showMessage(tr("Select an account before sending"), 5000);
|
statusBar()->showMessage(tr("Select an account before sending"), 5000);
|
||||||
return;
|
return;
|
||||||
@@ -1213,8 +1255,13 @@ void MainMainWindow::onEmbeddedSendRequested(const QString &to, const QString &c
|
|||||||
mail.setSubject(subject);
|
mail.setSubject(subject);
|
||||||
mail.setBodyHtml(body);
|
mail.setBodyHtml(body);
|
||||||
mail.setDate(QDateTime::currentDateTimeUtc());
|
mail.setDate(QDateTime::currentDateTimeUtc());
|
||||||
m_mailService->sendMail(mail, fromAddress, attachmentPaths);
|
if (scheduleTime.isValid()) {
|
||||||
statusBar()->showMessage(tr("Sending message…"), 5000);
|
m_mailService->scheduleSend(mail, fromAddress, attachmentPaths, scheduleTime);
|
||||||
|
statusBar()->showMessage(tr("Envío programado"), 5000);
|
||||||
|
} else {
|
||||||
|
m_mailService->sendMail(mail, fromAddress, attachmentPaths);
|
||||||
|
statusBar()->showMessage(tr("Sending message…"), 5000);
|
||||||
|
}
|
||||||
m_viewerStack->setCurrentIndex(0); // placeholder
|
m_viewerStack->setCurrentIndex(0); // placeholder
|
||||||
m_embeddedComposeView->initializeComposition();
|
m_embeddedComposeView->initializeComposition();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ private slots:
|
|||||||
void onBatchDeleteRequested(const QVector<int> &mailIds);
|
void onBatchDeleteRequested(const QVector<int> &mailIds);
|
||||||
void onBatchMarkUnreadRequested(const QVector<int> &mailIds);
|
void onBatchMarkUnreadRequested(const QVector<int> &mailIds);
|
||||||
void onBatchFlagRequested(const QVector<int> &mailIds, bool flagged);
|
void onBatchFlagRequested(const QVector<int> &mailIds, bool flagged);
|
||||||
|
void onMarkSpamRequested(int mailId);
|
||||||
void onMarkUnreadRequested(int mailId);
|
void onMarkUnreadRequested(int mailId);
|
||||||
// Online search
|
// Online search
|
||||||
void onOnlineSearchRequested(const QString& query);
|
void onOnlineSearchRequested(const QString& query);
|
||||||
|
|||||||
+23
-6
@@ -22,6 +22,7 @@
|
|||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
#include <QFile>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <QGraphicsDropShadowEffect>
|
#include <QGraphicsDropShadowEffect>
|
||||||
@@ -370,8 +371,7 @@ void ReaderView::setupUI() {
|
|||||||
});
|
});
|
||||||
moreMenu->addAction("Marcar como spam", [this]() {
|
moreMenu->addAction("Marcar como spam", [this]() {
|
||||||
if (m_currentMailId < 0) return;
|
if (m_currentMailId < 0) return;
|
||||||
QMessageBox::information(this, tr("Marcar como spam"),
|
emit markSpamRequested(m_currentMailId);
|
||||||
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(
|
||||||
@@ -729,8 +729,23 @@ void ReaderView::showAttachmentContextMenu(const QPoint &pos, const QString &pat
|
|||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
QAction *deleteAct = menu.addAction("Eliminar adjunto", [this, name]() {
|
QAction *deleteAct = menu.addAction("Eliminar adjunto", [this, name]() {
|
||||||
// TODO: Implement attachment deletion from stored mail
|
if (m_currentMailId < 0) return;
|
||||||
QMessageBox::information(this, "No implementado", "Eliminar adjuntos del almacenamiento local aún no está implementado.");
|
QVector<StoredAttachmentRecord> all = MailItemDao::attachmentsForMail(m_currentMailId);
|
||||||
|
QVector<StoredAttachmentRecord> remaining;
|
||||||
|
// Delete the matching stored file and keep the others.
|
||||||
|
for (const StoredAttachmentRecord &a : all) {
|
||||||
|
if (a.fileName == name) {
|
||||||
|
if (!a.storedPath.isEmpty())
|
||||||
|
QFile::remove(a.storedPath);
|
||||||
|
} else {
|
||||||
|
remaining.append(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (MailItemDao::replaceAttachments(m_currentMailId, remaining)) {
|
||||||
|
QMessageBox::information(this, "Adjunto", tr("Adjunto «%1» eliminado.").arg(name));
|
||||||
|
} else {
|
||||||
|
QMessageBox::warning(this, "Adjunto", tr("No se pudo eliminar el adjunto."));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
deleteAct->setIcon(QIcon::fromTheme("edit-delete", QApplication::style()->standardIcon(QStyle::SP_TrashIcon)));
|
deleteAct->setIcon(QIcon::fromTheme("edit-delete", QApplication::style()->standardIcon(QStyle::SP_TrashIcon)));
|
||||||
|
|
||||||
@@ -776,8 +791,10 @@ void ReaderView::setMailItem(const MailItem* item) {
|
|||||||
// From
|
// From
|
||||||
m_fromLabel->setText("De: " + formatAddress(sender));
|
m_fromLabel->setText("De: " + formatAddress(sender));
|
||||||
|
|
||||||
// To - MailItem doesn't have recipients easily accessible, skip for now
|
// To - prefer recipient()/to(), fallback to placeholder
|
||||||
m_toLabel->setText("Para: —"); // TODO: fetch from MailItem or DB
|
QString toText = item->recipient();
|
||||||
|
if (toText.isEmpty()) toText = item->to();
|
||||||
|
m_toLabel->setText("Para: " + (toText.isEmpty() ? QString("—") : formatAddress(toText)));
|
||||||
|
|
||||||
// Date
|
// Date
|
||||||
m_dateLabel->setText("Fecha: " + formatDate(item->date()));
|
m_dateLabel->setText("Fecha: " + formatDate(item->date()));
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ signals:
|
|||||||
void deleteRequested(int mailId);
|
void deleteRequested(int mailId);
|
||||||
void detachRequested();
|
void detachRequested();
|
||||||
void markUnreadRequested(int mailId);
|
void markUnreadRequested(int mailId);
|
||||||
|
void markSpamRequested(int mailId);
|
||||||
void openAttachmentRequested(const QString &path);
|
void openAttachmentRequested(const QString &path);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|||||||
@@ -68,19 +68,19 @@ void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
|
|||||||
// Bold / Italic / Underline
|
// Bold / Italic / Underline
|
||||||
QAction *boldAct = m_toolbar->addAction("B");
|
QAction *boldAct = m_toolbar->addAction("B");
|
||||||
boldAct->setCheckable(true);
|
boldAct->setCheckable(true);
|
||||||
boldAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Bold/Text Formatting/Text Bold.svg")));
|
boldAct->setIcon(QIcon(QStringLiteral(":/icons/bold.svg")));
|
||||||
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
|
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
|
||||||
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
|
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
|
||||||
|
|
||||||
QAction *italicAct = m_toolbar->addAction("I");
|
QAction *italicAct = m_toolbar->addAction("I");
|
||||||
italicAct->setCheckable(true);
|
italicAct->setCheckable(true);
|
||||||
italicAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Text Formatting/Text Italic.svg")));
|
italicAct->setIcon(QIcon(QStringLiteral(":/icons/italic.svg")));
|
||||||
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
|
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
|
||||||
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
|
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
|
||||||
|
|
||||||
QAction *underlineAct = m_toolbar->addAction("U");
|
QAction *underlineAct = m_toolbar->addAction("U");
|
||||||
underlineAct->setCheckable(true);
|
underlineAct->setCheckable(true);
|
||||||
underlineAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Text Formatting/Text Underline.svg")));
|
underlineAct->setIcon(QIcon(QStringLiteral(":/icons/underline.svg")));
|
||||||
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
|
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
|
||||||
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
|
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "rulesmanagerdialog.h"
|
#include "rulesmanagerdialog.h"
|
||||||
#include "ruleditordialog.h"
|
#include "ruleditordialog.h"
|
||||||
#include "db/dao/ruledao.h"
|
#include "db/dao/ruledao.h"
|
||||||
|
#include "db/dao/common_structs.h"
|
||||||
#include "db/dao/categorydao.h"
|
#include "db/dao/categorydao.h"
|
||||||
#include "db/dao/folderdao.h"
|
#include "db/dao/folderdao.h"
|
||||||
#include "services/rulesengine.h"
|
#include "services/rulesengine.h"
|
||||||
@@ -81,7 +82,7 @@ void RulesManagerDialog::setupUI()
|
|||||||
if (idx > 0) {
|
if (idx > 0) {
|
||||||
QTreeWidgetItem* prev = m_tree->takeTopLevelItem(idx);
|
QTreeWidgetItem* prev = m_tree->takeTopLevelItem(idx);
|
||||||
m_tree->insertTopLevelItem(idx - 1, prev);
|
m_tree->insertTopLevelItem(idx - 1, prev);
|
||||||
// TODO: update priority in DB
|
persistPriorityOrder();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
QAction* moveDownAct = menu.addAction(tr("Bajar prioridad"), [this, item]() {
|
QAction* moveDownAct = menu.addAction(tr("Bajar prioridad"), [this, item]() {
|
||||||
@@ -89,7 +90,7 @@ void RulesManagerDialog::setupUI()
|
|||||||
if (idx < m_tree->topLevelItemCount() - 1) {
|
if (idx < m_tree->topLevelItemCount() - 1) {
|
||||||
QTreeWidgetItem* next = m_tree->takeTopLevelItem(idx + 1);
|
QTreeWidgetItem* next = m_tree->takeTopLevelItem(idx + 1);
|
||||||
m_tree->insertTopLevelItem(idx + 1, item);
|
m_tree->insertTopLevelItem(idx + 1, item);
|
||||||
// TODO: update priority in DB
|
persistPriorityOrder();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
menu.exec(m_tree->viewport()->mapToGlobal(pos));
|
menu.exec(m_tree->viewport()->mapToGlobal(pos));
|
||||||
@@ -296,3 +297,20 @@ void RulesManagerDialog::refresh()
|
|||||||
{
|
{
|
||||||
loadRules();
|
loadRules();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RulesManagerDialog::persistPriorityOrder()
|
||||||
|
{
|
||||||
|
// Re-assign priority based on tree order (top = lowest number = highest priority).
|
||||||
|
for (int i = 0; i < m_tree->topLevelItemCount(); ++i) {
|
||||||
|
QTreeWidgetItem *item = m_tree->topLevelItem(i);
|
||||||
|
qint64 ruleId = item->data(0, Qt::UserRole).toLongLong();
|
||||||
|
int newPriority = i + 1;
|
||||||
|
item->setText(2, QString::number(newPriority));
|
||||||
|
auto rule = RuleDao::findById(ruleId);
|
||||||
|
if (rule.has_value()) {
|
||||||
|
Rule r = rule.value();
|
||||||
|
r.priority = newPriority;
|
||||||
|
RuleDao::update(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ private:
|
|||||||
void loadRules();
|
void loadRules();
|
||||||
void addRuleToTree(const Rule& rule);
|
void addRuleToTree(const Rule& rule);
|
||||||
Rule itemToRule(QTreeWidgetItem* item) const;
|
Rule itemToRule(QTreeWidgetItem* item) const;
|
||||||
|
void persistPriorityOrder();
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user