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:
@@ -688,4 +688,3 @@ void AccountSetupDialog::showError(const QString &message)
|
||||
m_btnNext->setVisible(false);
|
||||
}
|
||||
|
||||
#include "accountsetupdialog.moc"
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -221,4 +221,3 @@ void ConnectionWizard::accept()
|
||||
QWizard::accept();
|
||||
}
|
||||
|
||||
#include "connectionwizard.moc"
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; };
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user