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
+257
View File
@@ -0,0 +1,257 @@
#include "categorytreewidget.h"
#include "db/dao/categorydao.h"
#include "db/dao/mailitemdao.h"
#include <QInputDialog>
#include <QColorDialog>
#include <QMessageBox>
#include <QDebug>
#include <QApplication>
CategoryTreeWidget::CategoryTreeWidget(QWidget *parent) : QTreeWidget(parent)
{
setHeaderLabel(tr("Categorías"));
setContextMenuPolicy(Qt::CustomContextMenu);
setExpandsOnDoubleClick(false);
setAnimated(true);
setIndentation(16);
connect(this, &QTreeWidget::customContextMenuRequested, this, &CategoryTreeWidget::onCategoryContextMenu);
connect(this, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* item, int) {
Category cat = itemToCategory(item);
if (cat.isValid()) emit categoryDoubleClicked(cat);
});
connect(this, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int) {
Category cat = itemToCategory(item);
if (cat.isValid()) emit categorySelected(cat);
});
setupContextMenu();
}
void CategoryTreeWidget::setupContextMenu()
{
m_contextMenu = new QMenu(this);
m_contextMenu->addAction(tr("Nueva categoría"), this, &CategoryTreeWidget::onNewCategory);
m_contextMenu->addAction(tr("Nueva subcategoría"), this, [this]() {
QTreeWidgetItem* item = currentItem();
if (item) onEditCategory(item); // Reuse for subcategory creation
});
m_contextMenu->addSeparator();
m_contextMenu->addAction(tr("Editar"), this, [this]() {
QTreeWidgetItem* item = currentItem();
if (item) onEditCategory(item);
});
m_contextMenu->addAction(tr("Eliminar"), this, [this]() {
QTreeWidgetItem* item = currentItem();
if (item) onDeleteCategory(item);
});
m_contextMenu->addSeparator();
m_contextMenu->addAction(tr("Asignar a correos seleccionados"), this, [this]() {
QTreeWidgetItem* item = currentItem();
if (item) onAssignToSelectedMails(item);
});
}
void CategoryTreeWidget::setAccountId(qint64 accountId)
{
m_accountId = accountId;
refresh();
}
void CategoryTreeWidget::refresh()
{
clear();
m_categoryItems.clear();
loadCategories();
expandAll();
}
void CategoryTreeWidget::loadCategories()
{
QVector<Category> categories = CategoryDao::findByAccount(m_accountId);
// Build tree: first root categories, then children
QMap<qint64, QVector<Category>> childrenMap;
QVector<Category> roots;
for (const Category& cat : categories) {
if (cat.parentCategoryId >= 0) {
childrenMap[cat.parentCategoryId].append(cat);
} else {
roots.append(cat);
}
}
// Sort by sortOrder
auto sortByOrder = [](const Category& a, const Category& b) {
return a.sortOrder < b.sortOrder;
};
std::sort(roots.begin(), roots.end(), sortByOrder);
for (auto it = childrenMap.begin(); it != childrenMap.end(); ++it) {
std::sort(it.value().begin(), it.value().end(), sortByOrder);
}
for (const Category& root : roots) {
addCategory(root);
}
}
void CategoryTreeWidget::addCategory(const Category& cat, QTreeWidgetItem* parent)
{
QTreeWidgetItem* item = parent ? new QTreeWidgetItem(parent) : new QTreeWidgetItem(this);
item->setText(0, cat.name);
item->setData(0, Qt::UserRole, cat.id);
item->setData(0, Qt::UserRole + 1, cat.accountId);
item->setData(0, Qt::UserRole + 2, cat.parentCategoryId);
item->setData(0, Qt::UserRole + 3, cat.sortOrder);
// Color indicator
QPixmap pix(16, 16);
pix.fill(cat.color);
item->setIcon(0, QIcon(pix));
// Tooltip with details
QString tooltip = QString("%1\nColor: %2\nAccount: %3")
.arg(cat.name)
.arg(cat.color.name())
.arg(cat.isGlobal() ? "Global" : QString::number(cat.accountId));
item->setToolTip(0, tooltip);
m_categoryItems[cat.id] = item;
// Add children
QVector<Category> children = CategoryDao::findChildren(cat.id);
for (const Category& child : children) {
addCategory(child, item);
}
}
QTreeWidgetItem* CategoryTreeWidget::findItemByCategoryId(qint64 categoryId)
{
return m_categoryItems.value(categoryId, nullptr);
}
void CategoryTreeWidget::onMailSelected(qint64 mailId)
{
// Update checkboxes/selection state for categories this mail belongs to
QVector<qint64> catIds = CategoryDao::categoriesForMail(mailId);
for (auto it = m_categoryItems.begin(); it != m_categoryItems.end(); ++it) {
QTreeWidgetItem* item = it.value();
bool assigned = catIds.contains(it.key());
item->setCheckState(0, assigned ? Qt::Checked : Qt::Unchecked);
}
}
void CategoryTreeWidget::onCategoryContextMenu(const QPoint& pos)
{
QTreeWidgetItem* item = itemAt(pos);
if (item) setCurrentItem(item);
// Update action states
bool hasItem = item != nullptr;
QList<QAction*> actions = m_contextMenu->actions();
for (QAction* act : actions) {
QString text = act->text();
if (text.contains("Editar") || text.contains("Eliminar") || text.contains("Asignar")) {
act->setEnabled(hasItem);
}
}
m_contextMenu->exec(viewport()->mapToGlobal(pos));
}
void CategoryTreeWidget::onNewCategory()
{
QTreeWidgetItem* parentItem = currentItem();
qint64 parentId = parentItem ? parentItem->data(0, Qt::UserRole).toLongLong() : -1;
bool ok;
QString name = QInputDialog::getText(this, tr("Nueva categoría"), tr("Nombre:"), QLineEdit::Normal, QString(), &ok);
if (!ok || name.trimmed().isEmpty()) return;
QColor color = QColorDialog::getColor(QColor("#1976D2"), this, tr("Color de la categoría"));
if (!color.isValid()) color = QColor("#1976D2");
Category cat;
cat.name = name.trimmed();
cat.color = color;
cat.accountId = m_accountId;
cat.parentCategoryId = parentId;
cat.sortOrder = 0;
if (CategoryDao::insert(cat)) {
refresh();
emit categoryCreated(cat);
} else {
QMessageBox::warning(this, tr("Error"), tr("No se pudo crear la categoría."));
}
}
void CategoryTreeWidget::onEditCategory(QTreeWidgetItem* item)
{
if (!item) return;
qint64 catId = item->data(0, Qt::UserRole).toLongLong();
auto catOpt = CategoryDao::findById(catId);
if (!catOpt.has_value()) return;
Category cat = *catOpt;
bool ok;
QString name = QInputDialog::getText(this, tr("Editar categoría"), tr("Nombre:"), QLineEdit::Normal, cat.name, &ok);
if (!ok || name.trimmed().isEmpty()) return;
QColor color = QColorDialog::getColor(cat.color, this, tr("Color de la categoría"));
if (!color.isValid()) color = cat.color;
cat.name = name.trimmed();
cat.color = color;
if (CategoryDao::update(cat)) {
refresh();
emit categoryEdited(cat);
} else {
QMessageBox::warning(this, tr("Error"), tr("No se pudo actualizar la categoría."));
}
}
void CategoryTreeWidget::onDeleteCategory(QTreeWidgetItem* item)
{
if (!item) return;
qint64 catId = item->data(0, Qt::UserRole).toLongLong();
if (QMessageBox::question(this, tr("Eliminar categoría"),
tr("¿Eliminar esta categoría y quitarla de todos los correos?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
if (CategoryDao::remove(catId)) {
refresh();
emit categoryDeleted(catId);
} else {
QMessageBox::warning(this, tr("Error"), tr("No se pudo eliminar la categoría."));
}
}
void CategoryTreeWidget::onAssignToSelectedMails(QTreeWidgetItem* item)
{
if (!item) return;
qint64 catId = item->data(0, Qt::UserRole).toLongLong();
// This would need integration with the mail list view to get selected mail IDs
// For now, emit signal for the main window to handle
emit assignCategoryRequested(-1, catId); // -1 = use current selection
}
Category CategoryTreeWidget::itemToCategory(QTreeWidgetItem* item) const
{
if (!item) return Category();
qint64 id = item->data(0, Qt::UserRole).toLongLong();
auto catOpt = CategoryDao::findById(id);
return catOpt.value_or(Category());
}