feat: comprehensive Wino Mail updates

- ReaderView: complete rewrite with CSS styling, headers, attachments, zoom, find, dark mode, image blocking
- Categories: DAO + UI tree widget with colors, nested categories, assignment
- Rules engine: DAO + evaluation + actions (move, mark read, flag, delete, category, forward)
- Templates: DAO + editor + manager with variables support
- Signatures: DAO + manager + auto-per-account + manual selector in compose
- ComposeView: signature combo, template selector, auto-signature on new email
- MainWindow: frameless with rounded corners, 1px border, resize from edges, no status bar
- Database: new tables (Category, MailCategory, Rule, Template, Signature) with indexes
This commit is contained in:
2026-08-23 14:49:58 +02:00
parent 5fcedfa129
commit 0bb8738607
7399 changed files with 44711 additions and 209 deletions
+182 -40
View File
@@ -1,6 +1,9 @@
// ===================== ComposeView =====================
#include "composeview.h"
#include "db/dao/templatedao.h"
#include "ui/templatesmanagerdialog.h"
#include "ui/signaturemanagerdialog.h"
#include <QDialog>
#include <QInputDialog>
#include <QVBoxLayout>
@@ -16,6 +19,9 @@
#include <QStyle>
#include <QApplication>
#include <QFileIconProvider>
#include <QMainWindow>
#include <QSqlDatabase>
#include <QSqlQuery>
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
@@ -27,36 +33,31 @@ ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
void ComposeView::onSignatureClicked()
{
// Open signature manager dialog
qint64 accountId = -1;
if (m_accountService && m_accountCombo && m_accountCombo->currentIndex() > 0) {
accountId = m_accountCombo->itemData(m_accountCombo->currentIndex()).toLongLong();
}
SignatureManagerDialog dlg(m_accountService, accountId, this);
connect(&dlg, &SignatureManagerDialog::signatureSelected, this, [this](const QString& html) {
if (!html.isEmpty()) {
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(html);
}
});
connect(&dlg, &SignatureManagerDialog::signaturesChanged, this, [this]() {
emit statusMessageRequested(tr("Firmas actualizadas"));
});
dlg.exec();
}
void ComposeView::onSignatureEditRequested() {
QDialog dialog(this);
dialog.setWindowTitle(tr("Edit Signature"));
dialog.setMinimumSize(600, 400);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
RichTextEditor *editor = new RichTextEditor(&dialog);
editor->setupToolbar(layout);
layout->addWidget(editor);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (dialog.exec() == QDialog::Accepted) {
QString signatureHtml = editor->toHtml();
if (!signatureHtml.isEmpty()) {
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(signatureHtml);
}
}
void ComposeView::onSignatureEditRequested()
{
// Same as click - open manager
onSignatureClicked();
}
void ComposeView::setAccountService(AccountService *service) {
@@ -89,31 +90,123 @@ void ComposeView::onAccountChanged(int index) {
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
m_signatureCombo->clear();
m_signatureCombo->addItem(tr("Sin cuenta seleccionada"), QVariant());
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
populateSignatureCombo(m_currentAccountId);
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount() {
void ComposeView::populateSignatureCombo(qint64 accountId)
{
m_signatureCombo->clear();
m_signatureCombo->addItem(tr("Firma automática"), QVariant()); // Index 0 = auto
if (!m_accountService || accountId <= 0) {
m_signatureCombo->addItem(tr("Sin firmas disponibles"), QVariant());
return;
}
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
// Get signatures for this account + global
query.prepare("SELECT id, name FROM Signature WHERE accountId = :accountId OR accountId IS NULL ORDER BY isDefault DESC, name");
query.bindValue(":accountId", accountId);
if (query.exec()) {
while (query.next()) {
QString name = query.value("name").toString();
qint64 id = query.value("id").toLongLong();
m_signatureCombo->addItem(name, id);
}
}
// Set to first item (automatic)
m_signatureCombo->setCurrentIndex(0);
}
void ComposeView::onSignatureComboChanged(int index)
{
if (index <= 0) {
// Automatic mode - use default signature
loadSignatureForCurrentAccount();
return;
}
qint64 signatureId = m_signatureCombo->itemData(index).toLongLong();
if (signatureId <= 0) return;
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT html FROM Signature WHERE id = :id");
query.bindValue(":id", signatureId);
if (query.exec() && query.next()) {
QString html = query.value(0).toString();
if (!html.isEmpty()) {
m_bodyEditor->setHtml(html);
}
}
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
delete account;
QString signatureHtml = getDefaultSignatureForAccount(m_currentAccountId);
if (!signatureHtml.isEmpty()) {
m_bodyEditor->setHtml(signatureHtml);
} else {
m_bodyEditor->clear();
}
}
QString ComposeView::getDefaultSignatureForAccount(qint64 accountId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
// First try to get account-specific default signature
query.prepare("SELECT html FROM Signature WHERE accountId = :accountId AND name LIKE '%default%' ORDER BY updatedAt DESC LIMIT 1");
query.bindValue(":accountId", accountId);
if (query.exec() && query.next()) {
return query.value(0).toString();
}
// Fallback to global default signature
query.prepare("SELECT html FROM Signature WHERE accountId IS NULL AND name LIKE '%default%' ORDER BY updatedAt DESC LIMIT 1");
if (query.exec() && query.next()) {
return query.value(0).toString();
}
// Last resort: any signature for this account
query.prepare("SELECT html FROM Signature WHERE accountId = :accountId ORDER BY updatedAt DESC LIMIT 1");
query.bindValue(":accountId", accountId);
if (query.exec() && query.next()) {
return query.value(0).toString();
}
// Global any signature
query.prepare("SELECT html FROM Signature WHERE accountId IS NULL ORDER BY updatedAt DESC LIMIT 1");
if (query.exec() && query.next()) {
return query.value(0).toString();
}
return QString();
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
@@ -143,6 +236,16 @@ void ComposeView::setupUI() {
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo);
// Signature selector
m_signatureCombo = new QComboBox();
m_signatureCombo->setFixedWidth(180);
m_signatureCombo->setStyleSheet(m_accountCombo->styleSheet());
m_signatureCombo->setToolTip(tr("Seleccionar firma"));
connect(m_signatureCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onSignatureComboChanged);
fromLayout->addWidget(m_signatureCombo);
fromLayout->addStretch(); // Este resorte empuja el par (label+combo) a la izquierda
mainLayout->addLayout(fromLayout);
@@ -426,13 +529,16 @@ void ComposeView::initializeComposition() {
m_ccField->tags(QStringList{});
m_bccField->tags(QStringList{});
m_subjectField->clear();
m_bodyEditor->clear();
m_attachmentFiles.clear();
m_attachmentList->clear();
m_attachmentList->setVisible(false);
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
// Load signature for current account
loadSignatureForCurrentAccount();
m_toField->setFocus();
}
@@ -528,9 +634,45 @@ void ComposeView::startNewEmail(const QString &initialRecipient) {
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
void ComposeView::onTemplateClicked() {
// Placeholder for template functionality
QMessageBox::information(this, tr("Email Templates"), tr("Email templates feature not yet implemented."));
void ComposeView::onTemplateClicked()
{
// Get current account ID if available
qint64 accountId = -1;
if (m_accountService) {
// Try to get current account from combo
if (m_accountCombo && m_accountCombo->currentIndex() > 0) {
accountId = m_accountCombo->itemData(m_accountCombo->currentIndex()).toLongLong();
}
}
TemplatesManagerDialog dlg(accountId, this);
connect(&dlg, &TemplatesManagerDialog::templateSelected, this, [this](const Template& tmpl) {
applyTemplate(tmpl);
});
dlg.exec();
}
void ComposeView::applyTemplate(const Template& tmpl)
{
if (!tmpl.isValid()) return;
// Set subject
if (!tmpl.subject.isEmpty()) {
m_subjectField->setText(tmpl.subject);
}
// Set body HTML
if (!tmpl.bodyHtml.isEmpty()) {
m_bodyEditor->setHtml(tmpl.bodyHtml);
}
// If there are variables, we could show a dialog to fill them
// For now, just apply as-is
emit statusMessageRequested(tr("Plantilla aplicada: %1").arg(tmpl.name));
}
void ComposeView::statusBarMessage(const QString& msg)
{
emit statusMessageRequested(msg);
}
#include "composeview.moc"