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:
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user