feat: comprehensive Wino Mail updates

- ReaderView: complete rewrite with CSS styling, headers, attachments, zoom, find, dark mode, image blocking
- Categories: DAO + UI tree widget with colors, nested categories, assignment
- Rules engine: DAO + evaluation + actions (move, mark read, flag, delete, category, forward)
- Templates: DAO + editor + manager with variables support
- Signatures: DAO + manager + auto-per-account + manual selector in compose
- ComposeView: signature combo, template selector, auto-signature on new email
- MainWindow: frameless with rounded corners, 1px border, resize from edges, no status bar
- Database: new tables (Category, MailCategory, Rule, Template, Signature) with indexes
This commit is contained in:
2026-08-23 14:49:58 +02:00
parent 5fcedfa129
commit 0bb8738607
7399 changed files with 44711 additions and 209 deletions
-1
View File
@@ -688,4 +688,3 @@ void AccountSetupDialog::showError(const QString &message)
m_btnNext->setVisible(false);
}
#include "accountsetupdialog.moc"
+257
View File
@@ -0,0 +1,257 @@
#include "categorytreewidget.h"
#include "db/dao/categorydao.h"
#include "db/dao/mailitemdao.h"
#include <QInputDialog>
#include <QColorDialog>
#include <QMessageBox>
#include <QDebug>
#include <QApplication>
CategoryTreeWidget::CategoryTreeWidget(QWidget *parent) : QTreeWidget(parent)
{
setHeaderLabel(tr("Categorías"));
setContextMenuPolicy(Qt::CustomContextMenu);
setExpandsOnDoubleClick(false);
setAnimated(true);
setIndentation(16);
connect(this, &QTreeWidget::customContextMenuRequested, this, &CategoryTreeWidget::onCategoryContextMenu);
connect(this, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* item, int) {
Category cat = itemToCategory(item);
if (cat.isValid()) emit categoryDoubleClicked(cat);
});
connect(this, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int) {
Category cat = itemToCategory(item);
if (cat.isValid()) emit categorySelected(cat);
});
setupContextMenu();
}
void CategoryTreeWidget::setupContextMenu()
{
m_contextMenu = new QMenu(this);
m_contextMenu->addAction(tr("Nueva categoría"), this, &CategoryTreeWidget::onNewCategory);
m_contextMenu->addAction(tr("Nueva subcategoría"), this, [this]() {
QTreeWidgetItem* item = currentItem();
if (item) onEditCategory(item); // Reuse for subcategory creation
});
m_contextMenu->addSeparator();
m_contextMenu->addAction(tr("Editar"), this, [this]() {
QTreeWidgetItem* item = currentItem();
if (item) onEditCategory(item);
});
m_contextMenu->addAction(tr("Eliminar"), this, [this]() {
QTreeWidgetItem* item = currentItem();
if (item) onDeleteCategory(item);
});
m_contextMenu->addSeparator();
m_contextMenu->addAction(tr("Asignar a correos seleccionados"), this, [this]() {
QTreeWidgetItem* item = currentItem();
if (item) onAssignToSelectedMails(item);
});
}
void CategoryTreeWidget::setAccountId(qint64 accountId)
{
m_accountId = accountId;
refresh();
}
void CategoryTreeWidget::refresh()
{
clear();
m_categoryItems.clear();
loadCategories();
expandAll();
}
void CategoryTreeWidget::loadCategories()
{
QVector<Category> categories = CategoryDao::findByAccount(m_accountId);
// Build tree: first root categories, then children
QMap<qint64, QVector<Category>> childrenMap;
QVector<Category> roots;
for (const Category& cat : categories) {
if (cat.parentCategoryId >= 0) {
childrenMap[cat.parentCategoryId].append(cat);
} else {
roots.append(cat);
}
}
// Sort by sortOrder
auto sortByOrder = [](const Category& a, const Category& b) {
return a.sortOrder < b.sortOrder;
};
std::sort(roots.begin(), roots.end(), sortByOrder);
for (auto it = childrenMap.begin(); it != childrenMap.end(); ++it) {
std::sort(it.value().begin(), it.value().end(), sortByOrder);
}
for (const Category& root : roots) {
addCategory(root);
}
}
void CategoryTreeWidget::addCategory(const Category& cat, QTreeWidgetItem* parent)
{
QTreeWidgetItem* item = parent ? new QTreeWidgetItem(parent) : new QTreeWidgetItem(this);
item->setText(0, cat.name);
item->setData(0, Qt::UserRole, cat.id);
item->setData(0, Qt::UserRole + 1, cat.accountId);
item->setData(0, Qt::UserRole + 2, cat.parentCategoryId);
item->setData(0, Qt::UserRole + 3, cat.sortOrder);
// Color indicator
QPixmap pix(16, 16);
pix.fill(cat.color);
item->setIcon(0, QIcon(pix));
// Tooltip with details
QString tooltip = QString("%1\nColor: %2\nAccount: %3")
.arg(cat.name)
.arg(cat.color.name())
.arg(cat.isGlobal() ? "Global" : QString::number(cat.accountId));
item->setToolTip(0, tooltip);
m_categoryItems[cat.id] = item;
// Add children
QVector<Category> children = CategoryDao::findChildren(cat.id);
for (const Category& child : children) {
addCategory(child, item);
}
}
QTreeWidgetItem* CategoryTreeWidget::findItemByCategoryId(qint64 categoryId)
{
return m_categoryItems.value(categoryId, nullptr);
}
void CategoryTreeWidget::onMailSelected(qint64 mailId)
{
// Update checkboxes/selection state for categories this mail belongs to
QVector<qint64> catIds = CategoryDao::categoriesForMail(mailId);
for (auto it = m_categoryItems.begin(); it != m_categoryItems.end(); ++it) {
QTreeWidgetItem* item = it.value();
bool assigned = catIds.contains(it.key());
item->setCheckState(0, assigned ? Qt::Checked : Qt::Unchecked);
}
}
void CategoryTreeWidget::onCategoryContextMenu(const QPoint& pos)
{
QTreeWidgetItem* item = itemAt(pos);
if (item) setCurrentItem(item);
// Update action states
bool hasItem = item != nullptr;
QList<QAction*> actions = m_contextMenu->actions();
for (QAction* act : actions) {
QString text = act->text();
if (text.contains("Editar") || text.contains("Eliminar") || text.contains("Asignar")) {
act->setEnabled(hasItem);
}
}
m_contextMenu->exec(viewport()->mapToGlobal(pos));
}
void CategoryTreeWidget::onNewCategory()
{
QTreeWidgetItem* parentItem = currentItem();
qint64 parentId = parentItem ? parentItem->data(0, Qt::UserRole).toLongLong() : -1;
bool ok;
QString name = QInputDialog::getText(this, tr("Nueva categoría"), tr("Nombre:"), QLineEdit::Normal, QString(), &ok);
if (!ok || name.trimmed().isEmpty()) return;
QColor color = QColorDialog::getColor(QColor("#1976D2"), this, tr("Color de la categoría"));
if (!color.isValid()) color = QColor("#1976D2");
Category cat;
cat.name = name.trimmed();
cat.color = color;
cat.accountId = m_accountId;
cat.parentCategoryId = parentId;
cat.sortOrder = 0;
if (CategoryDao::insert(cat)) {
refresh();
emit categoryCreated(cat);
} else {
QMessageBox::warning(this, tr("Error"), tr("No se pudo crear la categoría."));
}
}
void CategoryTreeWidget::onEditCategory(QTreeWidgetItem* item)
{
if (!item) return;
qint64 catId = item->data(0, Qt::UserRole).toLongLong();
auto catOpt = CategoryDao::findById(catId);
if (!catOpt.has_value()) return;
Category cat = *catOpt;
bool ok;
QString name = QInputDialog::getText(this, tr("Editar categoría"), tr("Nombre:"), QLineEdit::Normal, cat.name, &ok);
if (!ok || name.trimmed().isEmpty()) return;
QColor color = QColorDialog::getColor(cat.color, this, tr("Color de la categoría"));
if (!color.isValid()) color = cat.color;
cat.name = name.trimmed();
cat.color = color;
if (CategoryDao::update(cat)) {
refresh();
emit categoryEdited(cat);
} else {
QMessageBox::warning(this, tr("Error"), tr("No se pudo actualizar la categoría."));
}
}
void CategoryTreeWidget::onDeleteCategory(QTreeWidgetItem* item)
{
if (!item) return;
qint64 catId = item->data(0, Qt::UserRole).toLongLong();
if (QMessageBox::question(this, tr("Eliminar categoría"),
tr("¿Eliminar esta categoría y quitarla de todos los correos?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
if (CategoryDao::remove(catId)) {
refresh();
emit categoryDeleted(catId);
} else {
QMessageBox::warning(this, tr("Error"), tr("No se pudo eliminar la categoría."));
}
}
void CategoryTreeWidget::onAssignToSelectedMails(QTreeWidgetItem* item)
{
if (!item) return;
qint64 catId = item->data(0, Qt::UserRole).toLongLong();
// This would need integration with the mail list view to get selected mail IDs
// For now, emit signal for the main window to handle
emit assignCategoryRequested(-1, catId); // -1 = use current selection
}
Category CategoryTreeWidget::itemToCategory(QTreeWidgetItem* item) const
{
if (!item) return Category();
qint64 id = item->data(0, Qt::UserRole).toLongLong();
auto catOpt = CategoryDao::findById(id);
return catOpt.value_or(Category());
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <QWidget>
#include <QTreeWidget>
#include <QMenu>
#include <QAction>
#include "db/dao/categorydao.h"
#include "db/dao/ruledao.h"
class CategoryTreeWidget : public QTreeWidget
{
Q_OBJECT
public:
explicit CategoryTreeWidget(QWidget *parent = nullptr);
void setAccountId(qint64 accountId);
void refresh();
void addCategory(const Category& cat, QTreeWidgetItem* parent = nullptr);
QTreeWidgetItem* findItemByCategoryId(qint64 categoryId);
signals:
void categorySelected(const Category& cat);
void categoryDoubleClicked(const Category& cat);
void assignCategoryRequested(qint64 mailId, qint64 categoryId);
void categoryCreated(const Category& cat);
void categoryEdited(const Category& cat);
void categoryDeleted(qint64 categoryId);
void mailCategorized(qint64 mailId, const QVector<qint64>& categoryIds);
public slots:
void onMailSelected(qint64 mailId);
void onCategoryContextMenu(const QPoint& pos);
private:
qint64 m_accountId = -1;
QMap<qint64, QTreeWidgetItem*> m_categoryItems;
QMenu* m_contextMenu = nullptr;
void setupContextMenu();
void onNewCategory();
void onEditCategory(QTreeWidgetItem* item);
void onDeleteCategory(QTreeWidgetItem* item);
void onAssignToSelectedMails(QTreeWidgetItem* item);
void loadCategories();
Category itemToCategory(QTreeWidgetItem* item) const;
};
+182 -40
View File
@@ -1,6 +1,9 @@
// ===================== ComposeView =====================
#include "composeview.h"
#include "db/dao/templatedao.h"
#include "ui/templatesmanagerdialog.h"
#include "ui/signaturemanagerdialog.h"
#include <QDialog>
#include <QInputDialog>
#include <QVBoxLayout>
@@ -16,6 +19,9 @@
#include <QStyle>
#include <QApplication>
#include <QFileIconProvider>
#include <QMainWindow>
#include <QSqlDatabase>
#include <QSqlQuery>
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
@@ -27,36 +33,31 @@ ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
void ComposeView::onSignatureClicked()
{
// Open signature manager dialog
qint64 accountId = -1;
if (m_accountService && m_accountCombo && m_accountCombo->currentIndex() > 0) {
accountId = m_accountCombo->itemData(m_accountCombo->currentIndex()).toLongLong();
}
SignatureManagerDialog dlg(m_accountService, accountId, this);
connect(&dlg, &SignatureManagerDialog::signatureSelected, this, [this](const QString& html) {
if (!html.isEmpty()) {
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(html);
}
});
connect(&dlg, &SignatureManagerDialog::signaturesChanged, this, [this]() {
emit statusMessageRequested(tr("Firmas actualizadas"));
});
dlg.exec();
}
void ComposeView::onSignatureEditRequested() {
QDialog dialog(this);
dialog.setWindowTitle(tr("Edit Signature"));
dialog.setMinimumSize(600, 400);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
RichTextEditor *editor = new RichTextEditor(&dialog);
editor->setupToolbar(layout);
layout->addWidget(editor);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (dialog.exec() == QDialog::Accepted) {
QString signatureHtml = editor->toHtml();
if (!signatureHtml.isEmpty()) {
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(signatureHtml);
}
}
void ComposeView::onSignatureEditRequested()
{
// Same as click - open manager
onSignatureClicked();
}
void ComposeView::setAccountService(AccountService *service) {
@@ -89,31 +90,123 @@ void ComposeView::onAccountChanged(int index) {
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
m_signatureCombo->clear();
m_signatureCombo->addItem(tr("Sin cuenta seleccionada"), QVariant());
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
populateSignatureCombo(m_currentAccountId);
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount() {
void ComposeView::populateSignatureCombo(qint64 accountId)
{
m_signatureCombo->clear();
m_signatureCombo->addItem(tr("Firma automática"), QVariant()); // Index 0 = auto
if (!m_accountService || accountId <= 0) {
m_signatureCombo->addItem(tr("Sin firmas disponibles"), QVariant());
return;
}
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
// Get signatures for this account + global
query.prepare("SELECT id, name FROM Signature WHERE accountId = :accountId OR accountId IS NULL ORDER BY isDefault DESC, name");
query.bindValue(":accountId", accountId);
if (query.exec()) {
while (query.next()) {
QString name = query.value("name").toString();
qint64 id = query.value("id").toLongLong();
m_signatureCombo->addItem(name, id);
}
}
// Set to first item (automatic)
m_signatureCombo->setCurrentIndex(0);
}
void ComposeView::onSignatureComboChanged(int index)
{
if (index <= 0) {
// Automatic mode - use default signature
loadSignatureForCurrentAccount();
return;
}
qint64 signatureId = m_signatureCombo->itemData(index).toLongLong();
if (signatureId <= 0) return;
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT html FROM Signature WHERE id = :id");
query.bindValue(":id", signatureId);
if (query.exec() && query.next()) {
QString html = query.value(0).toString();
if (!html.isEmpty()) {
m_bodyEditor->setHtml(html);
}
}
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
delete account;
QString signatureHtml = getDefaultSignatureForAccount(m_currentAccountId);
if (!signatureHtml.isEmpty()) {
m_bodyEditor->setHtml(signatureHtml);
} else {
m_bodyEditor->clear();
}
}
QString ComposeView::getDefaultSignatureForAccount(qint64 accountId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
// First try to get account-specific default signature
query.prepare("SELECT html FROM Signature WHERE accountId = :accountId AND name LIKE '%default%' ORDER BY updatedAt DESC LIMIT 1");
query.bindValue(":accountId", accountId);
if (query.exec() && query.next()) {
return query.value(0).toString();
}
// Fallback to global default signature
query.prepare("SELECT html FROM Signature WHERE accountId IS NULL AND name LIKE '%default%' ORDER BY updatedAt DESC LIMIT 1");
if (query.exec() && query.next()) {
return query.value(0).toString();
}
// Last resort: any signature for this account
query.prepare("SELECT html FROM Signature WHERE accountId = :accountId ORDER BY updatedAt DESC LIMIT 1");
query.bindValue(":accountId", accountId);
if (query.exec() && query.next()) {
return query.value(0).toString();
}
// Global any signature
query.prepare("SELECT html FROM Signature WHERE accountId IS NULL ORDER BY updatedAt DESC LIMIT 1");
if (query.exec() && query.next()) {
return query.value(0).toString();
}
return QString();
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
@@ -143,6 +236,16 @@ void ComposeView::setupUI() {
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo);
// Signature selector
m_signatureCombo = new QComboBox();
m_signatureCombo->setFixedWidth(180);
m_signatureCombo->setStyleSheet(m_accountCombo->styleSheet());
m_signatureCombo->setToolTip(tr("Seleccionar firma"));
connect(m_signatureCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onSignatureComboChanged);
fromLayout->addWidget(m_signatureCombo);
fromLayout->addStretch(); // Este resorte empuja el par (label+combo) a la izquierda
mainLayout->addLayout(fromLayout);
@@ -426,13 +529,16 @@ void ComposeView::initializeComposition() {
m_ccField->tags(QStringList{});
m_bccField->tags(QStringList{});
m_subjectField->clear();
m_bodyEditor->clear();
m_attachmentFiles.clear();
m_attachmentList->clear();
m_attachmentList->setVisible(false);
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
// Load signature for current account
loadSignatureForCurrentAccount();
m_toField->setFocus();
}
@@ -528,9 +634,45 @@ void ComposeView::startNewEmail(const QString &initialRecipient) {
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
void ComposeView::onTemplateClicked() {
// Placeholder for template functionality
QMessageBox::information(this, tr("Email Templates"), tr("Email templates feature not yet implemented."));
void ComposeView::onTemplateClicked()
{
// Get current account ID if available
qint64 accountId = -1;
if (m_accountService) {
// Try to get current account from combo
if (m_accountCombo && m_accountCombo->currentIndex() > 0) {
accountId = m_accountCombo->itemData(m_accountCombo->currentIndex()).toLongLong();
}
}
TemplatesManagerDialog dlg(accountId, this);
connect(&dlg, &TemplatesManagerDialog::templateSelected, this, [this](const Template& tmpl) {
applyTemplate(tmpl);
});
dlg.exec();
}
void ComposeView::applyTemplate(const Template& tmpl)
{
if (!tmpl.isValid()) return;
// Set subject
if (!tmpl.subject.isEmpty()) {
m_subjectField->setText(tmpl.subject);
}
// Set body HTML
if (!tmpl.bodyHtml.isEmpty()) {
m_bodyEditor->setHtml(tmpl.bodyHtml);
}
// If there are variables, we could show a dialog to fill them
// For now, just apply as-is
emit statusMessageRequested(tr("Plantilla aplicada: %1").arg(tmpl.name));
}
void ComposeView::statusBarMessage(const QString& msg)
{
emit statusMessageRequested(msg);
}
#include "composeview.moc"
+10 -1
View File
@@ -19,6 +19,7 @@
#include "models/EmailCompositionModel.h"
#include "../../thirdparty/tags/include/tags_line_edit.hpp"
#include "services/accountservice.h"
#include "db/dao/templatedao.h"
class AccountService;
@@ -41,6 +42,7 @@ signals:
const QString &fromAddress,
const QStringList &attachmentPaths);
void discardRequested();
void statusMessageRequested(const QString& message);
public slots:
void initializeComposition();
@@ -48,6 +50,8 @@ public slots:
void setTo(const QString &to);
void setSubject(const QString &subject);
void setBody(const QString &body);
void applyTemplate(const Template& tmpl);
QString getBody() const { return m_bodyEditor->toHtml(); }
private slots:
void onCcToggle();
@@ -56,22 +60,27 @@ private slots:
void onScheduleClicked();
void onDetachClicked();
void onAccountChanged(int index);
void onSignatureComboChanged(int index);
void onSignatureClicked();
void onSignatureEditRequested();
void onAddAttachmentClicked();
void onRemoveAttachmentClicked();
void onTemplateClicked(); // new slot for templates
void onTemplateClicked();
private:
void setupUI();
void populateAccountCombo();
void populateSignatureCombo(qint64 accountId);
void loadSignatureForCurrentAccount();
QString getDefaultSignatureForAccount(qint64 accountId);
void statusBarMessage(const QString& msg);
EmailCompositionModel *m_compositionModel;
AccountService *m_accountService = nullptr;
// UI components
QLabel *m_fromLabel = nullptr;
QComboBox *m_accountCombo = nullptr;
QComboBox *m_signatureCombo = nullptr;
everload_tags::TagsLineEdit *m_toField = nullptr;
everload_tags::TagsLineEdit *m_ccField = nullptr;
everload_tags::TagsLineEdit *m_bccField = nullptr;
-1
View File
@@ -221,4 +221,3 @@ void ConnectionWizard::accept()
QWizard::accept();
}
#include "connectionwizard.moc"
+65
View File
@@ -33,6 +33,55 @@ void MailListView::setupUI() {
layout->addWidget(headerBar);
// Search bar
QWidget *searchBar = new QWidget();
searchBar->setFixedHeight(44);
searchBar->setStyleSheet("background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6;");
QHBoxLayout *searchLayout = new QHBoxLayout(searchBar);
searchLayout->setContentsMargins(12, 4, 12, 4);
searchLayout->setSpacing(8);
m_searchEdit = new QLineEdit();
m_searchEdit->setPlaceholderText("Buscar correos…");
m_searchEdit->setClearButtonEnabled(true);
m_searchEdit->setStyleSheet(
"QLineEdit { background: white; border: 1px solid #d1d1d6; border-radius: 6px; "
"padding: 6px 12px; font-size: 13px; color: #1d1d1f; }"
"QLineEdit:focus { border: 2px solid #0071e3; }"
);
searchLayout->addWidget(m_searchEdit);
connect(m_searchEdit, &QLineEdit::textChanged, this, &MailListView::onSearchTextChanged);
layout->addWidget(searchBar);
// Filter bar
QWidget *filterBar = new QWidget();
filterBar->setFixedHeight(36);
filterBar->setStyleSheet("background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6;");
QHBoxLayout *filterLayout = new QHBoxLayout(filterBar);
filterLayout->setContentsMargins(12, 2, 12, 2);
filterLayout->setSpacing(12);
m_unreadOnlyCheck = new QCheckBox("No leídos");
m_unreadOnlyCheck->setStyleSheet("QCheckBox { font-size: 12px; color: #333; }");
filterLayout->addWidget(m_unreadOnlyCheck);
m_flaggedOnlyCheck = new QCheckBox("Marcados");
m_flaggedOnlyCheck->setStyleSheet("QCheckBox { font-size: 12px; color: #333; }");
filterLayout->addWidget(m_flaggedOnlyCheck);
m_hasAttachmentsCheck = new QCheckBox("Con adjuntos");
m_hasAttachmentsCheck->setStyleSheet("QCheckBox { font-size: 12px; color: #333; }");
filterLayout->addWidget(m_hasAttachmentsCheck);
filterLayout->addStretch();
connect(m_unreadOnlyCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
connect(m_flaggedOnlyCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
connect(m_hasAttachmentsCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
layout->addWidget(filterBar);
// Table
m_tableView = new QTableView();
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
@@ -55,6 +104,8 @@ void MailListView::setupUI() {
m_proxyModel->setSortRole(EmailListModel::DateRole);
m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
m_proxyModel->setDynamicSortFilter(true);
m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_proxyModel->setFilterKeyColumn(-1); // Search all columns
m_tableView->setModel(m_proxyModel);
@@ -90,4 +141,18 @@ void MailListView::onRowSelected(const QModelIndex &index) {
QModelIndex sourceIndex = m_proxyModel->mapToSource(index);
int mailId = sourceIndex.data(EmailListModel::IdRole).toInt();
emit emailSelected(mailId);
}
void MailListView::onSearchTextChanged(const QString &text) {
if (auto *model = qobject_cast<EmailListModel*>(m_proxyModel->sourceModel())) {
model->setSearchFilter(text);
}
}
void MailListView::onFilterChanged() {
if (auto *model = qobject_cast<EmailListModel*>(m_proxyModel->sourceModel())) {
model->setShowUnreadOnly(m_unreadOnlyCheck->isChecked());
model->setShowFlaggedOnly(m_flaggedOnlyCheck->isChecked());
model->setShowHasAttachments(m_hasAttachmentsCheck->isChecked());
}
}
+9
View File
@@ -7,6 +7,9 @@
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QLineEdit>
#include <QCheckBox>
#include <QHBoxLayout>
#include "ui/models/EmailListModel.h"
@@ -27,6 +30,8 @@ signals:
private slots:
void onRowSelected(const QModelIndex &index);
void onSearchTextChanged(const QString &text);
void onFilterChanged();
private:
void setupUI();
@@ -34,4 +39,8 @@ private:
QTableView *m_tableView;
QSortFilterProxyModel *m_proxyModel;
QPushButton *m_composeButton;
QLineEdit *m_searchEdit;
QCheckBox *m_unreadOnlyCheck;
QCheckBox *m_flaggedOnlyCheck;
QCheckBox *m_hasAttachmentsCheck;
};
+485 -36
View File
@@ -3,14 +3,22 @@
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include "db/dao/categorydao.h"
#include "db/dao/ruledao.h"
#include "services/rulesengine.h"
#include "ui/rulesmanagerdialog.h"
#include "ui/accountsetupdialog.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QFileDialog>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
#ifdef Q_OS_WIN
#include <windows.h>
#endif
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
@@ -53,8 +61,8 @@ void MainMainWindow::setupUI()
);
qDebug() << "[MainMainWindow::setupUI] Stylesheet set";
createToolBar();
qDebug() << "[MainMainWindow::setupUI] Toolbar created";
createTitleBar();
qDebug() << "[MainMainWindow::setupUI] Title bar created";
// === Central widget ===
QWidget *central = new QWidget();
@@ -194,9 +202,15 @@ void MainMainWindow::setupMailPage()
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
// Main splitter with 3 panes: folders | categories | mail list | viewer
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Left pane: Folder tree + Category tree (vertical splitter)
QSplitter* leftSplitter = new QSplitter(Qt::Vertical);
leftSplitter->setHandleWidth(1);
leftSplitter->setChildrenCollapsible(false);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
@@ -205,7 +219,26 @@ void MainMainWindow::setupMailPage()
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
leftSplitter->addWidget(m_folderTree);
// Category tree
m_categoryTree = new CategoryTreeWidget();
m_categoryTree->setMinimumHeight(200);
m_categoryTree->setMaximumHeight(400);
connect(m_categoryTree, &CategoryTreeWidget::categorySelected, this, &MainMainWindow::onCategorySelected);
connect(m_categoryTree, &CategoryTreeWidget::categoryDoubleClicked, this, &MainMainWindow::onCategoryDoubleClicked);
connect(m_categoryTree, &CategoryTreeWidget::assignCategoryRequested, this, &MainMainWindow::onAssignCategoryRequested);
connect(m_categoryTree, &CategoryTreeWidget::categoryCreated, this, &MainMainWindow::onCategoryCreated);
connect(m_categoryTree, &CategoryTreeWidget::categoryEdited, this, &MainMainWindow::onCategoryEdited);
connect(m_categoryTree, &CategoryTreeWidget::categoryDeleted, this, &MainMainWindow::onCategoryDeleted);
leftSplitter->addWidget(m_categoryTree);
// Set initial sizes for left splitter: folder tree gets 60%, categories 40%
leftSplitter->setSizes({400, 250});
leftSplitter->setStretchFactor(0, 3);
leftSplitter->setStretchFactor(1, 2);
m_folderSplitter->addWidget(leftSplitter);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
@@ -243,6 +276,8 @@ void MainMainWindow::setupMailPage()
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
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::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
@@ -254,6 +289,9 @@ void MainMainWindow::setupMailPage()
connect(m_embeddedComposeView, &ComposeView::sendRequested, this, &MainMainWindow::onEmbeddedSendRequested);
connect(m_embeddedComposeView, &ComposeView::discardRequested, this, &MainMainWindow::onEmbeddedDiscardRequested);
connect(m_embeddedComposeView, &ComposeView::detachRequested, this, &MainMainWindow::onEmbeddedDetachRequested);
connect(m_embeddedComposeView, &ComposeView::statusMessageRequested, this, [this](const QString& msg) {
statusBar()->showMessage(msg, 3000);
});
// Add to stack
m_viewerStack->addWidget(m_placeholderWidget);
@@ -263,8 +301,8 @@ void MainMainWindow::setupMailPage()
m_folderSplitter->addWidget(m_viewerStack);
// Default sizes: folder 240, list 380, viewer flex
m_folderSplitter->setSizes({240, 380, 600});
// Default sizes: left pane (folders+categories) 300, list 380, viewer flex
m_folderSplitter->setSizes({300, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
@@ -371,17 +409,55 @@ void MainMainWindow::onComposeRequested()
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{ if (item) {
void MainMainWindow::onReaderReplyRequested(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (item.has_value()) {
m_embeddedComposeView->initializeComposition();
m_embeddedComposeView->setTo(item->sender());
m_embeddedComposeView->setSubject(QString("Re: %1").arg(item->subject()));
// Optionally set body with quoted text? We'll leave empty for now.
m_embeddedComposeView->setBody(QString()); // clear
// Insert quoted original message BELOW the signature
QString quoted = QString("<blockquote cite=\"mailto:%1\">%2</blockquote>").arg(item->sender(), item->bodyHtml());
// Get current body (with signature) and append quoted text
QString currentBody = m_embeddedComposeView->getBody();
if (!currentBody.isEmpty()) {
m_embeddedComposeView->setBody(currentBody + "<br><br>" + quoted);
} else {
m_embeddedComposeView->setBody(quoted);
}
}
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::onReaderForwardRequested(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (item.has_value()) {
m_embeddedComposeView->initializeComposition();
m_embeddedComposeView->setSubject(QString("Fwd: %1").arg(item->subject()));
// Insert forwarded message BELOW the signature
QString quoted = QString("<blockquote cite=\"mailto:%1\">%2</blockquote>").arg(item->sender(), item->bodyHtml());
QString currentBody = m_embeddedComposeView->getBody();
if (!currentBody.isEmpty()) {
m_embeddedComposeView->setBody(currentBody + "<br><br>" + quoted);
} else {
m_embeddedComposeView->setBody(quoted);
}
}
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::onReaderDeleteRequested(int mailId)
{
MailItemDao::remove(mailId);
if (m_currentMailId == mailId) {
m_currentMailId = -1;
m_viewerStack->setCurrentIndex(0); // placeholder
}
m_emailModel->refresh();
statusBar()->showMessage(tr("Mensaje eliminado"), 3000);
}
void MainMainWindow::onNewMessage()
{ // Show compose view in the viewer stack
m_embeddedComposeView->initializeComposition();
@@ -407,42 +483,415 @@ void MainMainWindow::openMailInIndependentWindow(int mailId)
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::createToolBar()
// Category slots
void MainMainWindow::updateCategoryTreeForAccount(qint64 accountId)
{
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
if (m_categoryTree) {
m_categoryTree->setAccountId(accountId);
}
}
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
void MainMainWindow::onCategorySelected(const Category& cat)
{
// Filter mail list by category - could be implemented
statusBar()->showMessage(tr("Categoría seleccionada: %1").arg(cat.name), 2000);
}
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
const auto folder = FolderDao::findById(m_currentFolderId);
if (folder) {
m_mailService->fetchMails(QString::number(folder->accountId()), QString::number(m_currentFolderId));
statusBar()->showMessage(tr("Synchronizing…"), 3000);
void MainMainWindow::onCategoryDoubleClicked(const Category& cat)
{
// Edit category
onCategoryEdited(cat);
}
void MainMainWindow::onAssignCategoryRequested(qint64 mailId, qint64 categoryId)
{
assignCategoryToSelectedMails(categoryId);
}
void MainMainWindow::assignCategoryToSelectedMails(qint64 categoryId)
{
// Get selected mail IDs from mail list view
// This requires MailListView to expose selected mail IDs
// For now, use current mail if no multi-selection
if (m_currentMailId >= 0) {
if (CategoryDao::assignToMail(m_currentMailId, categoryId)) {
statusBar()->showMessage(tr("Categoría asignada"), 2000);
// Refresh category tree checkboxes
if (m_categoryTree) {
m_categoryTree->onMailSelected(m_currentMailId);
}
}
}
}
void MainMainWindow::onCategoryCreated(const Category& cat)
{
statusBar()->showMessage(tr("Categoría creada: %1").arg(cat.name), 3000);
}
void MainMainWindow::onCategoryEdited(const Category& cat)
{
statusBar()->showMessage(tr("Categoría actualizada: %1").arg(cat.name), 3000);
}
void MainMainWindow::onCategoryDeleted(qint64 categoryId)
{
statusBar()->showMessage(tr("Categoría eliminada"), 3000);
}
// Rules slots
void MainMainWindow::onRulesChanged()
{
statusBar()->showMessage(tr("Reglas actualizadas"), 2000);
}
void MainMainWindow::onRunRulesOnFolder()
{
if (m_currentFolderId >= 0) {
QMessageBox::information(this, tr("Ejecutar reglas"), tr("Ejecutando reglas en carpeta actual..."));
int processed = RulesEngine::runRulesOnFolder(m_currentFolderId, -1);
statusBar()->showMessage(tr("Reglas ejecutadas en %1 correos").arg(processed), 5000);
m_emailModel->refresh();
}
}
void MainMainWindow::onRunRulesOnSelected()
{
// Get selected mail IDs
QMessageBox::information(this, tr("Ejecutar reglas"), tr("Ejecutando reglas en correos seleccionados..."));
// TODO: Get selected mail IDs from MailListView
statusBar()->showMessage(tr("Reglas ejecutadas"), 3000);
}
void MainMainWindow::showRulesManager()
{
RulesManagerDialog dlg(-1, this); // Global rules
connect(&dlg, &RulesManagerDialog::rulesChanged, this, &MainMainWindow::onRulesChanged);
dlg.exec();
}
void MainMainWindow::showTemplatesManager()
{
qint64 accountId = -1;
// Could get current account from folder selection
TemplatesManagerDialog dlg(accountId, this);
connect(&dlg, &TemplatesManagerDialog::templateSelected, [this](const Template& tmpl) {
// If compose view is active, apply template
if (m_viewerStack->currentWidget() == m_embeddedComposeView) {
m_embeddedComposeView->applyTemplate(tmpl);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
connect(&dlg, &TemplatesManagerDialog::templatesChanged, this, [this]() {
statusBar()->showMessage(tr("Plantillas actualizadas"), 2000);
});
dlg.exec();
}
void MainMainWindow::createTitleBar()
{
// Enable frameless window
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint);
// Set Resizeable
setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint);
#ifdef Q_OS_WIN
HWND hwnd = (HWND)this->winId();
DWORD style = ::GetWindowLong(hwnd, GWL_STYLE);
::SetWindowLong(hwnd, GWL_STYLE, style | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CAPTION);
#endif
// Remove status bar (item 4)
//setStatusBar(nullptr);
// Apply rounded corners and border to main window (items 1, 2)
setAttribute(Qt::WA_TranslucentBackground);
setStyleSheet(
"MainMainWindow {"
" background-color: #f5f5f7;"
" border: 1px solid #d1d1d6;"
" border-radius: 8px;"
"}"
"QWidget#TitleBar {"
" background-color: #ffffff;"
" border-bottom: 1px solid #d1d1d6;"
" border-top-left-radius: 8px;"
" border-top-right-radius: 8px;"
"}"
"QToolButton {"
" border: none;"
" background: transparent;"
" border-radius: 4px;"
" padding: 4px;"
"}"
"QToolButton:hover {"
" background-color: #e8e8ed;"
"}"
"QToolButton:pressed {"
" background-color: #d1d1d6;"
"}"
"QLabel {"
" color: #1d1d1f;"
" font-weight: 600;"
" font-size: 13px;"
"}"
);
// Create title bar widget
m_titleBar = new QWidget(this);
m_titleBar->setObjectName("TitleBar");
m_titleBar->setFixedHeight(36);
QHBoxLayout *layout = new QHBoxLayout(m_titleBar);
layout->setContentsMargins(8, 0, 8, 0);
layout->setSpacing(4);
// App icon
QLabel *iconLabel = new QLabel();
iconLabel->setPixmap(QIcon(":/icons/app.svg").pixmap(18, 18));
layout->addWidget(iconLabel);
// Window title
QLabel *titleLabel = new QLabel("Wino Mail");
layout->addWidget(titleLabel);
layout->addStretch();
// Custom action buttons (sync, settings, etc.)
auto addActionButton = [&](const QString &iconName, const QString &tooltip, std::function<void()> slot) {
QToolButton *btn = new QToolButton();
btn->setIcon(QIcon(QString(":/icons/%1.svg").arg(iconName)));
btn->setToolTip(tooltip);
btn->setFixedSize(28, 28);
btn->setIconSize(QSize(16, 16));
connect(btn, &QToolButton::clicked, this, slot);
layout->addWidget(btn);
return btn;
};
addActionButton("sync", "Sincronizar", [this]() { onSyncRequested(); });
addActionButton("settings", "Ajustes", [this]() { onSettingsRequested(); });
addActionButton("filter", "Reglas", [this]() { showRulesManager(); });
addActionButton("play", "Ejecutar reglas en carpeta", [this]() { onRunRulesOnFolder(); });
addActionButton("file-text", "Plantillas", [this]() { showTemplatesManager(); });
layout->addSpacing(8);
// Window control buttons (minimize, maximize, close)
QToolButton *minBtn = new QToolButton();
minBtn->setIcon(QIcon(":/icons/minimize.svg"));
minBtn->setToolTip("Minimizar");
minBtn->setFixedSize(28, 28);
minBtn->setIconSize(QSize(16, 16));
connect(minBtn, &QToolButton::clicked, this, &QWidget::showMinimized);
layout->addWidget(minBtn);
m_maximizeButton = new QToolButton();
m_maximizeButton->setIcon(QIcon(isMaximized() ? ":/icons/restore.svg" : ":/icons/maximize.svg"));
m_maximizeButton->setToolTip(isMaximized() ? "Restaurar" : "Maximizar");
m_maximizeButton->setFixedSize(28, 28);
m_maximizeButton->setIconSize(QSize(16, 16));
connect(m_maximizeButton, &QToolButton::clicked, this, [this]() {
if (isMaximized()) {
showNormal();
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
showMaximized();
}
});
connect(deleteAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
m_mailService->deleteMail(QString::number(m_currentMailId));
m_currentMailId = -1;
m_emailModel->refresh();
m_viewerStack->setCurrentIndex(0);
layout->addWidget(m_maximizeButton);
m_closeButton = new QToolButton();
m_closeButton->setIcon(QIcon(":/icons/close.svg"));
m_closeButton->setToolTip("Cerrar");
m_closeButton->setFixedSize(28, 28);
m_closeButton->setIconSize(QSize(16, 16));
m_closeButton->setStyleSheet(
"QToolButton {"
" border: none;"
" background: transparent;"
" border-radius: 4px;"
" padding: 4px;"
"}"
"QToolButton:hover {"
" background-color: #ff3b30;"
" color: white;"
"}"
"QToolButton:pressed {"
" background-color: #e60000;"
"}"
);
connect(m_closeButton, &QToolButton::clicked, this, &QWidget::close);
layout->addWidget(m_closeButton);
// Set title bar as menu widget (appears in native title bar area on some platforms)
setMenuWidget(m_titleBar);
}
void MainMainWindow::onSyncRequested()
{
if (m_currentFolderId >= 0) {
const auto folder = FolderDao::findById(m_currentFolderId);
if (folder) {
m_mailService->fetchMails(QString::number(folder->accountId()), QString::number(m_currentFolderId));
statusBar()->showMessage(tr("Sincronizando…"), 3000);
}
});
}
}
void MainMainWindow::onSettingsRequested()
{
switchToPage(PageSettings);
}
void MainMainWindow::updateMaximizeButtonIcon()
{
if (m_maximizeButton) {
m_maximizeButton->setIcon(QIcon(isMaximized() ? ":/icons/restore.svg" : ":/icons/maximize.svg"));
m_maximizeButton->setToolTip(isMaximized() ? "Restaurar" : "Maximizar");
}
}
// Frameless window mouse handling
void MainMainWindow::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
QPoint pos = event->position().toPoint();
// Check if clicking on resize edges (8px margin)
int margin = 8;
QRect windowRect = this->rect();
bool onLeft = pos.x() <= margin;
bool onRight = pos.x() >= windowRect.width() - margin;
bool onTop = pos.y() <= margin;
bool onBottom = pos.y() >= windowRect.height() - margin;
m_resizeEdges = Qt::Edges();
if (onTop) m_resizeEdges |= Qt::TopEdge;
if (onBottom) m_resizeEdges |= Qt::BottomEdge;
if (onLeft) m_resizeEdges |= Qt::LeftEdge;
if (onRight) m_resizeEdges |= Qt::RightEdge;
if (m_resizeEdges) {
m_resizeStartPos = event->globalPosition().toPoint();
m_resizeStartGeom = frameGeometry();
m_resizing = true;
event->accept();
return;
}
// Only allow dragging from title bar area (top 36px)
if (pos.y() <= 36) {
m_dragPos = event->globalPosition().toPoint() - frameGeometry().topLeft();
m_dragging = true;
event->accept();
}
}
QMainWindow::mousePressEvent(event);
}
void MainMainWindow::mouseMoveEvent(QMouseEvent *event)
{
// Update cursor when not dragging/resizing
if (!m_dragging && !m_resizing) {
QPoint pos = event->position().toPoint();
int margin = 8;
QRect windowRect = this->rect();
bool onLeft = pos.x() <= margin;
bool onRight = pos.x() >= windowRect.width() - margin;
bool onTop = pos.y() <= margin;
bool onBottom = pos.y() >= windowRect.height() - margin;
Qt::Edges edges = Qt::Edges();
if (onTop) edges |= Qt::TopEdge;
if (onBottom) edges |= Qt::BottomEdge;
if (onLeft) edges |= Qt::LeftEdge;
if (onRight) edges |= Qt::RightEdge;
if (edges & Qt::TopEdge && edges & Qt::LeftEdge) setCursor(Qt::SizeFDiagCursor);
else if (edges & Qt::TopEdge && edges & Qt::RightEdge) setCursor(Qt::SizeBDiagCursor);
else if (edges & Qt::BottomEdge && edges & Qt::LeftEdge) setCursor(Qt::SizeBDiagCursor);
else if (edges & Qt::BottomEdge && edges & Qt::RightEdge) setCursor(Qt::SizeFDiagCursor);
else if (edges & Qt::LeftEdge || edges & Qt::RightEdge) setCursor(Qt::SizeHorCursor);
else if (edges & Qt::TopEdge || edges & Qt::BottomEdge) setCursor(Qt::SizeVerCursor);
else unsetCursor();
}
if (m_resizing && (event->buttons() & Qt::LeftButton)) {
QPoint delta = event->globalPosition().toPoint() - m_resizeStartPos;
QRect newGeom = m_resizeStartGeom;
if (m_resizeEdges & Qt::LeftEdge) {
int newLeft = m_resizeStartGeom.left() + delta.x();
int minWidth = minimumWidth();
if (m_resizeStartGeom.right() - newLeft >= minWidth) {
newGeom.setLeft(newLeft);
}
}
if (m_resizeEdges & Qt::RightEdge) {
int newWidth = m_resizeStartGeom.width() + delta.x();
if (newWidth >= minimumWidth()) {
newGeom.setWidth(newWidth);
}
}
if (m_resizeEdges & Qt::TopEdge) {
int newTop = m_resizeStartGeom.top() + delta.y();
int minHeight = minimumHeight();
if (m_resizeStartGeom.bottom() - newTop >= minHeight) {
newGeom.setTop(newTop);
}
}
if (m_resizeEdges & Qt::BottomEdge) {
int newHeight = m_resizeStartGeom.height() + delta.y();
if (newHeight >= minimumHeight()) {
newGeom.setHeight(newHeight);
}
}
setGeometry(newGeom);
event->accept();
return;
}
if (m_dragging && (event->buttons() & Qt::LeftButton)) {
move(event->globalPosition().toPoint() - m_dragPos);
event->accept();
}
QMainWindow::mouseMoveEvent(event);
}
void MainMainWindow::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
m_dragging = false;
m_resizing = false;
m_resizeEdges = Qt::Edges();
unsetCursor();
event->accept();
}
QMainWindow::mouseReleaseEvent(event);
}
bool MainMainWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr *result)
{
#ifdef Q_OS_WIN
// Handle Windows non-client area hits for resize
MSG *msg = static_cast<MSG *>(message);
if (msg->message == WM_NCHITTEST) {
// Let Windows handle resize borders, but we handle caption
// This allows native resize from edges
*result = 0;
return false; // Let Windows handle it
}
#endif
return QMainWindow::nativeEvent(eventType, message, result);
}
void MainMainWindow::changeEvent(QEvent *event)
{
if (event->type() == QEvent::WindowStateChange) {
updateMaximizeButtonIcon();
m_lastWindowState = windowState();
}
QMainWindow::changeEvent(event);
}
void MainMainWindow::onProgressChanged(int percent)
+52 -3
View File
@@ -9,6 +9,11 @@
#include <QProgressBar>
#include <QToolBar>
#include <QAction>
#include <QToolButton>
#include <QLabel>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QWindowStateChangeEvent>
#include "ui/readerview.h"
#include "ui/maillistview.h"
@@ -18,6 +23,8 @@
#include "ui/calendarview.h"
#include "ui/models/FolderListModel.h"
#include "ui/models/EmailListModel.h"
#include "ui/categorytreewidget.h"
#include "ui/templatesmanagerdialog.h"
#include "services/accountservice.h"
#include "services/mailservice.h"
@@ -28,12 +35,22 @@ public:
explicit MainMainWindow(QWidget *parent = nullptr);
~MainMainWindow() override = default;
protected:
// Frameless window handling
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;
void changeEvent(QEvent *event) override;
private slots:
void onNavChanged(int index);
void onFolderSelected(const QModelIndex &index);
void onEmailSelected(int mailId);
void onComposeRequested();
void onReaderReplyRequested(const MailItem *item);
void onReaderReplyRequested(int mailId);
void onReaderForwardRequested(int mailId);
void onReaderDeleteRequested(int mailId);
void onNewMessage();
void openMailInIndependentWindow(int mailId);
void onAddAccountRequested();
@@ -41,6 +58,20 @@ private slots:
void onAccountDeleteRequested(int accountId);
void onProgressChanged(int percent);
void onStatusMessage(const QString &msg);
void onSyncRequested();
void onSettingsRequested();
void updateMaximizeButtonIcon();
void onCategorySelected(const Category& cat);
void onCategoryDoubleClicked(const Category& cat);
void onAssignCategoryRequested(qint64 mailId, qint64 categoryId);
void onCategoryCreated(const Category& cat);
void onCategoryEdited(const Category& cat);
void onCategoryDeleted(qint64 categoryId);
void onRulesChanged();
void onRunRulesOnFolder();
void onRunRulesOnSelected();
void showRulesManager();
void showTemplatesManager();
// Slots for embedded compose view
void onEmbeddedSendRequested(const QString &to, const QString &cc, const QString &bcc,
@@ -55,8 +86,12 @@ private:
void setupSidebar();
void setupMailPage();
void connectModels();
void createToolBar();
void createTitleBar();
void switchToPage(int pageIndex);
QWidget *createTitleButton(const QString &iconName, const QString &tooltip, std::function<void()> slot);
QWidget *createWindowControlButton(const QString &iconName, const QString &tooltip, std::function<void()> slot);
void updateCategoryTreeForAccount(qint64 accountId);
void assignCategoryToSelectedMails(qint64 categoryId);
// Navigation
QListWidget *m_sidebar;
@@ -74,6 +109,7 @@ private:
QWidget *m_mailPage;
QSplitter *m_folderSplitter;
QTreeView *m_folderTree;
CategoryTreeWidget *m_categoryTree;
MailListView *m_mailListView;
ReaderView *m_emailViewer;
QStackedWidget *m_viewerStack;
@@ -92,7 +128,20 @@ private:
AccountService *m_accountService = nullptr;
MailService *m_mailService = nullptr;
QToolBar *m_toolBar;
// Frameless title bar
QWidget *m_titleBar;
QToolButton *m_maximizeButton;
QToolButton *m_closeButton;
QPoint m_dragPos;
bool m_dragging = false;
Qt::WindowStates m_lastWindowState;
// Resize handling (item 3)
QPoint m_resizeStartPos;
QRect m_resizeStartGeom;
bool m_resizing = false;
Qt::Edges m_resizeEdges = Qt::Edges();
int m_currentFolderId;
int m_currentMailId;
+66 -2
View File
@@ -120,11 +120,75 @@ void EmailListModel::setFolderId(int folderId)
void EmailListModel::refresh()
{
beginResetModel();
QVector<MailItem> allEmails;
if (m_folderId == -1) {
m_emails = MailItemDao::findAll();
allEmails = MailItemDao::findAll();
} else {
m_emails = MailItemDao::findByFolderId(m_folderId);
allEmails = MailItemDao::findByFolderId(m_folderId);
}
// Apply filters
m_emails.clear();
for (const auto &item : allEmails) {
// Search filter
if (!m_searchFilter.isEmpty()) {
if (!item.subject().contains(m_searchFilter, Qt::CaseInsensitive) &&
!item.sender().contains(m_searchFilter, Qt::CaseInsensitive) &&
!item.recipient().contains(m_searchFilter, Qt::CaseInsensitive) &&
!item.bodyHtml().contains(m_searchFilter, Qt::CaseInsensitive)) {
continue;
}
}
// Unread only
if (m_unreadOnly && item.isRead()) {
continue;
}
// Flagged only
if (m_flaggedOnly && !item.isFlagged()) {
continue;
}
// Has attachments
if (m_hasAttachments && item.attachments().isEmpty()) {
continue;
}
m_emails.append(item);
}
endResetModel();
qDebug() << "EmailListModel refreshed with" << m_emails.size() << "emails for folderId" << m_folderId;
}
void EmailListModel::setSearchFilter(const QString &filter)
{
if (m_searchFilter == filter)
return;
m_searchFilter = filter;
refresh();
}
void EmailListModel::setShowUnreadOnly(bool show)
{
if (m_unreadOnly == show)
return;
m_unreadOnly = show;
refresh();
}
void EmailListModel::setShowFlaggedOnly(bool show)
{
if (m_flaggedOnly == show)
return;
m_flaggedOnly = show;
refresh();
}
void EmailListModel::setShowHasAttachments(bool show)
{
if (m_hasAttachments == show)
return;
m_hasAttachments = show;
refresh();
}
+10
View File
@@ -46,9 +46,19 @@ public:
void setFolderId(int folderId);
// Refresh the model from the database based on current folderId
void refresh();
// Set search filter on the model
void setSearchFilter(const QString &filter);
// Set filter flags
void setShowUnreadOnly(bool show);
void setShowFlaggedOnly(bool show);
void setShowHasAttachments(bool show);
private:
QVector<MailItem> m_emails;
int m_folderId{-1}; // -1 means all folders
QString m_searchFilter;
bool m_unreadOnly{false};
bool m_flaggedOnly{false};
bool m_hasAttachments{false};
};
#endif // EMAILLISTMODEL_H
+842 -78
View File
@@ -2,106 +2,870 @@
#include <QFont>
#include <QDesktopServices>
#include <QUrl>
#include <QDebug>
#include <QMimeDatabase>
#include <QMimeType>
#include <QFileInfo>
#include <QMenu>
#include <QApplication>
#include <QStyle>
#include <QTextDocument>
#include <QTextCursor>
#include <QRegularExpression>
#include <QDateTime>
#include <QClipboard>
#include <QMessageBox>
#include <QScrollBar>
#include <QShortcut>
#include <QFileDialog>
#include <QDir>
#include <functional>
#include "db/dao/mailitemdao.h"
ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
setupUI();
applyEmailCss();
}
void ReaderView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(15, 15, 15, 15);
mainLayout->setSpacing(10);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// Header Section
QWidget *headerWidget = new QWidget();
QVBoxLayout *headerLayout = new QVBoxLayout(headerWidget);
headerLayout->setSpacing(5);
// ===== Toolbar (zoom, find, images) =====
m_toolbar = new QFrame();
m_toolbar->setFixedHeight(36);
m_toolbar->setStyleSheet("QFrame { background: #fafafa; border-bottom: 1px solid #e0e0e0; }");
QHBoxLayout *toolbarLayout = new QHBoxLayout(m_toolbar);
toolbarLayout->setContentsMargins(8, 4, 8, 4);
toolbarLayout->setSpacing(8);
m_subjectLabel = new QLabel();
QFont subjectFont = m_subjectLabel->font();
subjectFont.setBold(true);
subjectFont.setPointSize(14);
m_subjectLabel->setFont(subjectFont);
m_subjectLabel->setText("No subject");
m_subjectLabel->setWordWrap(true);
m_fromLabel = new QLabel();
m_fromLabel->setText("From: ");
m_dateLabel = new QLabel();
m_dateLabel->setText("Date: ");
m_dateLabel->setStyleSheet("color: gray; font-style: italic;");
headerLayout->addWidget(m_subjectLabel);
headerLayout->addWidget(m_fromLabel);
headerLayout->addWidget(m_dateLabel);
// Action Buttons
QHBoxLayout *actionsLayout = new QHBoxLayout();
m_replyButton = new QPushButton("Reply");
m_forwardButton = new QPushButton("Forward");
m_deleteButton = new QPushButton("Delete");
m_deleteButton->setStyleSheet("color: red;");
QPushButton *m_detachButton = new QPushButton("↗ Abrir en ventana");
m_detachButton->setToolTip("Abrir este correo en una ventana independiente");
actionsLayout->addWidget(m_replyButton);
actionsLayout->addWidget(m_forwardButton);
actionsLayout->addWidget(m_detachButton);
actionsLayout->addStretch();
actionsLayout->addWidget(m_deleteButton);
// Body Viewer
m_bodyViewer = new QTextBrowser();
m_bodyViewer->setOpenExternalLinks(true);
m_bodyViewer->setFrameStyle(QFrame::NoFrame);
m_attachmentList = new QListWidget();
m_attachmentList->setVisible(false);
m_attachmentList->setMaximumHeight(110);
m_attachmentList->setToolTip(tr("Double-click an attachment to open it"));
connect(m_attachmentList, &QListWidget::itemDoubleClicked, this,
[](QListWidgetItem *listItem) {
const QString path = listItem->data(Qt::UserRole).toString();
if (!path.isEmpty()) QDesktopServices::openUrl(QUrl::fromLocalFile(path));
});
mainLayout->addWidget(headerWidget);
mainLayout->addLayout(actionsLayout);
mainLayout->addWidget(m_attachmentList);
mainLayout->addWidget(m_bodyViewer);
// Connections
connect(m_replyButton, &QPushButton::clicked, [this]() {
if (m_bodyViewer->toPlainText().isEmpty()) return;
// Load images button (shows when external images are blocked)
m_loadImagesButton = new QToolButton();
m_loadImagesButton->setText("🖼 Cargar imágenes");
m_loadImagesButton->setToolTip("Cargar imágenes externas bloqueadas (tracking pixels, etc.)");
m_loadImagesButton->setFixedHeight(28);
m_loadImagesButton->setVisible(false);
m_loadImagesButton->setStyleSheet(
"QToolButton {"
" background: #e3f2fd;"
" border: 1px solid #1976D2;"
" border-radius: 4px;"
" padding: 4px 10px;"
" font-size: 11px;"
" color: #1976D2;"
" font-weight: 500;"
"}"
"QToolButton:hover {"
" background: #bbdefb;"
"}"
);
connect(m_loadImagesButton, &QToolButton::clicked, [this]() {
m_allowExternalImages = true;
m_loadImagesButton->setVisible(false);
refreshCurrentMail();
});
connect(m_detachButton, &QPushButton::clicked, this, &ReaderView::detachRequested);
m_zoomOutButton = new QToolButton();
m_zoomOutButton->setText("");
m_zoomOutButton->setFixedSize(28, 28);
m_zoomOutButton->setToolTip("Zoom out (Ctrl+-)");
connect(m_zoomOutButton, &QToolButton::clicked, [this]() {
m_zoomFactor = qMax(0.5, m_zoomFactor - 0.1);
updateZoom();
});
m_zoomLabel = new QLabel("100%");
m_zoomLabel->setFixedWidth(50);
m_zoomLabel->setAlignment(Qt::AlignCenter);
m_zoomLabel->setStyleSheet("font-size: 11px; color: #555;");
m_zoomInButton = new QToolButton();
m_zoomInButton->setText("+");
m_zoomInButton->setFixedSize(28, 28);
m_zoomInButton->setToolTip("Zoom in (Ctrl++)");
connect(m_zoomInButton, &QToolButton::clicked, [this]() {
m_zoomFactor = qMin(3.0, m_zoomFactor + 0.1);
updateZoom();
});
m_zoomResetButton = new QToolButton();
m_zoomResetButton->setText("100%");
m_zoomResetButton->setFixedSize(50, 28);
m_zoomResetButton->setToolTip("Reset zoom (Ctrl+0)");
connect(m_zoomResetButton, &QToolButton::clicked, [this]() {
m_zoomFactor = 1.0;
updateZoom();
});
toolbarLayout->addWidget(m_loadImagesButton);
toolbarLayout->addStretch();
toolbarLayout->addWidget(m_zoomOutButton);
toolbarLayout->addWidget(m_zoomLabel);
toolbarLayout->addWidget(m_zoomInButton);
toolbarLayout->addWidget(m_zoomResetButton);
toolbarLayout->addSpacing(20);
// Find bar (hidden by default)
m_findBar = new QFrame();
m_findBar->setFixedHeight(36);
m_findBar->setVisible(false);
m_findBar->setStyleSheet("QFrame { background: #fff3cd; border-bottom: 1px solid #ffc107; }");
QHBoxLayout *findLayout = new QHBoxLayout(m_findBar);
findLayout->setContentsMargins(8, 4, 8, 4);
findLayout->setSpacing(8);
QLabel *findLabel = new QLabel("Buscar:");
findLabel->setStyleSheet("font-weight: bold; color: #856404;");
m_findInput = new QLineEdit();
m_findInput->setPlaceholderText("Buscar en el mensaje...");
m_findInput->setFixedHeight(28);
m_findInput->setStyleSheet("QLineEdit { border: 1px solid #ffc107; border-radius: 3px; padding: 2px 8px; background: white; }");
connect(m_findInput, &QLineEdit::textChanged, [this](const QString &text) {
if (!text.isEmpty()) {
bool found = m_bodyViewer->find(text, QTextDocument::FindCaseSensitively);
if (!found) {
m_bodyViewer->moveCursor(QTextCursor::Start);
m_bodyViewer->find(text, QTextDocument::FindCaseSensitively);
}
}
// Update count
QString allText = m_bodyViewer->toPlainText();
int count = 0;
int pos = 0;
while ((pos = allText.indexOf(text, pos, Qt::CaseInsensitive)) != -1) {
count++;
pos += text.length();
}
m_findCountLabel->setText(QString("%1 coincidencias").arg(count));
});
connect(m_findInput, &QLineEdit::returnPressed, [this]() {
m_bodyViewer->find(m_findInput->text(), QTextDocument::FindCaseSensitively);
});
m_findPrevButton = new QToolButton();
m_findPrevButton->setText("");
m_findPrevButton->setFixedSize(28, 28);
m_findPrevButton->setToolTip("Anterior (Shift+Enter)");
connect(m_findPrevButton, &QToolButton::clicked, [this]() {
m_bodyViewer->find(m_findInput->text(), QTextDocument::FindBackward | QTextDocument::FindCaseSensitively);
});
m_findNextButton = new QToolButton();
m_findNextButton->setText("");
m_findNextButton->setFixedSize(28, 28);
m_findNextButton->setToolTip("Siguiente (Enter)");
connect(m_findNextButton, &QToolButton::clicked, [this]() {
m_bodyViewer->find(m_findInput->text(), QTextDocument::FindCaseSensitively);
});
m_findCloseButton = new QToolButton();
m_findCloseButton->setText("");
m_findCloseButton->setFixedSize(28, 28);
m_findCloseButton->setToolTip("Cerrar búsqueda (Esc)");
connect(m_findCloseButton, &QToolButton::clicked, [this]() {
m_findBar->setVisible(false);
m_findInput->clear();
m_bodyViewer->moveCursor(QTextCursor::End);
});
m_findCountLabel = new QLabel();
m_findCountLabel->setStyleSheet("color: #856404; font-size: 11px;");
findLayout->addWidget(findLabel);
findLayout->addWidget(m_findInput, 1);
findLayout->addWidget(m_findPrevButton);
findLayout->addWidget(m_findNextButton);
findLayout->addWidget(m_findCountLabel);
findLayout->addWidget(m_findCloseButton);
mainLayout->addWidget(m_toolbar);
mainLayout->addWidget(m_findBar);
// ===== Header Section =====
m_headerWidget = new QWidget();
m_headerWidget->setStyleSheet("QWidget { background: white; border-bottom: 1px solid #e0e0e0; }");
QVBoxLayout *headerLayout = new QVBoxLayout(m_headerWidget);
headerLayout->setContentsMargins(16, 12, 16, 12);
headerLayout->setSpacing(8);
// Subject row
QHBoxLayout *subjectRow = new QHBoxLayout();
subjectRow->setSpacing(12);
m_avatarLabel = new QLabel();
m_avatarLabel->setFixedSize(40, 40);
m_avatarLabel->setAlignment(Qt::AlignCenter);
m_avatarLabel->setStyleSheet("QLabel { background: #1976D2; color: white; border-radius: 20px; font-weight: bold; font-size: 14px; }");
subjectRow->addWidget(m_avatarLabel);
m_subjectLabel = new QLabel();
m_subjectLabel->setText("(Sin asunto)");
QFont subjectFont = m_subjectLabel->font();
subjectFont.setBold(true);
subjectFont.setPointSize(16);
m_subjectLabel->setFont(subjectFont);
m_subjectLabel->setWordWrap(true);
m_subjectLabel->setStyleSheet("color: #1d1d1f;");
m_subjectLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
subjectRow->addWidget(m_subjectLabel, 1);
headerLayout->addLayout(subjectRow);
// From / To / Date row
QHBoxLayout *metaRow = new QHBoxLayout();
metaRow->setSpacing(16);
m_fromLabel = new QLabel();
m_fromLabel->setStyleSheet("color: #333; font-size: 13px;");
m_fromLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_fromLabel->setCursor(Qt::PointingHandCursor);
connect(m_fromLabel, &QLabel::linkActivated, [this](const QString &link) {
QDesktopServices::openUrl(QUrl(link));
});
metaRow->addWidget(m_fromLabel, 1);
m_toLabel = new QLabel();
m_toLabel->setStyleSheet("color: #666; font-size: 12px;");
m_toLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_toLabel->setWordWrap(true);
metaRow->addWidget(m_toLabel, 2);
m_dateLabel = new QLabel();
m_dateLabel->setStyleSheet("color: #888; font-size: 12px;");
metaRow->addWidget(m_dateLabel);
headerLayout->addLayout(metaRow);
// Actions row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->setSpacing(4);
auto createActionButton = [&](const QString &text, const QString &tooltip, std::function<void()> callback) -> QToolButton* {
QToolButton *btn = new QToolButton();
btn->setText(text);
btn->setToolTip(tooltip);
btn->setFixedHeight(32);
btn->setMinimumWidth(80);
btn->setStyleSheet(
"QToolButton {"
" background: transparent;"
" border: 1px solid #d1d1d6;"
" border-radius: 4px;"
" padding: 4px 12px;"
" font-size: 12px;"
" color: #333;"
"}"
"QToolButton:hover {"
" background: #f0f0f0;"
" border-color: #bbb;"
"}"
"QToolButton:pressed {"
" background: #e0e0e0;"
"}"
);
connect(btn, &QToolButton::clicked, [callback]() { callback(); });
return btn;
};
m_replyButton = createActionButton("↩ Responder", "Responder (Ctrl+R)", [this]() { onReplyClicked(); });
m_forwardButton = createActionButton("⤴ Reenviar", "Reenviar (Ctrl+Shift+F)", [this]() { onForwardClicked(); });
m_deleteButton = createActionButton("🗑 Eliminar", "Eliminar (Del)", [this]() { onDeleteClicked(); });
m_deleteButton->setStyleSheet(m_deleteButton->styleSheet() + "QToolButton { color: #d32f2f; border-color: #ef9a9a; } QToolButton:hover { background: #fdeaea; border-color: #ef5350; }");
m_detachButton = createActionButton("↗ Ventana", "Abrir en ventana independiente", [this]() { onDetachClicked(); });
m_moreButton = new QToolButton();
m_moreButton->setText("");
m_moreButton->setFixedSize(32, 32);
m_moreButton->setToolTip("Más opciones");
m_moreButton->setPopupMode(QToolButton::InstantPopup);
QMenu *moreMenu = new QMenu(this);
moreMenu->addAction("Copiar asunto", [this]() {
if (m_currentMailId >= 0) {
std::optional<MailItem> item = MailItemDao::findById(m_currentMailId);
if (item.has_value()) QApplication::clipboard()->setText(item->subject());
}
});
moreMenu->addAction("Copiar remitente", [this]() {
if (m_currentMailId >= 0) {
std::optional<MailItem> item = MailItemDao::findById(m_currentMailId);
if (item.has_value()) QApplication::clipboard()->setText(item->sender());
}
});
moreMenu->addAction("Ver código fuente", [this]() { /* TODO */ });
moreMenu->addSeparator();
moreMenu->addAction("Marcar como no leído", [this]() { /* TODO */ });
moreMenu->addAction("Marcar como spam", [this]() { /* TODO */ });
m_moreButton->setMenu(moreMenu);
m_moreButton->setStyleSheet(
"QToolButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; font-size: 16px; }"
"QToolButton:hover { background: #f0f0f0; }"
);
actionsLayout->addWidget(m_replyButton);
actionsLayout->addWidget(m_forwardButton);
actionsLayout->addWidget(m_deleteButton);
actionsLayout->addWidget(m_detachButton);
actionsLayout->addStretch();
actionsLayout->addWidget(m_moreButton);
headerLayout->addLayout(actionsLayout);
mainLayout->addWidget(m_headerWidget);
// ===== Attachments Area =====
setupAttachmentsArea();
mainLayout->addWidget(m_attachmentsFrame);
// ===== Body Viewer =====
setupBodyViewer();
mainLayout->addWidget(m_scrollArea, 1);
// Shortcuts
QShortcut *findShortcut = new QShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_F), this);
connect(findShortcut, &QShortcut::activated, [this]() {
m_findBar->setVisible(true);
m_findInput->setFocus();
});
QShortcut *zoomInShortcut = new QShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_Plus), this);
connect(zoomInShortcut, &QShortcut::activated, [this]() {
m_zoomFactor = qMin(3.0, m_zoomFactor + 0.1);
updateZoom();
});
QShortcut *zoomOutShortcut = new QShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_Minus), this);
connect(zoomOutShortcut, &QShortcut::activated, [this]() {
m_zoomFactor = qMax(0.5, m_zoomFactor - 0.1);
updateZoom();
});
QShortcut *zoomResetShortcut = new QShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_0), this);
connect(zoomResetShortcut, &QShortcut::activated, [this]() {
m_zoomFactor = 1.0;
updateZoom();
});
}
void ReaderView::setupBodyViewer() {
m_scrollArea = new QScrollArea();
m_scrollArea->setWidgetResizable(true);
m_scrollArea->setFrameStyle(QFrame::NoFrame);
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_scrollArea->setStyleSheet("QScrollArea { background: white; border: none; }");
m_bodyContainer = new QWidget();
m_bodyLayout = new QVBoxLayout(m_bodyContainer);
m_bodyLayout->setContentsMargins(16, 16, 16, 16);
m_bodyLayout->setSpacing(0);
m_bodyViewer = new QTextBrowser();
m_bodyViewer->setOpenExternalLinks(false); // We'll handle link clicks securely
m_bodyViewer->setFrameStyle(QFrame::NoFrame);
m_bodyViewer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_bodyViewer->setStyleSheet("QTextBrowser { background: transparent; border: none; font-family: 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 13px; line-height: 1.5; color: #333; }");
m_bodyViewer->setMouseTracking(true);
// Handle link clicks securely
connect(m_bodyViewer, &QTextBrowser::anchorClicked, [this](const QUrl &url) {
QString scheme = url.scheme().toLower();
if (scheme == "http" || scheme == "https") {
// Ask before opening external links
if (QMessageBox::question(this, "Enlace externo",
QString("Abrir enlace externo?\n%1").arg(url.toString()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
QDesktopServices::openUrl(url);
}
} else if (scheme == "mailto") {
QDesktopServices::openUrl(url);
}
});
// Context menu for copy, etc.
m_bodyViewer->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_bodyViewer, &QWidget::customContextMenuRequested, [this](const QPoint &pos) {
QMenu *menu = m_bodyViewer->createStandardContextMenu(pos);
menu->addSeparator();
menu->addAction("Seleccionar todo", [this]() { m_bodyViewer->selectAll(); });
menu->addAction("Buscar...", [this]() {
m_findBar->setVisible(true);
m_findInput->setFocus();
});
menu->exec(m_bodyViewer->mapToGlobal(pos));
delete menu;
});
m_bodyLayout->addWidget(m_bodyViewer);
m_scrollArea->setWidget(m_bodyContainer);
}
void ReaderView::setupAttachmentsArea() {
m_attachmentsFrame = new QFrame();
m_attachmentsFrame->setVisible(false);
m_attachmentsFrame->setStyleSheet("QFrame { background: #fafafa; border-top: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0; }");
m_attachmentsLayout = new QVBoxLayout(m_attachmentsFrame);
m_attachmentsLayout->setContentsMargins(16, 8, 16, 8);
m_attachmentsLayout->setSpacing(8);
m_attachmentsHeader = new QLabel("Adjuntos");
QFont hdrFont = m_attachmentsHeader->font();
hdrFont.setBold(true);
hdrFont.setPointSize(11);
m_attachmentsHeader->setFont(hdrFont);
m_attachmentsHeader->setStyleSheet("color: #555;");
m_attachmentsLayout->addWidget(m_attachmentsHeader);
m_attachmentList = new QListWidget();
m_attachmentList->setFixedHeight(80);
m_attachmentList->setStyleSheet(
"QListWidget { background: white; border: 1px solid #e0e0e0; border-radius: 6px; padding: 4px; }"
"QListWidget::item { border: none; padding: 6px 8px; border-radius: 4px; }"
"QListWidget::item:hover { background: #f0f0f0; }"
"QListWidget::item:selected { background: #e3f2fd; color: #1976D2; }"
);
m_attachmentList->setSpacing(2);
m_attachmentList->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_attachmentList, &QListWidget::customContextMenuRequested, [this](const QPoint &pos) {
QListWidgetItem *item = m_attachmentList->itemAt(pos);
if (item) {
QString path = item->data(Qt::UserRole).toString();
QString name = item->text();
showAttachmentContextMenu(pos, path, name);
}
});
connect(m_attachmentList, &QListWidget::itemDoubleClicked, [this](QListWidgetItem *item) {
QString path = item->data(Qt::UserRole).toString();
if (!path.isEmpty()) {
emit openAttachmentRequested(path);
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
}
});
m_attachmentsLayout->addWidget(m_attachmentList);
}
void ReaderView::setupFindBar() {
// Already set up in setupUI
}
void ReaderView::applyEmailCss() {
// Base CSS for email rendering - injected into document
m_baseCss = R"(
/* Email CSS Reset & Base */
body { margin: 0; padding: 0; font-family: 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 13px; line-height: 1.55; color: #333; background: white; }
.email-wrapper { max-width: 100%; margin: 0 auto; }
.email-body { word-wrap: break-word; overflow-wrap: break-word; }
/* Typography */
h1, h2, h3, h4, h5, h6 { margin: 16px 0 8px; font-weight: 600; line-height: 1.3; color: #1d1d1f; }
h1 { font-size: 28px; } h2 { font-size: 24px; } h3 { font-size: 20px; } h4 { font-size: 16px; }
p { margin: 0 0 12px; }
a { color: #1976D2; text-decoration: none; }
a:hover { text-decoration: underline; }
/* Lists */
ul, ol { margin: 8px 0; padding-left: 24px; }
li { margin: 4px 0; }
/* Blockquotes (replies/forwards) */
blockquote { margin: 12px 0; padding: 8px 16px; border-left: 3px solid #1976D2; background: #f5f5f5; color: #555; font-style: italic; }
blockquote blockquote { border-left-color: #4CAF50; background: #f1f8e9; }
blockquote blockquote blockquote { border-left-color: #FF9800; background: #fff8e1; }
/* Tables */
table { border-collapse: collapse; width: 100%; max-width: 100%; margin: 12px 0; }
th, td { border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; }
th { background: #f5f5f5; font-weight: 600; }
tr:nth-child(even) td { background: #fafafa; }
/* Images */
img { max-width: 100%; height: auto; border-radius: 4px; }
img[src^="cid:"] { opacity: 0.6; } /* Embedded images placeholder */
/* Code */
code { background: #f5f5f5; padding: 2px 6px; border-radius: 3px; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }
pre { background: #2d2d2d; color: #f8f8f2; padding: 16px; border-radius: 6px; overflow-x: auto; margin: 12px 0; }
pre code { background: transparent; padding: 0; color: inherit; }
/* Horizontal rule */
hr { border: none; border-top: 1px solid #e0e0e0; margin: 16px 0; }
/* Email-specific */
.email-header { background: #fafafa; padding: 12px 16px; border-bottom: 1px solid #e0e0e0; margin: -16px -16px 16px; font-size: 12px; color: #666; }
.email-signature { border-top: 1px solid #e0e0e0; margin-top: 24px; padding-top: 12px; color: #888; font-size: 12px; }
.email-quote { margin: 16px 0; padding-left: 16px; border-left: 3px solid #ccc; color: #666; }
/* Dark mode adjustments (applied via class on body) */
body.dark { background: #1e1e1e; color: #e0e0e0; }
body.dark h1, body.dark h2, body.dark h3, body.dark h4 { color: #fff; }
body.dark blockquote { background: #2a2a2a; border-left-color: #64b5f6; color: #bbb; }
body.dark blockquote blockquote { background: #263238; border-left-color: #81c784; }
body.dark blockquote blockquote blockquote { background: #3e2723; border-left-color: #ffb74d; }
body.dark table, body.dark th, body.dark td { border-color: #333; }
body.dark th { background: #2a2a2a; }
body.dark tr:nth-child(even) td { background: #252525; }
body.dark code { background: #2a2a2a; color: #e0e0e0; }
body.dark pre { background: #1a1a1a; }
body.dark .email-header { background: #2a2a2a; border-bottom-color: #333; color: #aaa; }
body.dark .email-signature { border-top-color: #333; color: #aaa; }
body.dark .email-quote { border-left-color: #444; color: #aaa; }
body.dark a { color: #90caf9; }
body.dark img[src^="cid:"] { opacity: 0.4; }
)";
}
void ReaderView::updateZoom() {
m_zoomLabel->setText(QString("%1%").arg(qRound(m_zoomFactor * 100)));
QFont f = m_bodyViewer->font();
f.setPointSizeF(13 * m_zoomFactor);
m_bodyViewer->setFont(f);
// Also scale the whole container for better zoom using document zoom
// QTextBrowser doesn't have setZoomFactor, but we can use zoom on the document
// The font scaling above handles it
}
QString ReaderView::sanitizeHtml(const QString &html) {
if (html.isEmpty()) return "<div class='email-body'><i style='color:#999'>(Mensaje vacío)</i></div>";
QString result = html;
// Wrap in email wrapper if not already structured
if (!result.contains("<body", Qt::CaseInsensitive) && !result.contains("<div class=\"email", Qt::CaseInsensitive)) {
result = QString("<div class=\"email-wrapper\"><div class=\"email-body\">%1</div></div>").arg(result);
}
// Security: Remove scripts, iframes, event handlers
result.remove(QRegularExpression("(?is)<script[^>]*>.*?</script>"));
result.remove(QRegularExpression("(?is)<iframe[^>]*>.*?</iframe>"));
result.remove(QRegularExpression("(?is)\\s(on\\w+)\\s*=\\s*\"[^\"]*\""));
result.remove(QRegularExpression("(?is)\\s(on\\w+)\\s*=\\s*'[^']*'"));
result.remove(QRegularExpression("(?is)\\s(on\\w+)\\s*=\\s*\\w+"));
// Block external images (tracking pixels, etc.) - replace with placeholder, unless allowed
if (!m_allowExternalImages) {
// Match images with http/https/data URLs in src attribute
QRegularExpression extImgRegex("(?i)<img([^>]*src\\s*=\\s*[\"'](?:https?|data):[^\"']*[\"'][^>]*)>");
int beforeCount = result.count("data-blocked=\"true\"");
result.replace(extImgRegex,
"<img\\1 style=\"max-width:100%;height:auto;\" data-blocked=\"true\" title=\"Imagen externa bloqueada por privacidad\">");
int afterCount = result.count("data-blocked=\"true\"");
qDebug() << "[ReaderView] External images blocked:" << (afterCount - beforeCount);
// Show load images button if there were blocked images
if (m_loadImagesButton && result.contains("data-blocked=\"true\"")) {
m_loadImagesButton->setVisible(true);
}
}
// Add dark mode class if needed
if (m_darkMode) {
result.replace("<body", "<body class=\"dark\"");
result.replace("<div class=\"email-wrapper\"", "<div class=\"email-wrapper\"><body class=\"dark\">");
result.replace("</div></div>", "</body></div></div>");
}
// Inject base CSS
QString styleTag = QString("<style>%1</style>").arg(m_baseCss);
if (result.contains("<head>", Qt::CaseInsensitive)) {
result.replace(QRegularExpression("(?i)</head>"), styleTag + "</head>");
} else if (result.contains("<html", Qt::CaseInsensitive)) {
result.replace(QRegularExpression("(?i)<html[^>]*>"), QString("<html>%1").arg(styleTag));
} else {
result = styleTag + result;
}
return result;
}
QString ReaderView::formatAddress(const QString &raw) {
if (raw.isEmpty()) return "";
// Extract name and email from "Name <email@domain>" format
QRegularExpression re("^\\s*(.*?)\\s*<([^>]+)>\\s*$");
QRegularExpressionMatch match = re.match(raw);
if (match.hasMatch()) {
QString name = match.captured(1).trimmed();
QString email = match.captured(2).trimmed();
if (!name.isEmpty()) {
return QString("<a href=\"mailto:%1\" style=\"color:#1976D2;text-decoration:none;\">%2</a> <%1>").arg(email, name);
}
return QString("<a href=\"mailto:%1\" style=\"color:#1976D2;text-decoration:none;\">%1</a>").arg(email);
}
// Just an email
if (raw.contains("@")) {
return QString("<a href=\"mailto:%1\" style=\"color:#1976D2;text-decoration:none;\">%1</a>").arg(raw);
}
// Just a name
return raw.toHtmlEscaped();
}
QString ReaderView::formatDate(const QDateTime &dt) {
if (!dt.isValid()) return "";
QDateTime now = QDateTime::currentDateTime();
if (dt.date() == now.date()) {
return dt.toString("'Hoy' hh:mm");
} else if (dt.date() == now.date().addDays(-1)) {
return dt.toString("'Ayer' hh:mm");
} else if (dt.date().year() == now.date().year()) {
return dt.toString("ddd, d MMM 'a las' hh:mm");
} else {
return dt.toString("ddd, d MMM yyyy 'a las' hh:mm");
}
}
QIcon ReaderView::mimeIcon(const QString &mimeType) {
QStyle *style = QApplication::style();
if (mimeType.startsWith("image/")) return QIcon::fromTheme("image-x-generic", style->standardIcon(QStyle::SP_FileIcon));
if (mimeType.startsWith("application/pdf")) return QIcon::fromTheme("application-pdf", style->standardIcon(QStyle::SP_FileIcon));
if (mimeType.startsWith("application/msword") || mimeType.contains("wordprocessingml")) return QIcon::fromTheme("application-msword", style->standardIcon(QStyle::SP_FileIcon));
if (mimeType.contains("spreadsheet") || mimeType.contains("excel")) return QIcon::fromTheme("application-vnd.ms-excel", style->standardIcon(QStyle::SP_FileIcon));
if (mimeType.contains("presentation") || mimeType.contains("powerpoint")) return QIcon::fromTheme("application-vnd.ms-powerpoint", style->standardIcon(QStyle::SP_FileIcon));
if (mimeType.startsWith("text/")) return QIcon::fromTheme("text-plain", style->standardIcon(QStyle::SP_FileIcon));
if (mimeType.startsWith("audio/")) return QIcon::fromTheme("audio-x-generic", style->standardIcon(QStyle::SP_FileIcon));
if (mimeType.startsWith("video/")) return QIcon::fromTheme("video-x-generic", style->standardIcon(QStyle::SP_FileIcon));
if (mimeType.contains("zip") || mimeType.contains("compressed") || mimeType.contains("archive")) return QIcon::fromTheme("package-x-generic", style->standardIcon(QStyle::SP_FileIcon));
return QIcon::fromTheme("unknown", style->standardIcon(QStyle::SP_FileIcon));
}
void ReaderView::showAttachmentContextMenu(const QPoint &pos, const QString &path, const QString &name) {
QMenu menu(this);
QAction *openAct = menu.addAction("Abrir", [this, path]() {
emit openAttachmentRequested(path);
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
});
openAct->setIcon(mimeIcon(QMimeDatabase().mimeTypeForFile(path).name()));
QAction *saveAct = menu.addAction("Guardar como...", [this, path, name]() {
QString savePath = QFileDialog::getSaveFileName(this, "Guardar adjunto", QDir::homePath() + "/" + name);
if (!savePath.isEmpty()) {
QFile::copy(path, savePath);
}
});
saveAct->setIcon(QIcon::fromTheme("document-save", QApplication::style()->standardIcon(QStyle::SP_DialogSaveButton)));
QAction *copyPathAct = menu.addAction("Copiar ruta", [path]() {
QApplication::clipboard()->setText(path);
});
copyPathAct->setIcon(QIcon::fromTheme("edit-copy", QApplication::style()->standardIcon(QStyle::SP_DialogApplyButton)));
menu.addSeparator();
QAction *deleteAct = menu.addAction("Eliminar adjunto", [this, name]() {
// TODO: Implement attachment deletion from stored mail
QMessageBox::information(this, "No implementado", "Eliminar adjuntos del almacenamiento local aún no está implementado.");
});
deleteAct->setIcon(QIcon::fromTheme("edit-delete", QApplication::style()->standardIcon(QStyle::SP_TrashIcon)));
menu.exec(m_attachmentList->mapToGlobal(pos));
}
void ReaderView::setMailItem(const MailItem* item) {
if (!item) {
m_subjectLabel->setText("No mail selected");
m_fromLabel->setText("From: ");
m_dateLabel->setText("Date: ");
m_currentMailId = -1;
m_allowExternalImages = false;
m_loadImagesButton->setVisible(false);
m_subjectLabel->setText("(Sin asunto)");
m_avatarLabel->setText("?");
m_fromLabel->setText("Remitente: —");
m_toLabel->setText("Para: —");
m_dateLabel->setText("Fecha: —");
m_attachmentList->clear();
m_attachmentList->setVisible(false);
m_bodyViewer->setHtml("<i>Please select a message to read</i>");
m_attachmentsFrame->setVisible(false);
m_bodyViewer->setHtml("<div class='email-body'><i style='color:#999'>(Seleccione un correo para leerlo)</i></div>");
return;
}
m_subjectLabel->setText(item->subject());
m_fromLabel->setText(QString("From: %1").arg(item->sender()));
m_dateLabel->setText(QString("Date: %1").arg(item->date().toString("ddd, d MMM yyyy hh:mm")));
m_currentMailId = item->id();
m_allowExternalImages = false;
m_loadImagesButton->setVisible(false);
// Subject
m_subjectLabel->setText(item->subject().isEmpty() ? "(Sin asunto)" : item->subject());
// Avatar - use first letter of sender name
QString sender = item->sender();
QString avatarChar = "?";
QRegularExpression re("^\\s*(.*?)\\s*<[^>]+>\\s*$");
QRegularExpressionMatch match = re.match(sender);
if (match.hasMatch()) {
QString name = match.captured(1).trimmed();
if (!name.isEmpty()) avatarChar = name[0].toUpper();
} else if (!sender.isEmpty() && sender.contains("@")) {
avatarChar = sender[0].toUpper();
}
m_avatarLabel->setText(avatarChar);
// From
m_fromLabel->setText("De: " + formatAddress(sender));
// To - MailItem doesn't have recipients easily accessible, skip for now
m_toLabel->setText("Para: —"); // TODO: fetch from MailItem or DB
// Date
m_dateLabel->setText("Fecha: " + formatDate(item->date()));
// Attachments
m_attachmentList->clear();
const QVector<StoredAttachmentRecord> attachments = MailItemDao::attachmentsForMail(item->id());
for (const StoredAttachmentRecord &attachment : attachments) {
auto *listItem = new QListWidgetItem(attachment.fileName, m_attachmentList);
QListWidgetItem *listItem = new QListWidgetItem(m_attachmentList);
QWidget *widget = new QWidget();
QHBoxLayout *layout = new QHBoxLayout(widget);
layout->setContentsMargins(8, 4, 8, 4);
layout->setSpacing(10);
// Icon
QLabel *iconLabel = new QLabel();
QMimeDatabase mimeDb;
QMimeType mime = mimeDb.mimeTypeForFileNameAndData(attachment.fileName, QByteArray());
QIcon icon = mimeIcon(mime.name());
iconLabel->setPixmap(icon.pixmap(24, 24));
iconLabel->setFixedSize(28, 28);
layout->addWidget(iconLabel);
// Name + size
QVBoxLayout *textLayout = new QVBoxLayout();
textLayout->setSpacing(1);
textLayout->setContentsMargins(0, 0, 0, 0);
QLabel *nameLabel = new QLabel(attachment.fileName);
nameLabel->setStyleSheet("font-weight: 500; font-size: 12px; color: #333;");
QLabel *sizeLabel = new QLabel(QString("%1 KB").arg(attachment.size / 1024));
sizeLabel->setStyleSheet("font-size: 10px; color: #888;");
textLayout->addWidget(nameLabel);
textLayout->addWidget(sizeLabel);
layout->addLayout(textLayout, 1);
widget->setLayout(layout);
listItem->setSizeHint(widget->sizeHint());
listItem->setData(Qt::UserRole, attachment.storedPath);
listItem->setToolTip(attachment.storedPath);
listItem->setToolTip(QString("%1 (%2 KB)").arg(attachment.fileName).arg(attachment.size / 1024));
m_attachmentList->addItem(listItem);
m_attachmentList->setItemWidget(listItem, widget);
}
m_attachmentList->setVisible(!attachments.isEmpty());
m_bodyViewer->setHtml(item->bodyHtml());
m_attachmentsFrame->setVisible(!attachments.isEmpty());
// Body with sanitization + CSS
QString cleanHtml = sanitizeHtml(item->bodyHtml());
m_bodyViewer->setHtml(cleanHtml);
// Scroll to top
m_bodyViewer->moveCursor(QTextCursor::Start);
QScrollBar *vbar = m_scrollArea->verticalScrollBar();
if (vbar) vbar->setValue(0);
}
void ReaderView::refreshCurrentMail() {
if (m_currentMailId < 0) return;
std::optional<MailItem> item = MailItemDao::findById(m_currentMailId);
if (!item.has_value()) {
m_currentMailId = -1;
setMailItem(nullptr);
return;
}
// Re-apply without resetting zoom/state
MailItem &mail = item.value();
m_subjectLabel->setText(mail.subject().isEmpty() ? "(Sin asunto)" : mail.subject());
QString sender = mail.sender();
QString avatarChar = "?";
QRegularExpression re("^\\s*(.*?)\\s*<[^>]+>\\s*$");
QRegularExpressionMatch match = re.match(sender);
if (match.hasMatch()) {
QString name = match.captured(1).trimmed();
if (!name.isEmpty()) avatarChar = name[0].toUpper();
} else if (!sender.isEmpty() && sender.contains("@")) {
avatarChar = sender[0].toUpper();
}
m_avatarLabel->setText(avatarChar);
m_fromLabel->setText("De: " + formatAddress(sender));
m_dateLabel->setText("Fecha: " + formatDate(mail.date()));
QString cleanHtml = sanitizeHtml(mail.bodyHtml());
m_bodyViewer->setHtml(cleanHtml);
}
void ReaderView::setDarkMode(bool dark) {
m_darkMode = dark;
QString bg = dark ? "#1e1e1e" : "white";
QString text = dark ? "#e0e0e0" : "#333";
QString border = dark ? "#333" : "#e0e0e0";
QString headerBg = dark ? "#252525" : "#fafafa";
QString toolbarBg = dark ? "#2a2a2a" : "#fafafa";
QString findBg = dark ? "#3e2723" : "#fff3cd";
QString findBorder = dark ? "#ffb74d" : "#ffc107";
m_headerWidget->setStyleSheet(QString("QWidget { background: %1; border-bottom: 1px solid %2; }").arg(headerBg).arg(border));
m_toolbar->setStyleSheet(QString("QFrame { background: %1; border-bottom: 1px solid %2; }").arg(toolbarBg).arg(border));
m_findBar->setStyleSheet(QString("QFrame { background: %1; border-bottom: 1px solid %2; }").arg(findBg).arg(findBorder));
m_bodyViewer->setStyleSheet(QString("QTextBrowser { background: transparent; border: none; font-family: 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 13px; line-height: 1.5; color: %1; }").arg(text));
m_scrollArea->setStyleSheet(QString("QScrollArea { background: %1; border: none; }").arg(bg));
m_attachmentsFrame->setStyleSheet(QString("QFrame { background: %1; border-top: 1px solid %2; border-bottom: 1px solid %2; }").arg(headerBg).arg(border));
m_attachmentList->setStyleSheet(QString(
"QListWidget { background: %1; border: 1px solid %2; border-radius: 6px; padding: 4px; }"
"QListWidget::item { border: none; padding: 6px 8px; border-radius: 4px; color: %3; }"
"QListWidget::item:hover { background: %4; }"
"QListWidget::item:selected { background: %5; color: %6; }"
).arg(dark ? "#2a2a2a" : "white").arg(border).arg(text).arg(dark ? "#333" : "#f0f0f0").arg(dark ? "#1976D2" : "#e3f2fd").arg(dark ? "#90caf9" : "#1976D2"));
m_subjectLabel->setStyleSheet(QString("color: %1;").arg(text));
m_fromLabel->setStyleSheet(QString("color: %1; font-size: 13px;").arg(text));
m_toLabel->setStyleSheet(QString("color: %1; font-size: 12px;").arg(dark ? "#aaa" : "#666"));
m_dateLabel->setStyleSheet(QString("color: %1; font-size: 12px;").arg(dark ? "#888" : "#888"));
m_attachmentsHeader->setStyleSheet(QString("color: %1;").arg(dark ? "#ccc" : "#555"));
m_findCountLabel->setStyleSheet(QString("color: %1; font-size: 11px;").arg(dark ? "#ffb74d" : "#856404"));
m_zoomLabel->setStyleSheet(QString("font-size: 11px; color: %1;").arg(dark ? "#aaa" : "#555"));
// Update load images button for dark mode
if (m_loadImagesButton) {
if (dark) {
m_loadImagesButton->setStyleSheet(
"QToolButton {"
" background: #1e3a5f;"
" border: 1px solid #64b5f6;"
" border-radius: 4px;"
" padding: 4px 10px;"
" font-size: 11px;"
" color: #90caf9;"
" font-weight: 500;"
"}"
"QToolButton:hover {"
" background: #2a4a6f;"
"}"
);
} else {
m_loadImagesButton->setStyleSheet(
"QToolButton {"
" background: #e3f2fd;"
" border: 1px solid #1976D2;"
" border-radius: 4px;"
" padding: 4px 10px;"
" font-size: 11px;"
" color: #1976D2;"
" font-weight: 500;"
"}"
"QToolButton:hover {"
" background: #bbdefb;"
"}"
);
}
}
// Re-render body with new theme
refreshCurrentMail();
}
// Slots for action buttons
void ReaderView::onReplyClicked() {
if (m_currentMailId >= 0) emit replyRequested(m_currentMailId);
}
void ReaderView::onForwardClicked() {
if (m_currentMailId >= 0) emit forwardRequested(m_currentMailId);
}
void ReaderView::onDeleteClicked() {
if (m_currentMailId >= 0) emit deleteRequested(m_currentMailId);
}
void ReaderView::onDetachClicked() {
emit detachRequested();
}
+68 -6
View File
@@ -7,6 +7,11 @@
#include <QListWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QToolButton>
#include <QLineEdit>
#include <QScrollArea>
#include <QFrame>
#include <QMenu>
#include "core/mailitem.h"
class ReaderView : public QWidget {
@@ -17,24 +22,81 @@ public:
~ReaderView() override = default;
void setMailItem(const MailItem* item);
void setDarkMode(bool dark);
signals:
void replyRequested(const MailItem* item);
void forwardRequested(const MailItem* item);
void deleteRequested(const MailItem* item);
void replyRequested(int mailId);
void forwardRequested(int mailId);
void deleteRequested(int mailId);
void detachRequested();
void openAttachmentRequested(const QString &path);
private slots:
void onReplyClicked();
void onForwardClicked();
void onDeleteClicked();
void onDetachClicked();
private:
void setupUI();
void setupBodyViewer();
void setupAttachmentsArea();
void setupFindBar();
void applyEmailCss();
void updateZoom();
void refreshCurrentMail();
QString sanitizeHtml(const QString &html);
QString formatAddress(const QString &raw);
QString formatDate(const QDateTime &dt);
QIcon mimeIcon(const QString &mimeType);
void showAttachmentContextMenu(const QPoint &pos, const QString &path, const QString &name);
// Header
QWidget *m_headerWidget;
QLabel *m_avatarLabel;
QLabel *m_subjectLabel;
QLabel *m_fromLabel;
QLabel *m_dateLabel;
QLabel *m_toLabel;
// Actions
QToolButton *m_replyButton;
QToolButton *m_forwardButton;
QToolButton *m_deleteButton;
QToolButton *m_detachButton;
QToolButton *m_moreButton;
// Body
QScrollArea *m_scrollArea;
QTextBrowser *m_bodyViewer;
QWidget *m_bodyContainer;
QVBoxLayout *m_bodyLayout;
qreal m_zoomFactor = 1.0;
// Attachments
QFrame *m_attachmentsFrame;
QVBoxLayout *m_attachmentsLayout;
QLabel *m_attachmentsHeader;
QListWidget *m_attachmentList;
QPushButton *m_replyButton;
QPushButton *m_forwardButton;
QPushButton *m_deleteButton;
// Find bar
QFrame *m_findBar;
QLineEdit *m_findInput;
QToolButton *m_findPrevButton;
QToolButton *m_findNextButton;
QToolButton *m_findCloseButton;
QLabel *m_findCountLabel;
// Toolbar (zoom, etc.)
QFrame *m_toolbar;
QToolButton *m_zoomInButton;
QToolButton *m_zoomOutButton;
QToolButton *m_zoomResetButton;
QLabel *m_zoomLabel;
int m_currentMailId = -1;
bool m_darkMode = false;
QString m_baseCss;
bool m_allowExternalImages = false;
QToolButton *m_loadImagesButton = nullptr;
};
+585
View File
@@ -0,0 +1,585 @@
#include "ruleditordialog.h"
#include "db/dao/ruledao.h"
#include "db/dao/categorydao.h"
#include "db/dao/folderdao.h"
#include "services/rulesengine.h"
#include "services/accountservice.h"
#include <QHeaderView>
#include <QMessageBox>
#include <QInputDialog>
#include <QFileDialog>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QMenu>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QFormLayout>
#include <QTableWidget>
#include <QPushButton>
#include <QLineEdit>
#include <QSpinBox>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QGroupBox>
#include <QDebug>
RuleEditorDialog::RuleEditorDialog(const Rule& rule, qint64 accountId, QWidget *parent)
: QDialog(parent), m_rule(rule), m_accountId(accountId)
{
setWindowTitle(rule.isValid() ? tr("Editar regla") : tr("Nueva regla"));
resize(800, 600);
setupUI();
populateFromRule();
}
void RuleEditorDialog::setupUI()
{
QVBoxLayout* mainLayout = new QVBoxLayout(this);
// Form header
QGroupBox* headerGroup = new QGroupBox(tr("Información básica"));
QFormLayout* headerLayout = new QFormLayout(headerGroup);
m_nameEdit = new QLineEdit();
m_nameEdit->setPlaceholderText(tr("Nombre de la regla"));
headerLayout->addRow(tr("Nombre:"), m_nameEdit);
m_descEdit = new QTextEdit();
m_descEdit->setMaximumHeight(60);
m_descEdit->setPlaceholderText(tr("Descripción opcional"));
headerLayout->addRow(tr("Descripción:"), m_descEdit);
m_accountCombo = new QComboBox();
loadAccounts();
headerLayout->addRow(tr("Cuenta:"), m_accountCombo);
QHBoxLayout* optionsLayout = new QHBoxLayout();
m_enabledCheck = new QCheckBox(tr("Activada"));
m_enabledCheck->setChecked(true);
m_matchAllCheck = new QCheckBox(tr("Todas las condiciones (Y)"));
m_matchAllCheck->setChecked(true);
m_matchAllCheck->setToolTip(tr("Desactivar = Cualquier condición (O)"));
m_prioritySpin = new QSpinBox();
m_prioritySpin->setRange(1, 999);
m_prioritySpin->setValue(100);
m_prioritySpin->setToolTip(tr("Menor = mayor prioridad"));
optionsLayout->addWidget(m_enabledCheck);
optionsLayout->addWidget(m_matchAllCheck);
optionsLayout->addStretch();
optionsLayout->addWidget(new QLabel(tr("Prioridad:")));
optionsLayout->addWidget(m_prioritySpin);
headerLayout->addRow(optionsLayout);
mainLayout->addWidget(headerGroup);
// Conditions table
QGroupBox* condGroup = new QGroupBox(tr("Condiciones"));
QVBoxLayout* condLayout = new QVBoxLayout(condGroup);
m_conditionsTable = new QTableWidget();
m_conditionsTable->setColumnCount(5);
m_conditionsTable->setHorizontalHeaderLabels({tr("Campo"), tr("Operador"), tr("Valor"), tr("Mayúsculas"), ""});
m_conditionsTable->horizontalHeader()->setStretchLastSection(true);
m_conditionsTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
m_conditionsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_conditionsTable->verticalHeader()->setVisible(false);
condLayout->addWidget(m_conditionsTable);
QHBoxLayout* condBtnLayout = new QHBoxLayout();
QPushButton* btnAddCond = new QPushButton(tr("+ Añadir condición"));
btnAddCond->setStyleSheet("QPushButton { background: #1976D2; color: white; padding: 6px 12px; border-radius: 4px; }");
connect(btnAddCond, &QPushButton::clicked, this, &RuleEditorDialog::onAddCondition);
QPushButton* btnRemCond = new QPushButton(tr("- Eliminar"));
connect(btnRemCond, &QPushButton::clicked, this, &RuleEditorDialog::onRemoveCondition);
condBtnLayout->addWidget(btnAddCond);
condBtnLayout->addWidget(btnRemCond);
condBtnLayout->addStretch();
condLayout->addLayout(condBtnLayout);
mainLayout->addWidget(condGroup, 1);
// Actions table
QGroupBox* actGroup = new QGroupBox(tr("Acciones"));
QVBoxLayout* actLayout = new QVBoxLayout(actGroup);
m_actionsTable = new QTableWidget();
m_actionsTable->setColumnCount(4);
m_actionsTable->setHorizontalHeaderLabels({tr("Tipo"), tr("Parámetro"), tr("Detalles"), ""});
m_actionsTable->horizontalHeader()->setStretchLastSection(true);
m_actionsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
m_actionsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_actionsTable->verticalHeader()->setVisible(false);
actLayout->addWidget(m_actionsTable);
QHBoxLayout* actBtnLayout = new QHBoxLayout();
QPushButton* btnAddAct = new QPushButton(tr("+ Añadir acción"));
btnAddAct->setStyleSheet("QPushButton { background: #4CAF50; color: white; padding: 6px 12px; border-radius: 4px; }");
connect(btnAddAct, &QPushButton::clicked, this, &RuleEditorDialog::onAddAction);
QPushButton* btnRemAct = new QPushButton(tr("- Eliminar"));
connect(btnRemAct, &QPushButton::clicked, this, &RuleEditorDialog::onRemoveAction);
actBtnLayout->addWidget(btnAddAct);
actBtnLayout->addWidget(btnRemAct);
actBtnLayout->addStretch();
actLayout->addLayout(actBtnLayout);
mainLayout->addWidget(actGroup, 1);
// Buttons
QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &RuleEditorDialog::onAccept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttonBox);
}
void RuleEditorDialog::loadAccounts()
{
m_accountCombo->clear();
m_accountCombo->addItem(tr("Global (todas las cuentas)"), -1);
// Get accounts via AccountService (singleton doesn't have instance(), use parent's accountService)
// For now, we'll just add the global option and let the main window handle account-specific rules
if (m_accountId >= 0) {
int idx = m_accountCombo->findData(m_accountId);
if (idx >= 0) m_accountCombo->setCurrentIndex(idx);
}
}
void RuleEditorDialog::populateFromRule()
{
if (!m_rule.isValid()) return;
m_nameEdit->setText(m_rule.name);
m_descEdit->setPlainText(m_rule.description);
m_enabledCheck->setChecked(m_rule.enabled);
m_matchAllCheck->setChecked(m_rule.matchAll);
m_prioritySpin->setValue(m_rule.priority);
int idx = m_accountCombo->findData(m_rule.accountId >= 0 ? m_rule.accountId : -1);
if (idx >= 0) m_accountCombo->setCurrentIndex(idx);
for (const RuleCondition& cond : m_rule.conditions) {
int row = m_conditionsTable->rowCount();
m_conditionsTable->insertRow(row);
setupConditionRow(row, cond);
}
for (const RuleAction& act : m_rule.actions) {
int row = m_actionsTable->rowCount();
m_actionsTable->insertRow(row);
setupActionRow(row, act);
}
}
void RuleEditorDialog::setupConditionRow(int row, const RuleCondition& cond)
{
// Field combo
QComboBox* fieldCombo = new QComboBox();
fieldCombo->addItem(tr("De"), RuleCondition::From);
fieldCombo->addItem(tr("Para"), RuleCondition::To);
fieldCombo->addItem(tr("CC"), RuleCondition::Cc);
fieldCombo->addItem(tr("Asunto"), RuleCondition::Subject);
fieldCombo->addItem(tr("Cuerpo"), RuleCondition::Body);
fieldCombo->addItem(tr("Tiene adjuntos"), RuleCondition::HasAttachment);
fieldCombo->addItem(tr("Tamaño"), RuleCondition::Size);
fieldCombo->addItem(tr("Fecha"), RuleCondition::Date);
fieldCombo->addItem(tr("Marcado"), RuleCondition::Flagged);
fieldCombo->addItem(tr("Leído"), RuleCondition::Read);
fieldCombo->setCurrentIndex(fieldCombo->findData(cond.field));
connect(fieldCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this, row](int) { onConditionFieldChanged(row); });
m_conditionsTable->setCellWidget(row, 0, fieldCombo);
// Operator combo
QComboBox* opCombo = new QComboBox();
opCombo->addItem(tr("Contiene"), RuleCondition::Contains);
opCombo->addItem(tr("No contiene"), RuleCondition::NotContains);
opCombo->addItem(tr("Es igual a"), RuleCondition::Equals);
opCombo->addItem(tr("No es igual a"), RuleCondition::NotEquals);
opCombo->addItem(tr("Empieza por"), RuleCondition::StartsWith);
opCombo->addItem(tr("Termina en"), RuleCondition::EndsWith);
opCombo->addItem(tr("Expresión regular"), RuleCondition::Regex);
opCombo->addItem(tr("Mayor que"), RuleCondition::GreaterThan);
opCombo->addItem(tr("Menor que"), RuleCondition::LessThan);
opCombo->addItem(tr("Antes de"), RuleCondition::Before);
opCombo->addItem(tr("Después de"), RuleCondition::After);
opCombo->setCurrentIndex(opCombo->findData(cond.op));
m_conditionsTable->setCellWidget(row, 1, opCombo);
// Value edit
QLineEdit* valueEdit = new QLineEdit(cond.value);
m_conditionsTable->setCellWidget(row, 2, valueEdit);
// Case sensitive checkbox
QCheckBox* caseCheck = new QCheckBox();
caseCheck->setChecked(cond.caseSensitive);
caseCheck->setStyleSheet("QCheckBox { margin-left: 50%; }");
m_conditionsTable->setCellWidget(row, 3, caseCheck);
// Remove button
QPushButton* removeBtn = new QPushButton("");
removeBtn->setFixedSize(24, 24);
removeBtn->setToolTip(tr("Eliminar condición"));
removeBtn->setStyleSheet("QPushButton { background: transparent; color: #d32f2f; border: none; font-weight: bold; } QPushButton:hover { background: #fdeaea; border-radius: 3px; }");
connect(removeBtn, &QPushButton::clicked, [this, row]() {
m_conditionsTable->removeRow(row);
});
m_conditionsTable->setCellWidget(row, 4, removeBtn);
onConditionFieldChanged(row);
}
void RuleEditorDialog::onConditionFieldChanged(int row)
{
QComboBox* fieldCombo = qobject_cast<QComboBox*>(m_conditionsTable->cellWidget(row, 0));
QComboBox* opCombo = qobject_cast<QComboBox*>(m_conditionsTable->cellWidget(row, 1));
QLineEdit* valueEdit = qobject_cast<QLineEdit*>(m_conditionsTable->cellWidget(row, 2));
QCheckBox* caseCheck = qobject_cast<QCheckBox*>(m_conditionsTable->cellWidget(row, 3));
if (!fieldCombo || !opCombo) return;
RuleCondition::Field field = static_cast<RuleCondition::Field>(fieldCombo->currentData().toInt());
// Clear and repopulate operators based on field type
opCombo->clear();
bool isTextField = (field == RuleCondition::From || field == RuleCondition::To ||
field == RuleCondition::Cc || field == RuleCondition::Subject ||
field == RuleCondition::Body);
bool isBooleanField = (field == RuleCondition::HasAttachment || field == RuleCondition::Flagged || field == RuleCondition::Read);
bool isNumericField = (field == RuleCondition::Size);
bool isDateField = (field == RuleCondition::Date);
if (isTextField) {
opCombo->addItem(tr("Contiene"), RuleCondition::Contains);
opCombo->addItem(tr("No contiene"), RuleCondition::NotContains);
opCombo->addItem(tr("Es igual a"), RuleCondition::Equals);
opCombo->addItem(tr("No es igual a"), RuleCondition::NotEquals);
opCombo->addItem(tr("Empieza por"), RuleCondition::StartsWith);
opCombo->addItem(tr("Termina en"), RuleCondition::EndsWith);
opCombo->addItem(tr("Expresión regular"), RuleCondition::Regex);
valueEdit->setPlaceholderText(tr("Texto a buscar..."));
caseCheck->setVisible(true);
} else if (isBooleanField) {
opCombo->addItem(tr("Es"), RuleCondition::Equals);
opCombo->addItem(tr("No es"), RuleCondition::NotEquals);
valueEdit->setPlaceholderText("true/false");
valueEdit->setText("true");
caseCheck->setVisible(false);
} else if (isNumericField) {
opCombo->addItem(tr("Mayor que"), RuleCondition::GreaterThan);
opCombo->addItem(tr("Menor que"), RuleCondition::LessThan);
opCombo->addItem(tr("Es igual a"), RuleCondition::Equals);
valueEdit->setPlaceholderText("Tamaño en bytes");
caseCheck->setVisible(false);
} else if (isDateField) {
opCombo->addItem(tr("Antes de"), RuleCondition::Before);
opCombo->addItem(tr("Después de"), RuleCondition::After);
opCombo->addItem(tr("Es igual a"), RuleCondition::Equals);
valueEdit->setPlaceholderText("YYYY-MM-DD");
caseCheck->setVisible(false);
}
}
void RuleEditorDialog::setupActionRow(int row, const RuleAction& act)
{
// Type combo
QComboBox* typeCombo = new QComboBox();
typeCombo->addItem(tr("Mover a carpeta"), RuleAction::MoveToFolder);
typeCombo->addItem(tr("Marcar como leído/no leído"), RuleAction::MarkAsRead);
typeCombo->addItem(tr("Marcar/Desmarcar"), RuleAction::MarkAsFlagged);
typeCombo->addItem(tr("Eliminar"), RuleAction::Delete);
typeCombo->addItem(tr("Asignar categoría"), RuleAction::AssignCategory);
typeCombo->addItem(tr("Quitar categoría"), RuleAction::RemoveCategory);
typeCombo->addItem(tr("Reenviar a"), RuleAction::ForwardTo);
typeCombo->addItem(tr("Establecer prioridad"), RuleAction::SetPriority);
typeCombo->addItem(tr("Detener procesamiento"), RuleAction::StopProcessing);
typeCombo->setCurrentIndex(typeCombo->findData(act.type));
connect(typeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this, row](int) { onActionTypeChanged(row); });
m_actionsTable->setCellWidget(row, 0, typeCombo);
// Parameter edit (dynamic based on type)
QWidget* paramWidget = createParameterWidget(act);
m_actionsTable->setCellWidget(row, 1, paramWidget);
// Details label
QLabel* detailsLabel = new QLabel();
detailsLabel->setStyleSheet("color: #666; font-size: 11px;");
updateActionDetails(act, detailsLabel);
m_actionsTable->setCellWidget(row, 2, detailsLabel);
// Remove button
QPushButton* removeBtn = new QPushButton("");
removeBtn->setFixedSize(24, 24);
removeBtn->setToolTip(tr("Eliminar acción"));
removeBtn->setStyleSheet("QPushButton { background: transparent; color: #d32f2f; border: none; font-weight: bold; } QPushButton:hover { background: #fdeaea; border-radius: 3px; }");
connect(removeBtn, &QPushButton::clicked, [this, row]() {
m_actionsTable->removeRow(row);
});
m_actionsTable->setCellWidget(row, 3, removeBtn);
}
QWidget* RuleEditorDialog::createParameterWidget(const RuleAction& act)
{
QWidget* container = new QWidget();
QHBoxLayout* layout = new QHBoxLayout(container);
layout->setContentsMargins(0, 0, 0, 0);
switch (act.type) {
case RuleAction::MoveToFolder: {
QComboBox* folderCombo = new QComboBox();
folderCombo->addItem(tr("Seleccionar carpeta..."), -1);
// Load folders from database
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.exec("SELECT id, name FROM Folder ORDER BY name");
while (query.next()) {
folderCombo->addItem(query.value(1).toString(), query.value(0).toLongLong());
}
if (act.value.toLongLong() > 0) {
int idx = folderCombo->findData(act.value.toLongLong());
if (idx >= 0) folderCombo->setCurrentIndex(idx);
}
layout->addWidget(folderCombo, 1);
break;
}
case RuleAction::MarkAsRead: {
QComboBox* combo = new QComboBox();
combo->addItem(tr("Leído"), "true");
combo->addItem(tr("No leído"), "false");
combo->setCurrentIndex(combo->findData(act.value));
layout->addWidget(combo, 1);
break;
}
case RuleAction::MarkAsFlagged: {
QComboBox* combo = new QComboBox();
combo->addItem(tr("Marcado"), "true");
combo->addItem(tr("No marcado"), "false");
combo->setCurrentIndex(combo->findData(act.value));
layout->addWidget(combo, 1);
break;
}
case RuleAction::AssignCategory:
case RuleAction::RemoveCategory: {
QComboBox* catCombo = new QComboBox();
catCombo->addItem(tr("Seleccionar categoría..."), -1);
QVector<Category> cats = CategoryDao::findAll();
for (const Category& cat : cats) {
catCombo->addItem(cat.name, cat.id);
}
if (act.value.toLongLong() > 0) {
int idx = catCombo->findData(act.value.toLongLong());
if (idx >= 0) catCombo->setCurrentIndex(idx);
}
layout->addWidget(catCombo, 1);
break;
}
case RuleAction::ForwardTo: {
QLineEdit* edit = new QLineEdit(act.value);
edit->setPlaceholderText("email@dominio.com");
layout->addWidget(edit, 1);
break;
}
case RuleAction::SetPriority: {
QComboBox* combo = new QComboBox();
combo->addItem(tr("Alta"), "high");
combo->addItem(tr("Normal"), "normal");
combo->addItem(tr("Baja"), "low");
combo->setCurrentIndex(combo->findData(act.value));
layout->addWidget(combo, 1);
break;
}
case RuleAction::Delete:
case RuleAction::StopProcessing:
default: {
QLabel* label = new QLabel(tr("(sin parámetros)"));
label->setStyleSheet("color: #999; font-style: italic;");
layout->addWidget(label, 1);
break;
}
}
return container;
}
void RuleEditorDialog::updateActionDetails(const RuleAction& act, QLabel* label)
{
QString details;
switch (act.type) {
case RuleAction::MoveToFolder:
details = tr("Mueve el correo a la carpeta seleccionada");
break;
case RuleAction::MarkAsRead:
details = tr("Cambia el estado de lectura del correo");
break;
case RuleAction::MarkAsFlagged:
details = tr("Cambia el estado de marcado/estrella");
break;
case RuleAction::Delete:
details = tr("Elimina el correo permanentemente");
break;
case RuleAction::AssignCategory:
details = tr("Añade la categoría al correo");
break;
case RuleAction::RemoveCategory:
details = tr("Quita la categoría del correo");
break;
case RuleAction::ForwardTo:
details = tr("Reenvía el correo a la dirección indicada");
break;
case RuleAction::SetPriority:
details = tr("Marca el correo como alta prioridad (flagged)");
break;
case RuleAction::StopProcessing:
details = tr("Detiene la evaluación de más reglas para este correo");
break;
}
label->setText(details);
}
void RuleEditorDialog::onActionTypeChanged(int row)
{
QComboBox* typeCombo = qobject_cast<QComboBox*>(m_actionsTable->cellWidget(row, 0));
if (!typeCombo) return;
RuleAction::Type type = static_cast<RuleAction::Type>(typeCombo->currentData().toInt());
// Replace parameter widget
QWidget* oldWidget = m_actionsTable->cellWidget(row, 1);
if (oldWidget) oldWidget->deleteLater();
RuleAction act;
act.type = type;
QWidget* newWidget = createParameterWidget(act);
m_actionsTable->setCellWidget(row, 1, newWidget);
// Update details
QLabel* detailsLabel = qobject_cast<QLabel*>(m_actionsTable->cellWidget(row, 2));
if (detailsLabel) updateActionDetails(act, detailsLabel);
}
void RuleEditorDialog::onAddCondition()
{
int row = m_conditionsTable->rowCount();
m_conditionsTable->insertRow(row);
setupConditionRow(row);
}
void RuleEditorDialog::onRemoveCondition()
{
int row = m_conditionsTable->currentRow();
if (row >= 0) {
m_conditionsTable->removeRow(row);
}
}
void RuleEditorDialog::onAddAction()
{
int row = m_actionsTable->rowCount();
m_actionsTable->insertRow(row);
setupActionRow(row);
}
void RuleEditorDialog::onRemoveAction()
{
int row = m_actionsTable->currentRow();
if (row >= 0) {
m_actionsTable->removeRow(row);
}
}
RuleCondition RuleEditorDialog::conditionFromRow(int row) const
{
RuleCondition cond;
QComboBox* fieldCombo = qobject_cast<QComboBox*>(m_conditionsTable->cellWidget(row, 0));
QComboBox* opCombo = qobject_cast<QComboBox*>(m_conditionsTable->cellWidget(row, 1));
QLineEdit* valueEdit = qobject_cast<QLineEdit*>(m_conditionsTable->cellWidget(row, 2));
QCheckBox* caseCheck = qobject_cast<QCheckBox*>(m_conditionsTable->cellWidget(row, 3));
if (fieldCombo) cond.field = static_cast<RuleCondition::Field>(fieldCombo->currentData().toInt());
if (opCombo) cond.op = static_cast<RuleCondition::Operator>(opCombo->currentData().toInt());
if (valueEdit) cond.value = valueEdit->text();
if (caseCheck) cond.caseSensitive = caseCheck->isChecked();
return cond;
}
RuleAction RuleEditorDialog::actionFromRow(int row) const
{
RuleAction act;
QComboBox* typeCombo = qobject_cast<QComboBox*>(m_actionsTable->cellWidget(row, 0));
QWidget* paramWidget = m_actionsTable->cellWidget(row, 1);
if (typeCombo) act.type = static_cast<RuleAction::Type>(typeCombo->currentData().toInt());
if (!paramWidget) return act;
// Extract value based on action type
if (act.type == RuleAction::MoveToFolder ||
act.type == RuleAction::AssignCategory ||
act.type == RuleAction::RemoveCategory) {
QComboBox* combo = paramWidget->findChild<QComboBox*>();
if (combo && combo->currentData().toLongLong() > 0) {
act.value = QString::number(combo->currentData().toLongLong());
}
} else if (act.type == RuleAction::MarkAsRead ||
act.type == RuleAction::MarkAsFlagged ||
act.type == RuleAction::SetPriority) {
QComboBox* combo = paramWidget->findChild<QComboBox*>();
if (combo) act.value = combo->currentData().toString();
} else if (act.type == RuleAction::ForwardTo) {
QLineEdit* edit = paramWidget->findChild<QLineEdit*>();
if (edit) act.value = edit->text();
}
return act;
}
void RuleEditorDialog::onAccept()
{
if (m_nameEdit->text().trimmed().isEmpty()) {
QMessageBox::warning(this, tr("Error"), tr("El nombre de la regla es obligatorio."));
m_nameEdit->setFocus();
return;
}
m_rule.name = m_nameEdit->text().trimmed();
m_rule.description = m_descEdit->toPlainText().trimmed();
m_rule.enabled = m_enabledCheck->isChecked();
m_rule.matchAll = m_matchAllCheck->isChecked();
m_rule.priority = m_prioritySpin->value();
m_rule.accountId = m_accountCombo->currentData().toLongLong();
if (m_rule.accountId < 0) m_rule.accountId = -1;
m_rule.conditions.clear();
for (int i = 0; i < m_conditionsTable->rowCount(); ++i) {
m_rule.conditions.append(conditionFromRow(i));
}
m_rule.actions.clear();
for (int i = 0; i < m_actionsTable->rowCount(); ++i) {
m_rule.actions.append(actionFromRow(i));
}
m_rule.updatedAt = QDateTime::currentDateTime();
if (m_rule.isValid()) {
if (RuleDao::update(m_rule)) {
emit ruleSaved(m_rule);
accept();
} else {
QMessageBox::critical(this, tr("Error"), tr("No se pudo actualizar la regla."));
}
} else {
if (RuleDao::insert(m_rule)) {
emit ruleSaved(m_rule);
accept();
} else {
QMessageBox::critical(this, tr("Error"), tr("No se pudo crear la regla."));
}
}
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <QDialog>
#include "db/dao/ruledao.h"
#include <QTableWidget>
#include <QLineEdit>
#include <QTextEdit>
#include <QSpinBox>
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
class RuleEditorDialog : public QDialog
{
Q_OBJECT
public:
explicit RuleEditorDialog(const Rule& rule = Rule(), qint64 accountId = -1, QWidget *parent = nullptr);
Rule getRule() const { return m_rule; }
signals:
void ruleSaved(const Rule& rule);
private slots:
void onAddCondition();
void onRemoveCondition();
void onAddAction();
void onRemoveAction();
void onConditionFieldChanged(int row);
void onActionTypeChanged(int row);
void onAccept();
void loadAccounts();
private:
Rule m_rule;
qint64 m_accountId;
QTableWidget* m_conditionsTable;
QTableWidget* m_actionsTable;
QLineEdit* m_nameEdit;
QTextEdit* m_descEdit;
QSpinBox* m_prioritySpin;
QCheckBox* m_enabledCheck;
QCheckBox* m_matchAllCheck;
QComboBox* m_accountCombo;
void setupUI();
void populateFromRule();
void setupConditionRow(int row, const RuleCondition& cond = RuleCondition());
void setupActionRow(int row, const RuleAction& act = RuleAction());
RuleCondition conditionFromRow(int row) const;
RuleAction actionFromRow(int row) const;
QWidget* createParameterWidget(const RuleAction& act);
void updateActionDetails(const RuleAction& act, QLabel* label);
};
+298
View File
@@ -0,0 +1,298 @@
#include "rulesmanagerdialog.h"
#include "ruleditordialog.h"
#include "db/dao/ruledao.h"
#include "db/dao/categorydao.h"
#include "db/dao/folderdao.h"
#include "services/rulesengine.h"
#include "services/accountservice.h"
#include <QHeaderView>
#include <QMessageBox>
#include <QInputDialog>
#include <QFileDialog>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QMenu>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QDebug>
RulesManagerDialog::RulesManagerDialog(qint64 accountId, QWidget *parent)
: QDialog(parent), m_accountId(accountId)
{
setWindowTitle(m_accountId >= 0 ? tr("Reglas de la cuenta") : tr("Reglas globales"));
resize(900, 600);
setupUI();
loadRules();
}
void RulesManagerDialog::setupUI()
{
QVBoxLayout* mainLayout = new QVBoxLayout(this);
// Toolbar
QHBoxLayout* toolLayout = new QHBoxLayout();
m_btnNew = new QPushButton(tr("Nueva regla"));
m_btnRun = new QPushButton(tr("Ejecutar ahora"));
m_btnImport = new QPushButton(tr("Importar"));
m_btnExport = new QPushButton(tr("Exportar"));
m_btnClose = new QPushButton(tr("Cerrar"));
m_btnNew->setStyleSheet("QPushButton { background: #1976D2; color: white; font-weight: bold; padding: 6px 12px; border-radius: 4px; } QPushButton:hover { background: #1565C0; }");
m_btnRun->setStyleSheet("QPushButton { background: #4CAF50; color: white; padding: 6px 12px; border-radius: 4px; } QPushButton:hover { background: #43A047; }");
toolLayout->addWidget(m_btnNew);
toolLayout->addWidget(m_btnRun);
toolLayout->addStretch();
toolLayout->addWidget(m_btnImport);
toolLayout->addWidget(m_btnExport);
toolLayout->addWidget(m_btnClose);
mainLayout->addLayout(toolLayout);
// Tree widget
m_tree = new QTreeWidget();
m_tree->setHeaderLabels({tr("Activa"), tr("Nombre"), tr("Prioridad"), tr("Condiciones"), tr("Acciones"), tr("Cuenta"), tr("Última ejecución"), tr("Veces")});
m_tree->setAlternatingRowColors(true);
m_tree->setSelectionBehavior(QAbstractItemView::SelectRows);
m_tree->setSelectionMode(QAbstractItemView::SingleSelection);
m_tree->setContextMenuPolicy(Qt::CustomContextMenu);
m_tree->header()->setStretchLastSection(false);
m_tree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
m_tree->setColumnWidth(0, 60);
m_tree->setColumnWidth(2, 80);
m_tree->setColumnWidth(3, 200);
m_tree->setColumnWidth(4, 200);
m_tree->setColumnWidth(5, 100);
m_tree->setColumnWidth(6, 140);
m_tree->setColumnWidth(7, 60);
connect(m_tree, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
QTreeWidgetItem* item = m_tree->itemAt(pos);
if (!item) return;
QMenu menu(this);
QAction* editAct = menu.addAction(tr("Editar"), [this, item]() { onEditRule(item); });
QAction* delAct = menu.addAction(tr("Eliminar"), [this, item]() { onDeleteRule(item); });
menu.addSeparator();
QAction* toggleAct = menu.addAction(item->checkState(0) == Qt::Checked ? tr("Desactivar") : tr("Activar"), [this, item]() { onToggleEnabled(item); });
menu.addSeparator();
QAction* moveUpAct = menu.addAction(tr("Subir prioridad"), [this, item]() {
int idx = m_tree->indexOfTopLevelItem(item);
if (idx > 0) {
QTreeWidgetItem* prev = m_tree->takeTopLevelItem(idx);
m_tree->insertTopLevelItem(idx - 1, prev);
// TODO: update priority in DB
}
});
QAction* moveDownAct = menu.addAction(tr("Bajar prioridad"), [this, item]() {
int idx = m_tree->indexOfTopLevelItem(item);
if (idx < m_tree->topLevelItemCount() - 1) {
QTreeWidgetItem* next = m_tree->takeTopLevelItem(idx + 1);
m_tree->insertTopLevelItem(idx + 1, item);
// TODO: update priority in DB
}
});
menu.exec(m_tree->viewport()->mapToGlobal(pos));
});
connect(m_tree, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* item) { onEditRule(item); });
mainLayout->addWidget(m_tree, 1);
// Status
QLabel* statusLabel = new QLabel(tr("Doble click para editar. Click derecho para más opciones."));
statusLabel->setStyleSheet("color: #666; font-size: 11px; padding: 4px;");
mainLayout->addWidget(statusLabel);
// Connect buttons
connect(m_btnNew, &QPushButton::clicked, this, &RulesManagerDialog::onNewRule);
connect(m_btnRun, &QPushButton::clicked, this, &RulesManagerDialog::onRunRules);
connect(m_btnImport, &QPushButton::clicked, this, [this]() { onImportExport(true); });
connect(m_btnExport, &QPushButton::clicked, this, [this]() { onImportExport(false); });
connect(m_btnClose, &QPushButton::clicked, this, &QDialog::accept);
}
void RulesManagerDialog::loadRules()
{
m_tree->clear();
QVector<Rule> rules = RuleDao::findByAccount(m_accountId);
for (const Rule& rule : rules) {
addRuleToTree(rule);
}
}
void RulesManagerDialog::addRuleToTree(const Rule& rule)
{
QTreeWidgetItem* item = new QTreeWidgetItem(m_tree);
item->setData(0, Qt::UserRole, rule.id);
item->setCheckState(0, rule.enabled ? Qt::Checked : Qt::Unchecked);
item->setText(1, rule.name);
item->setText(2, QString::number(rule.priority));
// Conditions summary
QStringList condTexts;
for (const RuleCondition& cond : rule.conditions) {
QString fieldName;
switch (cond.field) {
case RuleCondition::From: fieldName = "De"; break;
case RuleCondition::To: fieldName = "Para"; break;
case RuleCondition::Cc: fieldName = "CC"; break;
case RuleCondition::Subject: fieldName = "Asunto"; break;
case RuleCondition::Body: fieldName = "Cuerpo"; break;
case RuleCondition::HasAttachment: fieldName = "Tiene adjuntos"; break;
case RuleCondition::Size: fieldName = "Tamaño"; break;
case RuleCondition::Date: fieldName = "Fecha"; break;
case RuleCondition::Flagged: fieldName = "Marcado"; break;
case RuleCondition::Read: fieldName = "Leído"; break;
}
QString opName;
switch (cond.op) {
case RuleCondition::Contains: opName = "contiene"; break;
case RuleCondition::NotContains: opName = "no contiene"; break;
case RuleCondition::Equals: opName = "es"; break;
case RuleCondition::NotEquals: opName = "no es"; break;
case RuleCondition::StartsWith: opName = "empieza por"; break;
case RuleCondition::EndsWith: opName = "termina en"; break;
case RuleCondition::Regex: opName = "regex"; break;
case RuleCondition::GreaterThan: opName = ">"; break;
case RuleCondition::LessThan: opName = "<"; break;
case RuleCondition::Before: opName = "antes de"; break;
case RuleCondition::After: opName = "después de"; break;
}
condTexts.append(QString("%1 %2 \"%3\"").arg(fieldName, opName, cond.value));
}
item->setText(3, condTexts.join(rule.matchAll ? " Y " : " O "));
// Actions summary
QStringList actTexts;
for (const RuleAction& act : rule.actions) {
QString actName;
switch (act.type) {
case RuleAction::MoveToFolder: actName = QString("Mover a carpeta %1").arg(act.value); break;
case RuleAction::MarkAsRead: actName = QString("Marcar como %1").arg(act.value == "false" ? "no leído" : "leído"); break;
case RuleAction::MarkAsFlagged: actName = QString("Marcar como %1").arg(act.value == "false" ? "no marcado" : "marcado"); break;
case RuleAction::Delete: actName = "Eliminar"; break;
case RuleAction::AssignCategory: actName = QString("Categoría %1").arg(act.value); break;
case RuleAction::RemoveCategory: actName = QString("Quitar categoría %1").arg(act.value); break;
case RuleAction::ForwardTo: actName = QString("Reenviar a %1").arg(act.value); break;
case RuleAction::SetPriority: actName = QString("Prioridad %1").arg(act.value); break;
case RuleAction::StopProcessing: actName = "Detener procesamiento"; break;
}
actTexts.append(actName);
}
item->setText(4, actTexts.join(", "));
item->setText(5, rule.isGlobal() ? tr("Global") : QString::number(rule.accountId));
item->setText(6, rule.lastRun.isValid() ? rule.lastRun.toString("dd/MM/yyyy hh:mm") : tr("Nunca"));
item->setText(7, QString::number(rule.runCount));
if (!rule.enabled) {
for (int i = 0; i < m_tree->columnCount(); ++i) {
item->setForeground(i, QBrush(QColor("#999")));
}
}
}
Rule RulesManagerDialog::itemToRule(QTreeWidgetItem* item) const
{
qint64 id = item->data(0, Qt::UserRole).toLongLong();
auto ruleOpt = RuleDao::findById(id);
return ruleOpt.value_or(Rule());
}
void RulesManagerDialog::onNewRule()
{
RuleEditorDialog dlg(Rule(), m_accountId, this);
connect(&dlg, &RuleEditorDialog::ruleSaved, this, &RulesManagerDialog::onRuleSaved);
dlg.exec();
}
void RulesManagerDialog::onEditRule(QTreeWidgetItem* item)
{
Rule rule = itemToRule(item);
if (!rule.isValid()) return;
RuleEditorDialog dlg(rule, m_accountId, this);
connect(&dlg, &RuleEditorDialog::ruleSaved, this, &RulesManagerDialog::onRuleSaved);
dlg.exec();
}
void RulesManagerDialog::onRuleSaved(const Rule& rule)
{
refresh();
emit rulesChanged();
}
void RulesManagerDialog::onDeleteRule(QTreeWidgetItem* item)
{
Rule rule = itemToRule(item);
if (!rule.isValid()) return;
if (QMessageBox::question(this, tr("Eliminar regla"),
tr("¿Eliminar la regla \"%1\"?").arg(rule.name),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
if (RuleDao::remove(rule.id)) {
refresh();
emit rulesChanged();
}
}
}
void RulesManagerDialog::onToggleEnabled(QTreeWidgetItem* item)
{
Rule rule = itemToRule(item);
if (!rule.isValid()) return;
bool newState = item->checkState(0) != Qt::Checked;
if (RuleDao::setEnabled(rule.id, newState)) {
item->setCheckState(0, newState ? Qt::Checked : Qt::Unchecked);
if (!newState) {
for (int i = 0; i < m_tree->columnCount(); ++i) {
item->setForeground(i, QBrush(QColor("#999")));
}
} else {
for (int i = 0; i < m_tree->columnCount(); ++i) {
item->setForeground(i, QBrush(QColor("#000")));
}
}
emit rulesChanged();
}
}
void RulesManagerDialog::onRunRules()
{
// TODO: Get selected folder/account from main window
QMessageBox::information(this, tr("Ejecutar reglas"), tr("Seleccione una carpeta en la ventana principal y pulse 'Ejecutar reglas' en el menú de herramientas."));
}
void RulesManagerDialog::onImportExport(bool import)
{
if (import) {
QString file = QFileDialog::getOpenFileName(this, tr("Importar reglas"), QString(), tr("JSON (*.json)"));
if (file.isEmpty()) return;
// TODO: Parse JSON and create rules
QMessageBox::information(this, tr("Importar"), tr("Función de importación en desarrollo."));
} else {
QString file = QFileDialog::getSaveFileName(this, tr("Exportar reglas"), "rules.json", tr("JSON (*.json)"));
if (file.isEmpty()) return;
QVector<Rule> rules = RuleDao::findByAccount(m_accountId);
QJsonArray arr;
for (const Rule& rule : rules) arr.append(rule.toJson());
QJsonDocument doc(arr);
QFile f(file);
if (f.open(QIODevice::WriteOnly)) {
f.write(doc.toJson(QJsonDocument::Indented));
QMessageBox::information(this, tr("Exportar"), tr("Reglas exportadas a %1").arg(file));
}
}
}
void RulesManagerDialog::refresh()
{
loadRules();
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include <QDialog>
#include <QTreeWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include "db/dao/ruledao.h"
#include "ruleditordialog.h"
class RulesManagerDialog : public QDialog
{
Q_OBJECT
public:
explicit RulesManagerDialog(qint64 accountId = -1, QWidget *parent = nullptr);
signals:
void rulesChanged();
private slots:
void onNewRule();
void onEditRule(QTreeWidgetItem* item);
void onDeleteRule(QTreeWidgetItem* item);
void onToggleEnabled(QTreeWidgetItem* item);
void onRunRules();
void onImportExport(bool import);
void onRuleSaved(const Rule& rule);
void refresh();
private:
qint64 m_accountId;
QTreeWidget* m_tree;
QPushButton* m_btnNew;
QPushButton* m_btnRun;
QPushButton* m_btnImport;
QPushButton* m_btnExport;
QPushButton* m_btnClose;
void setupUI();
void loadRules();
void addRuleToTree(const Rule& rule);
Rule itemToRule(QTreeWidgetItem* item) const;
};
+277
View File
@@ -0,0 +1,277 @@
#include "signaturemanagerdialog.h"
#include "db/dao/accountdao.h"
#include "services/accountservice.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QLineEdit>
#include <QTextEdit>
#include <QListWidget>
#include <QListWidgetItem>
#include <QDialogButtonBox>
#include <QMessageBox>
#include <QInputDialog>
#include <QDebug>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QMenu>
SignatureEditorDialog::SignatureEditorDialog(const QString& signatureHtml, QWidget *parent)
: QDialog(parent), m_signatureHtml(signatureHtml)
{
setWindowTitle(tr("Editor de firma"));
resize(600, 400);
setupUI();
}
void SignatureEditorDialog::setupUI()
{
QVBoxLayout* mainLayout = new QVBoxLayout(this);
// Name
QHBoxLayout* nameLayout = new QHBoxLayout();
nameLayout->addWidget(new QLabel(tr("Nombre:")));
m_nameEdit = new QLineEdit();
m_nameEdit->setPlaceholderText(tr("Ej: Firma personal, Firma trabajo, etc."));
nameLayout->addWidget(m_nameEdit);
mainLayout->addLayout(nameLayout);
// Editor
m_editor = new QTextEdit();
m_editor->setAcceptRichText(true);
m_editor->setHtml(m_signatureHtml);
m_editor->setPlaceholderText(tr("Escribe tu firma aquí (HTML soportado)..."));
mainLayout->addWidget(m_editor, 1);
// Buttons
QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &SignatureEditorDialog::onAccept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttonBox);
}
void SignatureEditorDialog::onAccept()
{
if (m_nameEdit->text().trimmed().isEmpty()) {
QMessageBox::warning(this, tr("Error"), tr("El nombre de la firma es obligatorio."));
m_nameEdit->setFocus();
return;
}
m_signatureHtml = m_editor->toHtml();
emit signatureSaved(m_nameEdit->text().trimmed(), m_signatureHtml);
accept();
}
SignatureManagerDialog::SignatureManagerDialog(AccountService* accountService, qint64 accountId, QWidget *parent)
: QDialog(parent), m_accountService(accountService), m_accountId(accountId)
{
setWindowTitle(tr("Gestor de firmas"));
resize(500, 400);
setupUI();
loadSignatures();
}
void SignatureManagerDialog::setupUI()
{
QVBoxLayout* mainLayout = new QVBoxLayout(this);
// Header
QLabel* titleLabel = new QLabel(tr("Firmas disponibles"));
titleLabel->setStyleSheet("font-weight: bold; font-size: 14px; padding: 8px;");
mainLayout->addWidget(titleLabel);
// List
m_list = new QListWidget();
m_list->setSelectionMode(QAbstractItemView::SingleSelection);
m_list->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_list, &QListWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
QListWidgetItem* item = m_list->itemAt(pos);
if (!item) return;
QMenu menu(this);
QAction* editAct = menu.addAction(tr("Editar"), [this, item]() { onEditSignature(item); });
QAction* delAct = menu.addAction(tr("Eliminar"), [this, item]() { onDeleteSignature(item); });
menu.exec(m_list->viewport()->mapToGlobal(pos));
});
connect(m_list, &QListWidget::itemClicked, this, &SignatureManagerDialog::onSignatureSelected);
connect(m_list, &QListWidget::itemDoubleClicked, this, &SignatureManagerDialog::onUseSignature);
mainLayout->addWidget(m_list, 1);
// Toolbar
QHBoxLayout* toolLayout = new QHBoxLayout();
m_btnNew = new QPushButton(tr("Nueva firma"));
m_btnEdit = new QPushButton(tr("Editar"));
m_btnDelete = new QPushButton(tr("Eliminar"));
m_btnUse = new QPushButton(tr("Usar"));
m_btnClose = new QPushButton(tr("Cerrar"));
m_btnNew->setStyleSheet("QPushButton { background: #1976D2; color: white; font-weight: bold; padding: 6px 12px; border-radius: 4px; }");
m_btnUse->setStyleSheet("QPushButton { background: #4CAF50; color: white; padding: 6px 12px; border-radius: 4px; }");
m_btnDelete->setStyleSheet("QPushButton { color: #d32f2f; }");
toolLayout->addWidget(m_btnNew);
toolLayout->addWidget(m_btnEdit);
toolLayout->addWidget(m_btnDelete);
toolLayout->addStretch();
toolLayout->addWidget(m_btnUse);
toolLayout->addWidget(m_btnClose);
mainLayout->addLayout(toolLayout);
// Status
QLabel* statusLabel = new QLabel(tr("Selecciona una firma y pulsa 'Usar' para insertarla en el correo."));
statusLabel->setStyleSheet("color: #666; font-size: 11px; padding: 4px;");
mainLayout->addWidget(statusLabel);
// Connect
connect(m_btnNew, &QPushButton::clicked, this, &SignatureManagerDialog::onNewSignature);
connect(m_btnEdit, &QPushButton::clicked, this, [this]() { onEditSignature(m_list->currentItem()); });
connect(m_btnDelete, &QPushButton::clicked, this, [this]() { onDeleteSignature(m_list->currentItem()); });
connect(m_btnUse, &QPushButton::clicked, this, [this]() { onUseSignature(m_list->currentItem()); });
connect(m_btnClose, &QPushButton::clicked, this, &QDialog::accept);
// Disable buttons when no selection
connect(m_list, &QListWidget::itemSelectionChanged, this, [this]() {
bool hasSel = m_list->currentItem() != nullptr;
m_btnEdit->setEnabled(hasSel);
m_btnDelete->setEnabled(hasSel);
m_btnUse->setEnabled(hasSel);
});
}
void SignatureManagerDialog::loadSignatures()
{
m_list->clear();
// For now, we'll use a simple SQLite table for signatures
// In a more complete implementation, this could be per-account
QSqlDatabase db = DatabaseManager::instance().database();
// Create table if not exists
QSqlQuery createQuery(db);
createQuery.exec(
"CREATE TABLE IF NOT EXISTS Signature ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"accountId INTEGER, " // -1 = global
"name TEXT NOT NULL, "
"html TEXT NOT NULL, "
"createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, "
"updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, "
"FOREIGN KEY(accountId) REFERENCES Account(id) ON DELETE CASCADE"
")"
);
QSqlQuery query(db);
if (m_accountId >= 0) {
query.prepare("SELECT id, name, html FROM Signature WHERE accountId = :accountId OR accountId IS NULL ORDER BY name");
query.bindValue(":accountId", m_accountId);
} else {
query.prepare("SELECT id, name, html FROM Signature WHERE accountId IS NULL ORDER BY name");
}
if (query.exec()) {
while (query.next()) {
QListWidgetItem* item = new QListWidgetItem(query.value("name").toString());
item->setData(Qt::UserRole, query.value("id").toLongLong());
item->setData(Qt::UserRole + 1, query.value("html").toString());
item->setToolTip(query.value("html").toString().left(200) + "...");
m_list->addItem(item);
}
}
}
void SignatureManagerDialog::onNewSignature()
{
SignatureEditorDialog dlg(QString(), this);
connect(&dlg, &SignatureEditorDialog::signatureSaved, this, [this](const QString& name, const QString& html) {
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("INSERT INTO Signature (accountId, name, html, createdAt, updatedAt) VALUES (:accountId, :name, :html, :createdAt, :updatedAt)");
query.bindValue(":accountId", m_accountId >= 0 ? m_accountId : QVariant());
query.bindValue(":name", name);
query.bindValue(":html", html);
query.bindValue(":createdAt", QDateTime::currentDateTime());
query.bindValue(":updatedAt", QDateTime::currentDateTime());
if (query.exec()) {
loadSignatures();
emit signaturesChanged();
} else {
QMessageBox::warning(this, tr("Error"), tr("No se pudo guardar la firma: %1").arg(query.lastError().text()));
}
});
dlg.exec();
}
void SignatureManagerDialog::onEditSignature(QListWidgetItem* item)
{
if (!item) return;
qint64 id = item->data(Qt::UserRole).toLongLong();
QString html = item->data(Qt::UserRole + 1).toString();
QString name = item->text();
SignatureEditorDialog dlg(html, this);
dlg.setSignatureName(name);
connect(&dlg, &SignatureEditorDialog::signatureSaved, this, [this, id](const QString& name, const QString& html) {
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("UPDATE Signature SET name = :name, html = :html, updatedAt = :updatedAt WHERE id = :id");
query.bindValue(":id", id);
query.bindValue(":name", name);
query.bindValue(":html", html);
query.bindValue(":updatedAt", QDateTime::currentDateTime());
if (query.exec()) {
loadSignatures();
emit signaturesChanged();
} else {
QMessageBox::warning(this, tr("Error"), tr("No se pudo actualizar la firma: %1").arg(query.lastError().text()));
}
});
dlg.exec();
}
void SignatureManagerDialog::onDeleteSignature(QListWidgetItem* item)
{
if (!item) return;
qint64 id = item->data(Qt::UserRole).toLongLong();
QString name = item->text();
if (QMessageBox::question(this, tr("Eliminar firma"),
tr("¿Eliminar la firma \"%1\"?").arg(name),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Signature WHERE id = :id");
query.bindValue(":id", id);
if (query.exec()) {
loadSignatures();
emit signaturesChanged();
} else {
QMessageBox::warning(this, tr("Error"), tr("No se pudo eliminar la firma: %1").arg(query.lastError().text()));
}
}
}
void SignatureManagerDialog::onUseSignature(QListWidgetItem* item)
{
if (!item) return;
m_selectedSignatureHtml = item->data(Qt::UserRole + 1).toString();
emit signatureSelected(m_selectedSignatureHtml);
accept();
}
void SignatureManagerDialog::onSignatureSelected(QListWidgetItem* item)
{
if (item) {
m_selectedSignatureHtml = item->data(Qt::UserRole + 1).toString();
}
}
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include <QDialog>
#include <QListWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTextEdit>
#include <QLabel>
#include <QLineEdit>
#include <QDialogButtonBox>
#include <QMessageBox>
#include "services/accountservice.h"
class SignatureEditorDialog : public QDialog
{
Q_OBJECT
public:
explicit SignatureEditorDialog(const QString& signatureHtml = QString(), QWidget *parent = nullptr);
QString getSignatureHtml() const { return m_signatureHtml; }
QString getSignatureName() const { return m_nameEdit->text().trimmed(); }
void setSignatureName(const QString& name) { m_nameEdit->setText(name); }
signals:
void signatureSaved(const QString& name, const QString& html);
private slots:
void onAccept();
private:
QString m_signatureHtml;
QLineEdit* m_nameEdit;
QTextEdit* m_editor;
void setupUI();
};
class SignatureManagerDialog : public QDialog
{
Q_OBJECT
public:
explicit SignatureManagerDialog(AccountService* accountService, qint64 accountId = -1, QWidget *parent = nullptr);
QString getSelectedSignature() const { return m_selectedSignatureHtml; }
signals:
void signatureSelected(const QString& html);
void signaturesChanged();
private slots:
void onNewSignature();
void onEditSignature(QListWidgetItem* item);
void onDeleteSignature(QListWidgetItem* item);
void onUseSignature(QListWidgetItem* item);
void onSignatureSelected(QListWidgetItem* item);
private:
AccountService* m_accountService;
qint64 m_accountId;
QString m_selectedSignatureHtml;
QListWidget* m_list;
QPushButton* m_btnNew;
QPushButton* m_btnEdit;
QPushButton* m_btnDelete;
QPushButton* m_btnUse;
QPushButton* m_btnClose;
void setupUI();
void loadSignatures();
struct SignatureData { QString name; QString html; };
};
+321
View File
@@ -0,0 +1,321 @@
#include "templateeditordialog.h"
#include "db/dao/templatedao.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QTableWidget>
#include <QPushButton>
#include <QLabel>
#include <QComboBox>
#include <QLineEdit>
#include <QTextEdit>
#include <QSpinBox>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QMessageBox>
#include <QHeaderView>
#include <QGroupBox>
#include <QDebug>
#include <QFileDialog>
#include <QTextBrowser>
TemplateEditorDialog::TemplateEditorDialog(const Template& tmpl, qint64 accountId, QWidget *parent)
: QDialog(parent), m_template(tmpl), m_accountId(accountId)
{
setWindowTitle(tmpl.isValid() ? tr("Editar plantilla") : tr("Nueva plantilla"));
resize(900, 700);
setupUI();
populateFromTemplate();
}
void TemplateEditorDialog::setupUI()
{
QVBoxLayout* mainLayout = new QVBoxLayout(this);
// Header form
QGroupBox* headerGroup = new QGroupBox(tr("Información básica"));
QFormLayout* headerLayout = new QFormLayout(headerGroup);
m_nameEdit = new QLineEdit();
m_nameEdit->setPlaceholderText(tr("Nombre de la plantilla"));
headerLayout->addRow(tr("Nombre:"), m_nameEdit);
m_subjectEdit = new QLineEdit();
m_subjectEdit->setPlaceholderText(tr("Asunto (puede usar variables {{nombre}})"));
headerLayout->addRow(tr("Asunto:"), m_subjectEdit);
m_isDefaultCheck = new QCheckBox(tr("Plantilla por defecto para esta cuenta"));
headerLayout->addRow(m_isDefaultCheck);
mainLayout->addWidget(headerGroup);
// Body HTML
QGroupBox* htmlGroup = new QGroupBox(tr("Cuerpo HTML"));
QVBoxLayout* htmlLayout = new QVBoxLayout(htmlGroup);
QHBoxLayout* htmlToolbar = new QHBoxLayout();
htmlToolbar->addWidget(new QLabel(tr("Variables disponibles: {{nombre}}, {{email}}, {{fecha}}, etc.")));
htmlToolbar->addStretch();
QPushButton* btnInsertVar = new QPushButton(tr("Insertar variable"));
btnInsertVar->setStyleSheet("QPushButton { background: #1976D2; color: white; padding: 4px 8px; border-radius: 3px; }");
connect(btnInsertVar, &QPushButton::clicked, [this]() {
if (!m_variablesTable->rowCount()) return;
int row = m_variablesTable->currentRow();
if (row < 0) row = 0;
QTableWidgetItem* item = m_variablesTable->item(row, 0);
if (item) {
QString varName = item->text();
m_bodyHtmlEdit->insertPlainText(QString("{{%1}}").arg(varName));
}
});
htmlToolbar->addWidget(btnInsertVar);
htmlLayout->addLayout(htmlToolbar);
m_bodyHtmlEdit = new QTextEdit();
m_bodyHtmlEdit->setAcceptRichText(true);
m_bodyHtmlEdit->setPlaceholderText(tr("Cuerpo HTML del mensaje...<br>Use {{variable}} para insertar variables."));
m_bodyHtmlEdit->setMinimumHeight(250);
htmlLayout->addWidget(m_bodyHtmlEdit);
mainLayout->addWidget(htmlGroup, 1);
// Body Text (plain text alternative)
QGroupBox* textGroup = new QGroupBox(tr("Cuerpo texto plano (opcional)"));
QVBoxLayout* textLayout = new QVBoxLayout(textGroup);
m_bodyTextEdit = new QTextEdit();
m_bodyTextEdit->setPlaceholderText(tr("Versión texto plano para clientes que no soportan HTML"));
m_bodyTextEdit->setMinimumHeight(150);
textLayout->addWidget(m_bodyTextEdit);
mainLayout->addWidget(textGroup);
// Variables table
QGroupBox* varsGroup = new QGroupBox(tr("Variables de la plantilla"));
QVBoxLayout* varsLayout = new QVBoxLayout(varsGroup);
m_variablesTable = new QTableWidget();
m_variablesTable->setColumnCount(6);
m_variablesTable->setHorizontalHeaderLabels({tr("Nombre"), tr("Etiqueta"), tr("Valor por defecto"), tr("Tipo"), tr("Opciones"), ""});
m_variablesTable->horizontalHeader()->setStretchLastSection(true);
m_variablesTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
m_variablesTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_variablesTable->verticalHeader()->setVisible(false);
varsLayout->addWidget(m_variablesTable);
QHBoxLayout* varsBtnLayout = new QHBoxLayout();
QPushButton* btnAddVar = new QPushButton(tr("+ Añadir variable"));
btnAddVar->setStyleSheet("QPushButton { background: #1976D2; color: white; padding: 6px 12px; border-radius: 4px; }");
connect(btnAddVar, &QPushButton::clicked, this, &TemplateEditorDialog::onAddVariable);
QPushButton* btnRemVar = new QPushButton(tr("- Eliminar"));
connect(btnRemVar, &QPushButton::clicked, this, &TemplateEditorDialog::onRemoveVariable);
varsBtnLayout->addWidget(btnAddVar);
varsBtnLayout->addWidget(btnRemVar);
varsBtnLayout->addStretch();
varsLayout->addLayout(varsBtnLayout);
mainLayout->addWidget(varsGroup, 1);
// Preview button
QHBoxLayout* previewLayout = new QHBoxLayout();
previewLayout->addStretch();
QPushButton* btnPreview = new QPushButton(tr("Vista previa"));
btnPreview->setStyleSheet("QPushButton { background: #4CAF50; color: white; padding: 8px 16px; border-radius: 4px; }");
connect(btnPreview, &QPushButton::clicked, this, &TemplateEditorDialog::onPreview);
previewLayout->addWidget(btnPreview);
mainLayout->addLayout(previewLayout);
// Buttons
QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &TemplateEditorDialog::onAccept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttonBox);
}
void TemplateEditorDialog::populateFromTemplate()
{
if (!m_template.isValid()) return;
m_nameEdit->setText(m_template.name);
m_subjectEdit->setText(m_template.subject);
m_bodyHtmlEdit->setHtml(m_template.bodyHtml);
m_bodyTextEdit->setPlainText(m_template.bodyText);
m_isDefaultCheck->setChecked(m_template.isDefault);
for (const TemplateVariable& var : m_template.variables) {
int row = m_variablesTable->rowCount();
m_variablesTable->insertRow(row);
setupVariableRow(row, var);
}
}
void TemplateEditorDialog::setupVariableRow(int row, const TemplateVariable& var)
{
// Name
QLineEdit* nameEdit = new QLineEdit(var.name);
nameEdit->setPlaceholderText("nombre_variable");
m_variablesTable->setCellWidget(row, 0, nameEdit);
// Label
QLineEdit* labelEdit = new QLineEdit(var.label.isEmpty() ? var.name : var.label);
labelEdit->setPlaceholderText("Etiqueta visible");
m_variablesTable->setCellWidget(row, 1, labelEdit);
// Default value
QLineEdit* defaultEdit = new QLineEdit(var.defaultValue);
defaultEdit->setPlaceholderText("Valor por defecto");
m_variablesTable->setCellWidget(row, 2, defaultEdit);
// Type combo
QComboBox* typeCombo = new QComboBox();
typeCombo->addItem(tr("Texto"), "text");
typeCombo->addItem(tr("Email"), "email");
typeCombo->addItem(tr("Fecha"), "date");
typeCombo->addItem(tr("Selección"), "select");
typeCombo->addItem(tr("Área de texto"), "textarea");
typeCombo->addItem(tr("Número"), "number");
typeCombo->setCurrentIndex(typeCombo->findData(var.type));
connect(typeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this, row](int) { onVariableTypeChanged(row); });
m_variablesTable->setCellWidget(row, 3, typeCombo);
// Options (for select type)
QLineEdit* optionsEdit = new QLineEdit(var.options.join(","));
optionsEdit->setPlaceholderText("opcion1,opcion2,opcion3 (solo para selección)");
optionsEdit->setVisible(var.type == "select");
m_variablesTable->setCellWidget(row, 4, optionsEdit);
// Remove button
QPushButton* removeBtn = new QPushButton("");
removeBtn->setFixedSize(24, 24);
removeBtn->setToolTip(tr("Eliminar variable"));
removeBtn->setStyleSheet("QPushButton { background: transparent; color: #d32f2f; border: none; font-weight: bold; } QPushButton:hover { background: #fdeaea; border-radius: 3px; }");
connect(removeBtn, &QPushButton::clicked, [this, row]() {
m_variablesTable->removeRow(row);
});
m_variablesTable->setCellWidget(row, 5, removeBtn);
onVariableTypeChanged(row);
}
void TemplateEditorDialog::onAddVariable()
{
int row = m_variablesTable->rowCount();
m_variablesTable->insertRow(row);
setupVariableRow(row);
}
void TemplateEditorDialog::onRemoveVariable()
{
int row = m_variablesTable->currentRow();
if (row >= 0) {
m_variablesTable->removeRow(row);
}
}
void TemplateEditorDialog::onVariableTypeChanged(int row)
{
QComboBox* typeCombo = qobject_cast<QComboBox*>(m_variablesTable->cellWidget(row, 3));
QLineEdit* optionsEdit = qobject_cast<QLineEdit*>(m_variablesTable->cellWidget(row, 4));
if (typeCombo && optionsEdit) {
bool isSelect = typeCombo->currentData().toString() == "select";
optionsEdit->setVisible(isSelect);
}
}
TemplateVariable TemplateEditorDialog::variableFromRow(int row) const
{
TemplateVariable var;
QLineEdit* nameEdit = qobject_cast<QLineEdit*>(m_variablesTable->cellWidget(row, 0));
QLineEdit* labelEdit = qobject_cast<QLineEdit*>(m_variablesTable->cellWidget(row, 1));
QLineEdit* defaultEdit = qobject_cast<QLineEdit*>(m_variablesTable->cellWidget(row, 2));
QComboBox* typeCombo = qobject_cast<QComboBox*>(m_variablesTable->cellWidget(row, 3));
QLineEdit* optionsEdit = qobject_cast<QLineEdit*>(m_variablesTable->cellWidget(row, 4));
if (nameEdit) var.name = nameEdit->text().trimmed();
if (labelEdit) var.label = labelEdit->text().trimmed();
if (defaultEdit) var.defaultValue = defaultEdit->text();
if (typeCombo) var.type = typeCombo->currentData().toString();
if (optionsEdit && var.type == "select") {
var.options = optionsEdit->text().split(",", Qt::SkipEmptyParts);
for (QString& opt : var.options) opt = opt.trimmed();
}
return var;
}
void TemplateEditorDialog::onAccept()
{
if (m_nameEdit->text().trimmed().isEmpty()) {
QMessageBox::warning(this, tr("Error"), tr("El nombre de la plantilla es obligatorio."));
m_nameEdit->setFocus();
return;
}
m_template.name = m_nameEdit->text().trimmed();
m_template.subject = m_subjectEdit->text();
m_template.bodyHtml = m_bodyHtmlEdit->toHtml();
m_template.bodyText = m_bodyTextEdit->toPlainText();
m_template.isDefault = m_isDefaultCheck->isChecked();
m_template.accountId = m_accountId >= 0 ? m_accountId : -1;
m_template.variables.clear();
for (int i = 0; i < m_variablesTable->rowCount(); ++i) {
TemplateVariable var = variableFromRow(i);
if (!var.name.isEmpty()) {
m_template.variables.append(var);
}
}
m_template.updatedAt = QDateTime::currentDateTime();
if (m_template.isValid()) {
if (TemplateDao::update(m_template)) {
emit templateSaved(m_template);
accept();
} else {
QMessageBox::critical(this, tr("Error"), tr("No se pudo actualizar la plantilla."));
}
} else {
if (TemplateDao::insert(m_template)) {
emit templateSaved(m_template);
accept();
} else {
QMessageBox::critical(this, tr("Error"), tr("No se pudo crear la plantilla."));
}
}
}
void TemplateEditorDialog::onPreview()
{
// Simple preview - show rendered HTML in a dialog
QString html = m_bodyHtmlEdit->toHtml();
QString subject = m_subjectEdit->text();
// Replace variables with default values for preview
for (int i = 0; i < m_variablesTable->rowCount(); ++i) {
TemplateVariable var = variableFromRow(i);
if (!var.name.isEmpty() && !var.defaultValue.isEmpty()) {
html.replace(QString("{{%1}}").arg(var.name), var.defaultValue);
subject.replace(QString("{{%1}}").arg(var.name), var.defaultValue);
}
}
QDialog previewDlg(this);
previewDlg.setWindowTitle(tr("Vista previa: %1").arg(subject));
previewDlg.resize(700, 500);
QVBoxLayout* layout = new QVBoxLayout(&previewDlg);
QLabel* subjectLabel = new QLabel(QString("<b>%1</b>").arg(subject));
subjectLabel->setStyleSheet("font-size: 14px; padding: 8px; background: #f5f5f5; border-bottom: 1px solid #ddd;");
layout->addWidget(subjectLabel);
QTextBrowser* browser = new QTextBrowser();
browser->setHtml(html);
browser->setOpenExternalLinks(false);
layout->addWidget(browser);
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
connect(buttons, &QDialogButtonBox::rejected, &previewDlg, &QDialog::reject);
layout->addWidget(buttons);
previewDlg.exec();
}
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <QDialog>
#include "db/dao/templatedao.h"
#include <QTableWidget>
#include <QLineEdit>
#include <QTextEdit>
#include <QSpinBox>
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <QTextBrowser>
class TemplateEditorDialog : public QDialog
{
Q_OBJECT
public:
explicit TemplateEditorDialog(const Template& tmpl = Template(), qint64 accountId = -1, QWidget *parent = nullptr);
Template getTemplate() const { return m_template; }
signals:
void templateSaved(const Template& tmpl);
private slots:
void onAddVariable();
void onRemoveVariable();
void onVariableTypeChanged(int row);
void onAccept();
void onPreview();
private:
Template m_template;
qint64 m_accountId;
QLineEdit* m_nameEdit;
QLineEdit* m_subjectEdit;
QTextEdit* m_bodyHtmlEdit;
QTextEdit* m_bodyTextEdit;
QTableWidget* m_variablesTable;
QCheckBox* m_isDefaultCheck;
void setupUI();
void populateFromTemplate();
void setupVariableRow(int row, const TemplateVariable& var = TemplateVariable());
TemplateVariable variableFromRow(int row) const;
};
+226
View File
@@ -0,0 +1,226 @@
#include "templatesmanagerdialog.h"
#include "templateeditordialog.h"
#include "db/dao/templatedao.h"
#include <QHeaderView>
#include <QMessageBox>
#include <QInputDialog>
#include <QFileDialog>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QMenu>
#include <QDebug>
TemplatesManagerDialog::TemplatesManagerDialog(qint64 accountId, QWidget *parent)
: QDialog(parent), m_accountId(accountId)
{
setWindowTitle(m_accountId >= 0 ? tr("Plantillas de la cuenta") : tr("Plantillas globales"));
resize(800, 500);
setupUI();
loadTemplates();
}
void TemplatesManagerDialog::setupUI()
{
QVBoxLayout* mainLayout = new QVBoxLayout(this);
// Toolbar
QHBoxLayout* toolLayout = new QHBoxLayout();
m_btnNew = new QPushButton(tr("Nueva plantilla"));
m_btnUse = new QPushButton(tr("Usar plantilla"));
m_btnImport = new QPushButton(tr("Importar"));
m_btnExport = new QPushButton(tr("Exportar"));
m_btnClose = new QPushButton(tr("Cerrar"));
m_btnNew->setStyleSheet("QPushButton { background: #1976D2; color: white; font-weight: bold; padding: 6px 12px; border-radius: 4px; } QPushButton:hover { background: #1565C0; }");
m_btnUse->setStyleSheet("QPushButton { background: #4CAF50; color: white; padding: 6px 12px; border-radius: 4px; } QPushButton:hover { background: #43A047; }");
toolLayout->addWidget(m_btnNew);
toolLayout->addWidget(m_btnUse);
toolLayout->addStretch();
toolLayout->addWidget(m_btnImport);
toolLayout->addWidget(m_btnExport);
toolLayout->addWidget(m_btnClose);
mainLayout->addLayout(toolLayout);
// Tree widget
m_tree = new QTreeWidget();
m_tree->setHeaderLabels({tr("Por defecto"), tr("Nombre"), tr("Asunto"), tr("Variables"), tr("Cuenta"), tr("Creada"), tr("Actualizada")});
m_tree->setAlternatingRowColors(true);
m_tree->setSelectionBehavior(QAbstractItemView::SelectRows);
m_tree->setSelectionMode(QAbstractItemView::SingleSelection);
m_tree->setContextMenuPolicy(Qt::CustomContextMenu);
m_tree->header()->setStretchLastSection(false);
m_tree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
m_tree->setColumnWidth(0, 80);
m_tree->setColumnWidth(2, 250);
m_tree->setColumnWidth(3, 80);
m_tree->setColumnWidth(4, 100);
m_tree->setColumnWidth(5, 120);
m_tree->setColumnWidth(6, 120);
connect(m_tree, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
QTreeWidgetItem* item = m_tree->itemAt(pos);
if (!item) return;
QMenu menu(this);
QAction* editAct = menu.addAction(tr("Editar"), [this, item]() { onEditTemplate(item); });
QAction* delAct = menu.addAction(tr("Eliminar"), [this, item]() { onDeleteTemplate(item); });
menu.addSeparator();
QAction* defaultAct = menu.addAction(item->checkState(0) == Qt::Checked ? tr("Quitar por defecto") : tr("Marcar por defecto"), [this, item]() { onToggleDefault(item); });
menu.exec(m_tree->viewport()->mapToGlobal(pos));
});
connect(m_tree, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* item) { onEditTemplate(item); });
connect(m_tree, &QTreeWidget::itemSelectionChanged, this, [this]() {
QList<QTreeWidgetItem*> items = m_tree->selectedItems();
if (!items.isEmpty()) {
m_selectedTemplate = itemToTemplate(items.first());
}
});
mainLayout->addWidget(m_tree, 1);
// Status
QLabel* statusLabel = new QLabel(tr("Doble click para editar. Check = plantilla por defecto. Click derecho para más opciones."));
statusLabel->setStyleSheet("color: #666; font-size: 11px; padding: 4px;");
mainLayout->addWidget(statusLabel);
// Connect buttons
connect(m_btnNew, &QPushButton::clicked, this, &TemplatesManagerDialog::onNewTemplate);
connect(m_btnUse, &QPushButton::clicked, this, [this]() {
if (m_selectedTemplate.isValid()) {
emit templateSelected(m_selectedTemplate);
accept();
}
});
connect(m_btnImport, &QPushButton::clicked, this, [this]() { onImportExport(true); });
connect(m_btnExport, &QPushButton::clicked, this, [this]() { onImportExport(false); });
connect(m_btnClose, &QPushButton::clicked, this, &QDialog::accept);
}
void TemplatesManagerDialog::loadTemplates()
{
m_tree->clear();
QVector<Template> templates = TemplateDao::findByAccount(m_accountId);
for (const Template& tmpl : templates) {
addTemplateToTree(tmpl);
}
}
void TemplatesManagerDialog::addTemplateToTree(const Template& tmpl)
{
QTreeWidgetItem* item = new QTreeWidgetItem(m_tree);
item->setData(0, Qt::UserRole, tmpl.id);
item->setCheckState(0, tmpl.isDefault ? Qt::Checked : Qt::Unchecked);
item->setText(1, tmpl.name);
item->setText(2, tmpl.subject.isEmpty() ? tr("(sin asunto)") : tmpl.subject);
item->setText(3, QString::number(tmpl.variables.size()));
item->setText(4, tmpl.isGlobal() ? tr("Global") : QString::number(tmpl.accountId));
item->setText(5, tmpl.createdAt.isValid() ? tmpl.createdAt.toString("dd/MM/yyyy hh:mm") : tr("Desconocido"));
item->setText(6, tmpl.updatedAt.isValid() ? tmpl.updatedAt.toString("dd/MM/yyyy hh:mm") : tr("Desconocido"));
if (tmpl.isDefault) {
QFont font = item->font(1);
font.setBold(true);
item->setFont(1, font);
}
}
Template TemplatesManagerDialog::itemToTemplate(QTreeWidgetItem* item) const
{
qint64 id = item->data(0, Qt::UserRole).toLongLong();
auto tmplOpt = TemplateDao::findById(id);
return tmplOpt.value_or(Template());
}
void TemplatesManagerDialog::onNewTemplate()
{
TemplateEditorDialog dlg(Template(), m_accountId, this);
connect(&dlg, &TemplateEditorDialog::templateSaved, this, &TemplatesManagerDialog::onTemplateSaved);
dlg.exec();
}
void TemplatesManagerDialog::onEditTemplate(QTreeWidgetItem* item)
{
Template tmpl = itemToTemplate(item);
if (!tmpl.isValid()) return;
TemplateEditorDialog dlg(tmpl, m_accountId, this);
connect(&dlg, &TemplateEditorDialog::templateSaved, this, &TemplatesManagerDialog::onTemplateSaved);
dlg.exec();
}
void TemplatesManagerDialog::onTemplateSaved(const Template& tmpl)
{
refresh();
emit templatesChanged();
}
void TemplatesManagerDialog::onDeleteTemplate(QTreeWidgetItem* item)
{
Template tmpl = itemToTemplate(item);
if (!tmpl.isValid()) return;
if (QMessageBox::question(this, tr("Eliminar plantilla"),
tr("¿Eliminar la plantilla \"%1\"?").arg(tmpl.name),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
if (TemplateDao::remove(tmpl.id)) {
refresh();
emit templatesChanged();
}
}
}
void TemplatesManagerDialog::onToggleDefault(QTreeWidgetItem* item)
{
Template tmpl = itemToTemplate(item);
if (!tmpl.isValid()) return;
bool newState = item->checkState(0) != Qt::Checked;
if (TemplateDao::setDefault(tmpl.id, newState)) {
item->setCheckState(0, newState ? Qt::Checked : Qt::Unchecked);
if (newState) {
QFont font = item->font(1);
font.setBold(true);
item->setFont(1, font);
} else {
QFont font = item->font(1);
font.setBold(false);
item->setFont(1, font);
}
refresh(); // Refresh to update other templates' default status
emit templatesChanged();
}
}
void TemplatesManagerDialog::onImportExport(bool import)
{
if (import) {
QString file = QFileDialog::getOpenFileName(this, tr("Importar plantillas"), QString(), tr("JSON (*.json)"));
if (file.isEmpty()) return;
QMessageBox::information(this, tr("Importar"), tr("Función de importación en desarrollo."));
} else {
QString file = QFileDialog::getSaveFileName(this, tr("Exportar plantillas"), "templates.json", tr("JSON (*.json)"));
if (file.isEmpty()) return;
QVector<Template> templates = TemplateDao::findByAccount(m_accountId);
QJsonArray arr;
for (const Template& tmpl : templates) arr.append(tmpl.toJson());
QJsonDocument doc(arr);
QFile f(file);
if (f.open(QIODevice::WriteOnly)) {
f.write(doc.toJson(QJsonDocument::Indented));
QMessageBox::information(this, tr("Exportar"), tr("Plantillas exportadas a %1").arg(file));
}
}
}
void TemplatesManagerDialog::refresh()
{
loadTemplates();
}
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <QDialog>
#include <QTreeWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include "db/dao/templatedao.h"
#include "templateeditordialog.h"
class TemplatesManagerDialog : public QDialog
{
Q_OBJECT
public:
explicit TemplatesManagerDialog(qint64 accountId = -1, QWidget *parent = nullptr);
Template getSelectedTemplate() const { return m_selectedTemplate; }
signals:
void templateSelected(const Template& tmpl);
void templatesChanged();
private slots:
void onNewTemplate();
void onEditTemplate(QTreeWidgetItem* item);
void onDeleteTemplate(QTreeWidgetItem* item);
void onToggleDefault(QTreeWidgetItem* item);
void onImportExport(bool import);
void onTemplateSaved(const Template& tmpl);
void refresh();
private:
qint64 m_accountId;
Template m_selectedTemplate;
QTreeWidget* m_tree;
QPushButton* m_btnNew;
QPushButton* m_btnUse;
QPushButton* m_btnImport;
QPushButton* m_btnExport;
QPushButton* m_btnClose;
void setupUI();
void loadTemplates();
void addTemplateToTree(const Template& tmpl);
Template itemToTemplate(QTreeWidgetItem* item) const;
};