feat: complete settings view + signatures + signatures DAO + icons

- SettingsView: complete UI with tabs for Accounts, Categories, Rules, Templates, Signatures, General, Appearance
- Categories: QTreeWidget with hierarchical view, color picker, parent categories
- Rules: QListWidget with RuleEditorDialog integration, toggle enabled
- Templates: QListWidget with TemplateEditorDialog integration
- Signatures: QListWidget with SignatureEditorDialog integration
- SignatureDao: full CRUD with default signature support
- common_structs.h: QColor for Category, all structs centralized
- icons.qrc: updated with Linear/Outline/Bold icons from resources
- Category: QColor instead of QString for color
- EmailTreeModel/DateGroupProxyModel: email grouping by date
- MailListView: QTreeView with date grouping combo
- ReaderView: subject frame with shadow, avatar in header, body inside header
- MainWindow: frameless with rounded corners, 1px border, edge resize, no status bar
- icons.qrc: complete icon set from resources/icons/SVG
This commit is contained in:
2026-08-24 10:29:13 +02:00
parent 2281b01f49
commit fdef311257
12 changed files with 1406 additions and 310 deletions
+1
View File
@@ -77,6 +77,7 @@ set(SRC_FILES
src/db/dao/categorydao.cpp src/db/dao/categorydao.cpp
src/db/dao/ruledao.cpp src/db/dao/ruledao.cpp
src/db/dao/templatedao.cpp src/db/dao/templatedao.cpp
src/db/dao/signaturedao.cpp
src/db/dbchangeprocessor.cpp src/db/dbchangeprocessor.cpp
src/core/eventbus.cpp src/core/eventbus.cpp
src/utils/notificationmanager.cpp src/utils/notificationmanager.cpp
+49 -15
View File
@@ -1,21 +1,55 @@
<RCC> <RCC>
<qresource prefix="/icons"> <qresource prefix="/icons">
<!-- App icons -->
<file alias="app.svg">resources/icons/app.svg</file> <file alias="app.svg">resources/icons/app.svg</file>
<file alias="sync.svg">resources/icons/sync.svg</file> <file alias="sync.svg">resources/icons/sync.svg</file>
<file alias="settings.svg">resources/icons/settings.svg</file> <file alias="settings.svg">resources/icons/settings.svg</file>
<file alias="minimize.svg">resources/icons/minimize.svg</file>
<file alias="maximize.svg">resources/icons/maximize.svg</file> <!-- Window controls -->
<file alias="restore.svg">resources/icons/restore.svg</file> <file alias="minimize.svg">resources/icons/SVG/Linear/Arrows Action/Minimize Square Minimalistic.svg</file>
<file alias="close.svg">resources/icons/close.svg</file> <file alias="maximize.svg">resources/icons/SVG/Linear/Arrows Action/Maximize Square Minimalistic.svg</file>
<file>resources/icons/SVG/Outline/Essentional, UI/Trash Bin Minimalistic.svg</file> <file alias="restore.svg">resources/icons/SVG/Linear/Arrows Action/Maximize Square 2.svg</file>
<file>resources/icons/SVG/Outline/Messages, Conversation/Paperclip.svg</file> <file alias="close.svg">resources/icons/SVG/Linear/Essentional, UI/Close Square.svg</file>
<file>resources/icons/SVG/Bold Duotone/Messages, Conversation/Plain.svg</file>
<file>resources/icons/SVG/Outline/Files/File Text.svg</file> <!-- Mail actions -->
<file>resources/icons/SVG/Bold/Text Formatting/Text Bold.svg</file> <file alias="attachment.svg">resources/icons/SVG/Outline/Messages, Conversation/Paperclip.svg</file>
<file>resources/icons/SVG/Outline/Text Formatting/Text Italic.svg</file> <file alias="send.svg">resources/icons/SVG/Bold Duotone/Messages, Conversation/Plain.svg</file>
<file>resources/icons/SVG/Outline/Text Formatting/Text Underline.svg</file> <file alias="reply.svg">resources/icons/SVG/Linear/Arrows Action/Reply.svg</file>
<file>resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg</file> <file alias="forward.svg">resources/icons/SVG/Linear/Arrows Action/Forward.svg</file>
<file>resources/icons/SVG/Linear/Arrows Action/Square Top Up.svg</file> <file alias="trash.svg">resources/icons/SVG/Linear/Essentional, UI/Trash Bin Minimalistic.svg</file>
<file>resources/icons/SVG/Linear/Essentional, UI/Close Square.svg</file> <file alias="filter.svg">resources/icons/SVG/Linear/Design, Tools/Filters.svg</file>
<file alias="play.svg">resources/icons/SVG/Linear/Time/Stopwatch Play.svg</file>
<file alias="file-text.svg">resources/icons/SVG/Linear/Files/File Text.svg</file>
<!-- UI elements -->
<file alias="dropdown.png">resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg</file>
<file alias="detach.svg">resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg</file>
<file alias="import.svg">resources/icons/SVG/Linear/Arrows Action/Import.svg</file>
<file alias="export.svg">resources/icons/SVG/Linear/Arrows Action/Export.svg</file>
<!-- Bold text formatting -->
<file alias="bold.svg">resources/icons/SVG/Bold/Text Formatting/Text Bold.svg</file>
<file alias="italic.svg">resources/icons/SVG/Outline/Text Formatting/Text Italic.svg</file>
<file alias="underline.svg">resources/icons/SVG/Outline/Text Formatting/Text Underline.svg</file>
<!-- User/Contact -->
<file alias="user.svg">resources/icons/SVG/Linear/Users/User.svg</file>
<!-- Calendar -->
<file alias="calendar.svg">resources/icons/SVG/Linear/Time/Calendar.svg</file>
<file alias="calendar-add.svg">resources/icons/SVG/Linear/Time/Calendar Add.svg</file>
<!-- Sync/Refresh - using existing icons -->
<file alias="refresh.svg">resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg</file>
<file alias="sync_all.svg">resources/icons/SVG/Linear/Arrows Action/Square Top Up.svg</file>
<!-- Menu dots -->
<file alias="dots.svg">resources/icons/SVG/Linear/Essentional, UI/Menu Dots.svg</file>
<!-- Design tools -->
<file alias="palette.svg">resources/icons/SVG/Linear/Design, Tools/Palette.svg</file>
<!-- Calendar -->
<file alias="calendar-add.svg">resources/icons/SVG/Linear/Time/Calendar Add.svg</file>
</qresource> </qresource>
</RCC> </RCC>
+107 -112
View File
@@ -10,19 +10,20 @@ bool CategoryDao::insert(Category& cat)
QSqlQuery query(db); QSqlQuery query(db);
query.prepare( query.prepare(
"INSERT INTO Category (accountId, name, color, parentCategoryId, sortOrder, createdAt) " "INSERT INTO Category (accountId, name, color, parentCategoryId, sortOrder, createdAt) "
"VALUES (:accountId, :name, :color, :parentCategoryId, :sortOrder, :createdAt)" "VALUES (:accountId, :name, :color, :parentCategoryId, :sortOrder, :createdAt)");
);
query.bindValue(":accountId", cat.accountId >= 0 ? cat.accountId : QVariant()); query.bindValue(":accountId", cat.accountId >= 0 ? cat.accountId : QVariant());
query.bindValue(":name", cat.name); query.bindValue(":name", cat.name);
query.bindValue(":color", cat.color.name()); query.bindValue(":color", cat.color.name(QColor::HexArgb));
query.bindValue(":parentCategoryId", cat.parentCategoryId >= 0 ? cat.parentCategoryId : QVariant()); query.bindValue(":parentCategoryId", cat.parentCategoryId >= 0 ? cat.parentCategoryId : QVariant());
query.bindValue(":sortOrder", cat.sortOrder); query.bindValue(":sortOrder", cat.sortOrder);
query.bindValue(":createdAt", QDateTime::currentDateTime()); query.bindValue(":createdAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
if (!query.exec()) { if (!query.exec()) {
qWarning() << "Failed to insert category:" << query.lastError().text(); qDebug() << "[CategoryDao] Insert failed:" << query.lastError().text();
return false; return false;
} }
cat.id = query.lastInsertId().toLongLong(); cat.id = query.lastInsertId().toLongLong();
return true; return true;
} }
@@ -32,26 +33,23 @@ bool CategoryDao::update(const Category& cat)
QSqlDatabase db = DatabaseManager::instance().database(); QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db); QSqlQuery query(db);
query.prepare( query.prepare(
"UPDATE Category SET " "UPDATE Category SET accountId = :accountId, name = :name, color = :color, "
"accountId = :accountId, " "parentCategoryId = :parentCategoryId, sortOrder = :sortOrder "
"name = :name, " "WHERE id = :id");
"color = :color, "
"parentCategoryId = :parentCategoryId, "
"sortOrder = :sortOrder "
"WHERE id = :id"
);
query.bindValue(":id", cat.id); query.bindValue(":id", cat.id);
query.bindValue(":accountId", cat.accountId >= 0 ? cat.accountId : QVariant()); query.bindValue(":accountId", cat.accountId >= 0 ? cat.accountId : QVariant());
query.bindValue(":name", cat.name); query.bindValue(":name", cat.name);
query.bindValue(":color", cat.color.name()); // Use default hex format query.bindValue(":color", cat.color.name(QColor::HexArgb));
query.bindValue(":parentCategoryId", cat.parentCategoryId >= 0 ? cat.parentCategoryId : QVariant()); query.bindValue(":parentCategoryId", cat.parentCategoryId >= 0 ? cat.parentCategoryId : QVariant());
query.bindValue(":sortOrder", cat.sortOrder); query.bindValue(":sortOrder", cat.sortOrder);
if (!query.exec()) { if (!query.exec()) {
qWarning() << "Failed to update category:" << query.lastError().text(); qDebug() << "[CategoryDao] Update failed:" << query.lastError().text();
return false; return false;
} }
return query.numRowsAffected() > 0;
return true;
} }
bool CategoryDao::remove(qint64 id) bool CategoryDao::remove(qint64 id)
@@ -60,12 +58,13 @@ bool CategoryDao::remove(qint64 id)
QSqlQuery query(db); QSqlQuery query(db);
query.prepare("DELETE FROM Category WHERE id = :id"); query.prepare("DELETE FROM Category WHERE id = :id");
query.bindValue(":id", id); query.bindValue(":id", id);
if (!query.exec()) { if (!query.exec()) {
qWarning() << "Failed to delete category:" << query.lastError().text(); qDebug() << "[CategoryDao] Remove failed:" << query.lastError().text();
return false; return false;
} }
return query.numRowsAffected() > 0;
return true;
} }
std::optional<Category> CategoryDao::findById(qint64 id) std::optional<Category> CategoryDao::findById(qint64 id)
@@ -74,19 +73,20 @@ std::optional<Category> CategoryDao::findById(qint64 id)
QSqlQuery query(db); QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE id = :id"); query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE id = :id");
query.bindValue(":id", id); query.bindValue(":id", id);
if (!query.exec() || !query.next()) { if (!query.exec() || !query.next()) {
return std::nullopt; return std::nullopt;
} }
Category cat; Category cat;
cat.id = query.value(0).toLongLong(); cat.id = query.value("id").toLongLong();
cat.accountId = query.value(1).toLongLong(); cat.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong();
cat.name = query.value(2).toString(); cat.name = query.value("name").toString();
cat.color = QColor(query.value(3).toString()); cat.color = QColor(query.value("color").toString());
cat.parentCategoryId = query.value(4).toLongLong(); cat.parentCategoryId = query.value("parentCategoryId").isNull() ? -1 : query.value("parentCategoryId").toLongLong();
cat.sortOrder = query.value(5).toInt(); cat.sortOrder = query.value("sortOrder").toInt();
cat.createdAt = query.value(6).toDateTime(); cat.createdAt = QDateTime::fromString(query.value("createdAt").toString(), Qt::ISODate);
return cat; return cat;
} }
@@ -94,24 +94,20 @@ QVector<Category> CategoryDao::findByAccount(qint64 accountId)
{ {
QSqlDatabase db = DatabaseManager::instance().database(); QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db); QSqlQuery query(db);
if (accountId >= 0) { query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE accountId = :accountId ORDER BY sortOrder, name");
query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE accountId = :accountId OR accountId IS NULL ORDER BY sortOrder, name"); query.bindValue(":accountId", accountId);
query.bindValue(":accountId", accountId);
} else {
query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE accountId IS NULL ORDER BY sortOrder, name");
}
QVector<Category> cats; QVector<Category> cats;
if (query.exec()) { if (query.exec()) {
while (query.next()) { while (query.next()) {
Category cat; Category cat;
cat.id = query.value(0).toLongLong(); cat.id = query.value("id").toLongLong();
cat.accountId = query.value(1).toLongLong(); cat.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong();
cat.name = query.value(2).toString(); cat.name = query.value("name").toString();
cat.color = QColor(query.value(3).toString()); cat.color = QColor(query.value("color").toString());
cat.parentCategoryId = query.value(4).toLongLong(); cat.parentCategoryId = query.value("parentCategoryId").isNull() ? -1 : query.value("parentCategoryId").toLongLong();
cat.sortOrder = query.value(5).toInt(); cat.sortOrder = query.value("sortOrder").toInt();
cat.createdAt = query.value(6).toDateTime(); cat.createdAt = QDateTime::fromString(query.value("createdAt").toString(), Qt::ISODate);
cats.append(cat); cats.append(cat);
} }
} }
@@ -120,28 +116,42 @@ QVector<Category> CategoryDao::findByAccount(qint64 accountId)
QVector<Category> CategoryDao::findGlobal() QVector<Category> CategoryDao::findGlobal()
{ {
return findByAccount(-1); QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.exec("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE accountId IS NULL ORDER BY sortOrder, name");
QVector<Category> cats;
while (query.next()) {
Category cat;
cat.id = query.value("id").toLongLong();
cat.accountId = -1;
cat.name = query.value("name").toString();
cat.color = QColor(query.value("color").toString());
cat.parentCategoryId = query.value("parentCategoryId").isNull() ? -1 : query.value("parentCategoryId").toLongLong();
cat.sortOrder = query.value("sortOrder").toInt();
cat.createdAt = QDateTime::fromString(query.value("createdAt").toString(), Qt::ISODate);
cats.append(cat);
}
return cats;
} }
QVector<Category> CategoryDao::findAll() QVector<Category> CategoryDao::findAll()
{ {
QSqlDatabase db = DatabaseManager::instance().database(); QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db); QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category ORDER BY accountId, sortOrder, name"); query.exec("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category ORDER BY sortOrder, name");
QVector<Category> cats; QVector<Category> cats;
if (query.exec()) { while (query.next()) {
while (query.next()) { Category cat;
Category cat; cat.id = query.value("id").toLongLong();
cat.id = query.value(0).toLongLong(); cat.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong();
cat.accountId = query.value(1).toLongLong(); cat.name = query.value("name").toString();
cat.name = query.value(2).toString(); cat.color = QColor(query.value("color").toString());
cat.color = QColor(query.value(3).toString()); cat.parentCategoryId = query.value("parentCategoryId").isNull() ? -1 : query.value("parentCategoryId").toLongLong();
cat.parentCategoryId = query.value(4).toLongLong(); cat.sortOrder = query.value("sortOrder").toInt();
cat.sortOrder = query.value(5).toInt(); cat.createdAt = QDateTime::fromString(query.value("createdAt").toString(), Qt::ISODate);
cat.createdAt = query.value(6).toDateTime(); cats.append(cat);
cats.append(cat);
}
} }
return cats; return cats;
} }
@@ -152,103 +162,88 @@ QVector<Category> CategoryDao::findChildren(qint64 parentId)
QSqlQuery query(db); QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE parentCategoryId = :parentId ORDER BY sortOrder, name"); query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE parentCategoryId = :parentId ORDER BY sortOrder, name");
query.bindValue(":parentId", parentId); query.bindValue(":parentId", parentId);
QVector<Category> cats; QVector<Category> cats;
if (query.exec()) { if (query.exec()) {
while (query.next()) { while (query.next()) {
Category cat; Category cat;
cat.id = query.value(0).toLongLong(); cat.id = query.value("id").toLongLong();
cat.accountId = query.value(1).toLongLong(); cat.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong();
cat.name = query.value(2).toString(); cat.name = query.value("name").toString();
cat.color = QColor(query.value(3).toString()); cat.color = QColor(query.value("color").toString());
cat.parentCategoryId = query.value(4).toLongLong(); cat.parentCategoryId = query.value("parentCategoryId").isNull() ? -1 : query.value("parentCategoryId").toLongLong();
cat.sortOrder = query.value(5).toInt(); cat.sortOrder = query.value("sortOrder").toInt();
cat.createdAt = query.value(6).toDateTime(); cat.createdAt = QDateTime::fromString(query.value("createdAt").toString(), Qt::ISODate);
cats.append(cat); cats.append(cat);
} }
} }
return cats; return cats;
} }
bool CategoryDao::assignToMail(qint64 mailCopyId, qint64 categoryId) bool CategoryDao::assignToMail(qint64 mailId, qint64 categoryId)
{ {
QSqlDatabase db = DatabaseManager::instance().database(); QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db); QSqlQuery query(db);
query.prepare( query.prepare("INSERT OR IGNORE INTO MailCategory (mailCopyId, categoryId, assignedAt) VALUES (:mailId, :categoryId, :assignedAt)");
"INSERT OR IGNORE INTO MailCategory (mailCopyId, categoryId, assignedAt) " query.bindValue(":mailId", mailId);
"VALUES (:mailCopyId, :categoryId, :assignedAt)"
);
query.bindValue(":mailCopyId", mailCopyId);
query.bindValue(":categoryId", categoryId); query.bindValue(":categoryId", categoryId);
query.bindValue(":assignedAt", QDateTime::currentDateTime()); query.bindValue(":assignedAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
if (!query.exec()) { if (!query.exec()) {
qWarning() << "Failed to assign category to mail:" << query.lastError().text(); qDebug() << "[CategoryDao] assignToMail failed:" << query.lastError().text();
return false; return false;
} }
return true; return true;
} }
bool CategoryDao::removeFromMail(qint64 mailCopyId, qint64 categoryId) bool CategoryDao::removeFromMail(qint64 mailId, qint64 categoryId)
{ {
QSqlDatabase db = DatabaseManager::instance().database(); QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db); QSqlQuery query(db);
query.prepare("DELETE FROM MailCategory WHERE mailCopyId = :mailCopyId AND categoryId = :categoryId"); query.prepare("DELETE FROM MailCategory WHERE mailCopyId = :mailId AND categoryId = :categoryId");
query.bindValue(":mailCopyId", mailCopyId); query.bindValue(":mailId", mailId);
query.bindValue(":categoryId", categoryId); query.bindValue(":categoryId", categoryId);
if (!query.exec()) { if (!query.exec()) {
qWarning() << "Failed to remove category from mail:" << query.lastError().text(); qDebug() << "[CategoryDao] removeFromMail failed:" << query.lastError().text();
return false; return false;
} }
return query.numRowsAffected() > 0; return true;
} }
QVector<qint64> CategoryDao::categoriesForMail(qint64 mailCopyId) QVector<qint64> CategoryDao::categoriesForMail(qint64 mailId)
{ {
QSqlDatabase db = DatabaseManager::instance().database(); QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db); QSqlQuery query(db);
query.prepare("SELECT categoryId FROM MailCategory WHERE mailCopyId = :mailCopyId"); query.prepare("SELECT categoryId FROM MailCategory WHERE mailCopyId = :mailId");
query.bindValue(":mailCopyId", mailCopyId); query.bindValue(":mailId", mailId);
QVector<qint64> ids; QVector<qint64> catIds;
if (query.exec()) { if (query.exec()) {
while (query.next()) { while (query.next()) {
ids.append(query.value(0).toLongLong()); catIds.append(query.value("categoryId").toLongLong());
} }
} }
return ids; return catIds;
} }
QVector<Category> CategoryDao::fullCategoriesForMail(qint64 mailCopyId) bool CategoryDao::reorder(const QVector<qint64>& ids)
{
QVector<qint64> ids = categoriesForMail(mailCopyId);
QVector<Category> cats;
for (qint64 id : ids) {
if (auto cat = findById(id)) {
cats.append(*cat);
}
}
return cats;
}
bool CategoryDao::reorder(const QVector<qint64>& idsInOrder)
{ {
QSqlDatabase db = DatabaseManager::instance().database(); QSqlDatabase db = DatabaseManager::instance().database();
db.transaction(); db.transaction();
QSqlQuery query(db); for (int i = 0; i < ids.size(); ++i) {
query.prepare("UPDATE Category SET sortOrder = :order WHERE id = :id"); QSqlQuery query(db);
query.prepare("UPDATE Category SET sortOrder = :sortOrder WHERE id = :id");
for (int i = 0; i < idsInOrder.size(); ++i) { query.bindValue(":sortOrder", i);
query.bindValue(":id", idsInOrder[i]); query.bindValue(":id", ids[i]);
query.bindValue(":order", i);
if (!query.exec()) { if (!query.exec()) {
db.rollback(); db.rollback();
qWarning() << "Failed to reorder category:" << query.lastError().text(); qDebug() << "[CategoryDao] reorder failed:" << query.lastError().text();
return false; return false;
} }
} }
return db.commit(); db.commit();
return true;
} }
+6 -22
View File
@@ -3,22 +3,7 @@
#include "../databasemanager.h" #include "../databasemanager.h"
#include <QVector> #include <QVector>
#include <optional> #include <optional>
#include <QColor> #include "common_structs.h"
#include <QDateTime>
struct Category
{
qint64 id{-1};
qint64 accountId{-1}; // -1 = global
QString name;
QColor color{QColor("#1976D2")};
qint64 parentCategoryId{-1};
int sortOrder{0};
QDateTime createdAt;
bool isGlobal() const { return accountId < 0; }
bool isValid() const { return id >= 0 && !name.isEmpty(); }
};
class CategoryDao class CategoryDao
{ {
@@ -27,13 +12,12 @@ public:
static bool update(const Category& cat); static bool update(const Category& cat);
static bool remove(qint64 id); static bool remove(qint64 id);
static std::optional<Category> findById(qint64 id); static std::optional<Category> findById(qint64 id);
static QVector<Category> findByAccount(qint64 accountId); // accountId = -1 for global static QVector<Category> findByAccount(qint64 accountId);
static QVector<Category> findGlobal(); static QVector<Category> findGlobal();
static QVector<Category> findAll(); static QVector<Category> findAll();
static QVector<Category> findChildren(qint64 parentId); static QVector<Category> findChildren(qint64 parentId);
static bool assignToMail(qint64 mailCopyId, qint64 categoryId); static bool assignToMail(qint64 mailId, qint64 categoryId);
static bool removeFromMail(qint64 mailCopyId, qint64 categoryId); static bool removeFromMail(qint64 mailId, qint64 categoryId);
static QVector<qint64> categoriesForMail(qint64 mailCopyId); static QVector<qint64> categoriesForMail(qint64 mailId); // Returns category IDs
static QVector<Category> fullCategoriesForMail(qint64 mailCopyId); static bool reorder(const QVector<qint64>& ids);
static bool reorder(const QVector<qint64>& idsInOrder);
}; };
+126
View File
@@ -0,0 +1,126 @@
#pragma once
#include <QDateTime>
#include <QVector>
#include <optional>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QColor>
struct TemplateVariable
{
QString name;
QString label;
QString defaultValue;
QString type; // "text", "email", "date", "select", "textarea"
QStringList options; // for select type
};
struct Template
{
qint64 id{-1};
qint64 accountId{-1}; // -1 = global
QString name;
QString subject;
QString bodyHtml;
QString bodyText;
QVector<TemplateVariable> variables;
bool isDefault{false};
QDateTime createdAt;
QDateTime updatedAt;
bool isGlobal() const { return accountId < 0; }
bool isValid() const { return id >= 0 && !name.isEmpty(); }
QJsonObject toJson() const;
static Template fromJson(const QJsonObject& obj);
};
struct Signature
{
qint64 id{-1};
qint64 accountId{-1}; // -1 = global
QString name;
QString html;
bool isDefault{false};
QDateTime createdAt;
QDateTime updatedAt;
bool isGlobal() const { return accountId < 0; }
bool isValid() const { return id >= 0 && !name.isEmpty(); }
};
struct Category
{
qint64 id{-1};
qint64 accountId{-1}; // -1 = global
QString name;
QColor color; // color
qint64 parentCategoryId{-1};
int sortOrder{0};
QDateTime createdAt;
bool isGlobal() const { return accountId < 0; }
bool isValid() const { return id >= 0 && !name.isEmpty(); }
};
struct RuleCondition
{
enum Field { From, To, Cc, Subject, Body, HasAttachment, Size, Date, Flagged, Read };
enum Operator { Contains, NotContains, Equals, NotEquals, StartsWith, EndsWith, Regex, GreaterThan, LessThan, Before, After };
Field field{From};
Operator op{Contains};
QString value;
bool caseSensitive{false};
bool match(const QString& text) const;
QJsonObject toJson() const;
static RuleCondition fromJson(const QJsonObject& obj);
};
struct RuleAction
{
enum Type {
MoveToFolder, // value = folderId
MarkAsRead, // value = "true"/"false"
MarkAsFlagged, // value = "true"/"false"
Delete, // no value
AssignCategory, // value = categoryId
RemoveCategory, // value = categoryId
ForwardTo, // value = email address
ReplyWithTemplate, // value = templateId
SetPriority, // value = "high"/"normal"/"low"
StopProcessing // no value (stop evaluating more rules)
};
Type type{MoveToFolder};
QString value;
QJsonObject toJson() const;
static RuleAction fromJson(const QJsonObject& obj);
};
struct Rule
{
qint64 id{-1};
qint64 accountId{-1}; // -1 = global
QString name;
QString description;
bool enabled{true};
int priority{100}; // lower = higher priority
bool matchAll{true}; // true = AND, false = OR
QVector<RuleCondition> conditions;
QVector<RuleAction> actions;
QDateTime createdAt;
QDateTime updatedAt;
QDateTime lastRun;
qint64 runCount{0};
bool isGlobal() const { return accountId < 0; }
bool isValid() const { return id >= 0 && !name.isEmpty(); }
QJsonObject toJson() const;
static Rule fromJson(const QJsonObject& obj);
};
+3 -63
View File
@@ -3,68 +3,7 @@
#include "../databasemanager.h" #include "../databasemanager.h"
#include <QVector> #include <QVector>
#include <optional> #include <optional>
#include <QJsonDocument> #include "common_structs.h"
#include <QJsonObject>
#include <QJsonArray>
struct RuleCondition
{
enum Field { From, To, Cc, Subject, Body, HasAttachment, Size, Date, Flagged, Read };
enum Operator { Contains, NotContains, Equals, NotEquals, StartsWith, EndsWith, Regex, GreaterThan, LessThan, Before, After };
Field field{From};
Operator op{Contains};
QString value;
bool caseSensitive{false};
bool match(const QString& text) const;
QJsonObject toJson() const;
static RuleCondition fromJson(const QJsonObject& obj);
};
struct RuleAction
{
enum Type {
MoveToFolder, // value = folderId
MarkAsRead, // value = "true"/"false"
MarkAsFlagged, // value = "true"/"false"
Delete, // no value
AssignCategory, // value = categoryId
RemoveCategory, // value = categoryId
ForwardTo, // value = email address
ReplyWithTemplate, // value = templateId
SetPriority, // value = "high"/"normal"/"low"
StopProcessing // no value (stop evaluating more rules)
};
Type type{MoveToFolder};
QString value;
QJsonObject toJson() const;
static RuleAction fromJson(const QJsonObject& obj);
};
struct Rule
{
qint64 id{-1};
qint64 accountId{-1}; // -1 = global
QString name;
QString description;
bool enabled{true};
int priority{100}; // lower = higher priority
bool matchAll{true}; // true = AND, false = OR
QVector<RuleCondition> conditions;
QVector<RuleAction> actions;
QDateTime createdAt;
QDateTime updatedAt;
QDateTime lastRun;
qint64 runCount{0};
bool isGlobal() const { return accountId < 0; }
bool isValid() const { return id >= 0 && !name.isEmpty(); }
QJsonObject toJson() const;
static Rule fromJson(const QJsonObject& obj);
};
class RuleDao class RuleDao
{ {
@@ -75,7 +14,8 @@ public:
static std::optional<Rule> findById(qint64 id); static std::optional<Rule> findById(qint64 id);
static QVector<Rule> findByAccount(qint64 accountId); // accountId = -1 for global static QVector<Rule> findByAccount(qint64 accountId); // accountId = -1 for global
static QVector<Rule> findGlobal(); static QVector<Rule> findGlobal();
static QVector<Rule> findAllEnabled(); static QVector<Rule> findAll();
static QVector<Rule> findAllEnabled(); // only enabled rules
static bool setEnabled(qint64 id, bool enabled); static bool setEnabled(qint64 id, bool enabled);
static bool updateLastRun(qint64 id, const QDateTime& when, qint64 runCount); static bool updateLastRun(qint64 id, const QDateTime& when, qint64 runCount);
}; };
+220
View File
@@ -0,0 +1,220 @@
#include "signaturedao.h"
#include <QSqlQuery>
#include <QSqlError>
#include <QDebug>
#include <QDateTime>
bool SignatureDao::insert(Signature& sig)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Signature (accountId, name, html, isDefault, createdAt, updatedAt) "
"VALUES (:accountId, :name, :html, :isDefault, :createdAt, :updatedAt)");
query.bindValue(":accountId", sig.accountId >= 0 ? sig.accountId : QVariant());
query.bindValue(":name", sig.name);
query.bindValue(":html", sig.html);
query.bindValue(":isDefault", sig.isDefault ? 1 : 0);
query.bindValue(":createdAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
query.bindValue(":updatedAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
if (!query.exec()) {
qDebug() << "[SignatureDao] Insert failed:" << query.lastError().text();
return false;
}
sig.id = query.lastInsertId().toLongLong();
return true;
}
bool SignatureDao::update(const Signature& sig)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE Signature SET accountId = :accountId, name = :name, html = :html, "
"isDefault = :isDefault, updatedAt = :updatedAt "
"WHERE id = :id");
query.bindValue(":id", sig.id);
query.bindValue(":accountId", sig.accountId >= 0 ? sig.accountId : QVariant());
query.bindValue(":name", sig.name);
query.bindValue(":html", sig.html);
query.bindValue(":isDefault", sig.isDefault ? 1 : 0);
query.bindValue(":updatedAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
if (!query.exec()) {
qDebug() << "[SignatureDao] Update failed:" << query.lastError().text();
return false;
}
return true;
}
bool SignatureDao::remove(qint64 id)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Signature WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qDebug() << "[SignatureDao] Remove failed:" << query.lastError().text();
return false;
}
return true;
}
std::optional<Signature> SignatureDao::findById(qint64 id)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, html, isDefault, createdAt, updatedAt FROM Signature WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec() || !query.next()) {
return std::nullopt;
}
Signature sig;
sig.id = query.value("id").toLongLong();
sig.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong();
sig.name = query.value("name").toString();
sig.html = query.value("html").toString();
sig.isDefault = query.value("isDefault").toBool();
sig.createdAt = QDateTime::fromString(query.value("createdAt").toString(), Qt::ISODate);
sig.updatedAt = QDateTime::fromString(query.value("updatedAt").toString(), Qt::ISODate);
return sig;
}
QVector<Signature> SignatureDao::findByAccount(qint64 accountId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (accountId >= 0) {
query.prepare("SELECT id, accountId, name, html, isDefault, createdAt, updatedAt FROM Signature WHERE accountId = :accountId OR accountId IS NULL ORDER BY name");
query.bindValue(":accountId", accountId);
} else {
query.prepare("SELECT id, accountId, name, html, isDefault, createdAt, updatedAt FROM Signature WHERE accountId IS NULL ORDER BY name");
}
QVector<Signature> sigs;
if (query.exec()) {
while (query.next()) {
Signature sig;
sig.id = query.value("id").toLongLong();
sig.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong();
sig.name = query.value("name").toString();
sig.html = query.value("html").toString();
sig.isDefault = query.value("isDefault").toBool();
sig.createdAt = QDateTime::fromString(query.value("createdAt").toString(), Qt::ISODate);
sig.updatedAt = QDateTime::fromString(query.value("updatedAt").toString(), Qt::ISODate);
sigs.append(sig);
}
}
return sigs;
}
QVector<Signature> SignatureDao::findGlobal()
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.exec("SELECT id, accountId, name, html, isDefault, createdAt, updatedAt FROM Signature WHERE accountId IS NULL ORDER BY name");
QVector<Signature> sigs;
while (query.next()) {
Signature sig;
sig.id = query.value("id").toLongLong();
sig.accountId = -1;
sig.name = query.value("name").toString();
sig.html = query.value("html").toString();
sig.isDefault = query.value("isDefault").toBool();
sig.createdAt = QDateTime::fromString(query.value("createdAt").toString(), Qt::ISODate);
sig.updatedAt = QDateTime::fromString(query.value("updatedAt").toString(), Qt::ISODate);
sigs.append(sig);
}
return sigs;
}
QVector<Signature> SignatureDao::findAll()
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.exec("SELECT id, accountId, name, html, isDefault, createdAt, updatedAt FROM Signature ORDER BY name");
QVector<Signature> sigs;
while (query.next()) {
Signature sig;
sig.id = query.value("id").toLongLong();
sig.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong();
sig.name = query.value("name").toString();
sig.html = query.value("html").toString();
sig.isDefault = query.value("isDefault").toBool();
sig.createdAt = QDateTime::fromString(query.value("createdAt").toString(), Qt::ISODate);
sig.updatedAt = QDateTime::fromString(query.value("updatedAt").toString(), Qt::ISODate);
sigs.append(sig);
}
return sigs;
}
std::optional<Signature> SignatureDao::findDefault(qint64 accountId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"SELECT id, accountId, name, html, isDefault, createdAt, updatedAt "
"FROM Signature "
"WHERE (accountId = :accountId OR accountId IS NULL) AND isDefault = 1 "
"ORDER BY accountId DESC LIMIT 1");
query.bindValue(":accountId", accountId >= 0 ? accountId : QVariant());
if (!query.exec() || !query.next()) {
return std::nullopt;
}
Signature sig;
sig.id = query.value("id").toLongLong();
sig.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong();
sig.name = query.value("name").toString();
sig.html = query.value("html").toString();
sig.isDefault = query.value("isDefault").toBool();
sig.createdAt = QDateTime::fromString(query.value("createdAt").toString(), Qt::ISODate);
sig.updatedAt = QDateTime::fromString(query.value("updatedAt").toString(), Qt::ISODate);
return sig;
}
bool SignatureDao::setDefault(qint64 id, bool isDefault)
{
QSqlDatabase db = DatabaseManager::instance().database();
db.transaction();
// First unset all defaults for the same account
QSqlQuery query(db);
query.prepare(
"UPDATE Signature SET isDefault = 0 WHERE accountId = "
"(SELECT accountId FROM Signature WHERE id = :id)");
query.bindValue(":id", id);
if (!query.exec()) {
db.rollback();
return false;
}
// Set the new default
query.prepare("UPDATE Signature SET isDefault = :isDefault, updatedAt = :updatedAt WHERE id = :id");
query.bindValue(":id", id);
query.bindValue(":isDefault", isDefault ? 1 : 0);
query.bindValue(":updatedAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
if (!query.exec()) {
db.rollback();
qDebug() << "[SignatureDao] setDefault failed:" << query.lastError().text();
return false;
}
db.commit();
return true;
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "../databasemanager.h"
#include <QVector>
#include <optional>
#include "common_structs.h"
class SignatureDao
{
public:
static bool insert(Signature& sig);
static bool update(const Signature& sig);
static bool remove(qint64 id);
static std::optional<Signature> findById(qint64 id);
static QVector<Signature> findByAccount(qint64 accountId);
static QVector<Signature> findGlobal();
static QVector<Signature> findAll();
static std::optional<Signature> findDefault(qint64 accountId);
static bool setDefault(qint64 id, bool isDefault);
};
+1 -29
View File
@@ -6,35 +6,7 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
#include "common_structs.h"
struct TemplateVariable
{
QString name;
QString label;
QString defaultValue;
QString type; // "text", "email", "date", "select", "textarea"
QStringList options; // for select type
};
struct Template
{
qint64 id{-1};
qint64 accountId{-1}; // -1 = global
QString name;
QString subject;
QString bodyHtml;
QString bodyText;
QVector<TemplateVariable> variables;
bool isDefault{false};
QDateTime createdAt;
QDateTime updatedAt;
bool isGlobal() const { return accountId < 0; }
bool isValid() const { return id >= 0 && !name.isEmpty(); }
QJsonObject toJson() const;
static Template fromJson(const QJsonObject& obj);
};
class TemplateDao class TemplateDao
{ {
+43 -42
View File
@@ -30,9 +30,51 @@ ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
void ReaderView::setupUI() { void ReaderView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this); QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(12, 0, 12, 0); mainLayout->setContentsMargins(10, 0, 10, 0);
mainLayout->setSpacing(0); mainLayout->setSpacing(0);
// ===== Subject Section (independent, with shadow) =====
QFrame *subjectFrame = new QFrame();
subjectFrame->setObjectName("SubjectFrame");
subjectFrame->setStyleSheet(
"QFrame#SubjectFrame {"
" background: white;"
" border: none;"
" border: 1px solid #e0e0e0;"
" border-radius: 4px;"
"}"
);
// Add shadow effect
QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setBlurRadius(22);
shadowEffect->setOffset(0, 6);
shadowEffect->setColor(QColor(0, 0, 0, 40));
subjectFrame->setGraphicsEffect(shadowEffect);
QVBoxLayout *subjectLayout = new QVBoxLayout(subjectFrame);
subjectLayout->setContentsMargins(16, 16, 16, 16);
subjectLayout->setSpacing(8);
QHBoxLayout *subjectRow = new QHBoxLayout();
subjectRow->setSpacing(12);
m_subjectLabel = new QLabel();
m_subjectLabel->setText("(Sin asunto)");
QFont subjectFont = m_subjectLabel->font();
subjectFont.setBold(true);
subjectFont.setPointSize(12);
m_subjectLabel->setFont(subjectFont);
m_subjectLabel->setWordWrap(true);
m_subjectLabel->setStyleSheet("color: #1d1d1f;");
m_subjectLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
subjectRow->addWidget(m_subjectLabel, 1);
subjectLayout->addLayout(subjectRow);
mainLayout->addWidget(subjectFrame);
// Add spacing after subject frame so shadow is visible
mainLayout->addSpacing(12);
// ===== Toolbar (zoom, find, images) ===== // ===== Toolbar (zoom, find, images) =====
m_toolbar = new QFrame(); m_toolbar = new QFrame();
m_toolbar->setFixedHeight(36); m_toolbar->setFixedHeight(36);
@@ -183,47 +225,6 @@ void ReaderView::setupUI() {
mainLayout->addWidget(m_toolbar); mainLayout->addWidget(m_toolbar);
mainLayout->addWidget(m_findBar); mainLayout->addWidget(m_findBar);
// ===== Subject Section (independent, with shadow) =====
QFrame *subjectFrame = new QFrame();
subjectFrame->setObjectName("SubjectFrame");
subjectFrame->setStyleSheet(
"QFrame#SubjectFrame {"
" background: white;"
" border: none;"
" border: 1px solid #e0e0e0;"
" border-radius: 10px;"
"}"
);
// Add shadow effect
QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setBlurRadius(8);
shadowEffect->setOffset(0, 3);
shadowEffect->setColor(QColor(0, 0, 0, 40));
subjectFrame->setGraphicsEffect(shadowEffect);
QVBoxLayout *subjectLayout = new QVBoxLayout(subjectFrame);
subjectLayout->setContentsMargins(16, 16, 16, 16);
subjectLayout->setSpacing(8);
QHBoxLayout *subjectRow = new QHBoxLayout();
subjectRow->setSpacing(12);
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);
subjectLayout->addLayout(subjectRow);
mainLayout->addWidget(subjectFrame);
// Add spacing after subject frame so shadow is visible
mainLayout->addSpacing(12);
// ===== Header Section ===== // ===== Header Section =====
m_headerWidget = new QWidget(); m_headerWidget = new QWidget();
m_headerWidget->setStyleSheet("QWidget { background: white; border-bottom: 1px solid #e0e0e0; }"); m_headerWidget->setStyleSheet("QWidget { background: white; border-bottom: 1px solid #e0e0e0; }");
+734 -27
View File
@@ -1,6 +1,14 @@
#include "ui/settingsview.h" #include "settingsview.h"
#include "services/accountservice.h" #include "services/accountservice.h"
#include "core/models/account.h" #include "core/models/account.h"
#include "db/dao/common_structs.h"
#include "db/dao/categorydao.h"
#include "db/dao/ruledao.h"
#include "db/dao/templatedao.h"
#include "db/dao/signaturedao.h"
#include "ui/ruleditordialog.h"
#include "ui/templateeditordialog.h"
#include "ui/signaturemanagerdialog.h"
#include <QDebug> #include <QDebug>
#include <QMessageBox> #include <QMessageBox>
@@ -13,6 +21,19 @@
#include <QButtonGroup> #include <QButtonGroup>
#include <QComboBox> #include <QComboBox>
#include <QCheckBox> #include <QCheckBox>
#include <QRadioButton>
#include <QTreeWidget>
#include <QLineEdit>
#include <QDialog>
#include <QColorDialog>
#include <QInputDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QScrollArea>
#include <QGroupBox>
#include <QSqlQuery>
#include <QSqlDatabase>
#include <QHeaderView>
SettingsView::SettingsView(QWidget *parent) : QWidget(parent) { SettingsView::SettingsView(QWidget *parent) : QWidget(parent) {
setupUI(); setupUI();
@@ -23,9 +44,13 @@ void SettingsView::setupUI() {
mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->setContentsMargins(0, 0, 0, 0);
m_tabWidget = new QTabWidget(); m_tabWidget = new QTabWidget();
m_tabWidget->addTab(createAccountsTab(), "Accounts"); m_tabWidget->addTab(createAccountsTab(), tr("Accounts"));
m_tabWidget->addTab(createGeneralTab(), "General"); m_tabWidget->addTab(createCategoriesTab(), tr("Categories"));
m_tabWidget->addTab(createAppearanceTab(), "Appearance"); m_tabWidget->addTab(createRulesTab(), tr("Rules"));
m_tabWidget->addTab(createTemplatesTab(), tr("Templates"));
m_tabWidget->addTab(createSignaturesTab(), tr("Signatures"));
m_tabWidget->addTab(createGeneralTab(), tr("General"));
m_tabWidget->addTab(createAppearanceTab(), tr("Appearance"));
mainLayout->addWidget(m_tabWidget); mainLayout->addWidget(m_tabWidget);
} }
@@ -35,9 +60,10 @@ QWidget* SettingsView::createAccountsTab() {
QVBoxLayout *layout = new QVBoxLayout(widget); QVBoxLayout *layout = new QVBoxLayout(widget);
layout->setContentsMargins(20, 20, 20, 20); layout->setContentsMargins(20, 20, 20, 20);
QLabel *title = new QLabel("Email Accounts"); QLabel *title = new QLabel(tr("Email Accounts"));
QFont titleFont = title->font(); QFont titleFont = title->font();
titleFont.setPointSize(16); titleFont.setPointSize(16);
titleFont.setBold(true);
title->setFont(titleFont); title->setFont(titleFont);
layout->addWidget(title); layout->addWidget(title);
@@ -51,21 +77,22 @@ QWidget* SettingsView::createAccountsTab() {
layout->addWidget(m_accountList); layout->addWidget(m_accountList);
QHBoxLayout *buttonLayout = new QHBoxLayout(); QHBoxLayout *buttonLayout = new QHBoxLayout();
m_addBtn = new QPushButton("Add Account"); m_addBtn = new QPushButton(tr("Add Account"));
m_addBtn->setStyleSheet( m_addBtn->setStyleSheet(
"QPushButton { background: #FFEB3B; color: white; border: none; border-radius: 4px; " "QPushButton { background: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }" "padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #FDD835; }" "QPushButton:hover { background: #1565C0; }"
); );
buttonLayout->addWidget(m_addBtn); buttonLayout->addWidget(m_addBtn);
m_editBtn = new QPushButton("Edit");
m_editBtn = new QPushButton(tr("Edit"));
m_editBtn->setEnabled(false); m_editBtn->setEnabled(false);
m_editBtn->setStyleSheet( m_editBtn->setStyleSheet(
"QPushButton { background: #FFA726; color: white; border: none; border-radius: 4px; " "QPushButton { background: #FFA726; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }" "padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #FB8C00; }" "QPushButton:hover { background: #FB8C00; }"
); );
m_deleteBtn = new QPushButton("Delete"); m_deleteBtn = new QPushButton(tr("Delete"));
m_deleteBtn->setEnabled(false); m_deleteBtn->setEnabled(false);
m_deleteBtn->setStyleSheet( m_deleteBtn->setStyleSheet(
"QPushButton { background: #EF5350; color: white; border: none; border-radius: 4px; " "QPushButton { background: #EF5350; color: white; border: none; border-radius: 4px; "
@@ -85,32 +112,283 @@ QWidget* SettingsView::createAccountsTab() {
return widget; return widget;
} }
QWidget* SettingsView::createCategoriesTab() {
QWidget *widget = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(widget);
layout->setContentsMargins(12, 12, 12, 12);
layout->setSpacing(8);
QLabel *title = new QLabel(tr("Categories"));
QFont titleFont = title->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
title->setFont(titleFont);
layout->addWidget(title);
m_categoryTree = new QTreeWidget();
m_categoryTree->setHeaderLabels({tr("Category"), tr("Color"), tr("Account")});
m_categoryTree->setSelectionMode(QAbstractItemView::SingleSelection);
m_categoryTree->setRootIsDecorated(true);
m_categoryTree->setItemsExpandable(true);
m_categoryTree->setStyleSheet(
"QTreeWidget { background: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; }"
"QTreeWidget::item { padding: 8px; border-bottom: 1px solid #f0f0f0; }"
"QTreeWidget::item:selected { background: #e3f2fd; }"
);
m_categoryTree->setColumnWidth(0, 250);
m_categoryTree->setColumnWidth(1, 80);
m_categoryTree->header()->setStretchLastSection(false);
layout->addWidget(m_categoryTree);
QHBoxLayout *buttonLayout = new QHBoxLayout();
m_categoryAddBtn = new QPushButton(tr("New Category"));
m_categoryAddBtn->setStyleSheet(
"QPushButton { background: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #1565C0; }"
);
m_categoryEditBtn = new QPushButton(tr("Edit"));
m_categoryEditBtn->setEnabled(false);
m_categoryEditBtn->setStyleSheet(
"QPushButton { background: #FFA726; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #FB8C00; }"
);
m_categoryDeleteBtn = new QPushButton(tr("Delete"));
m_categoryDeleteBtn->setEnabled(false);
m_categoryDeleteBtn->setStyleSheet(
"QPushButton { background: #EF5350; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #E53935; }"
);
buttonLayout->addWidget(m_categoryAddBtn);
buttonLayout->addWidget(m_categoryEditBtn);
buttonLayout->addWidget(m_categoryDeleteBtn);
buttonLayout->addStretch();
layout->addLayout(buttonLayout);
connect(m_categoryTree, &QTreeWidget::itemSelectionChanged, this, &SettingsView::onCategorySelectionChanged);
connect(m_categoryAddBtn, &QPushButton::clicked, this, &SettingsView::onCategoryAddClicked);
connect(m_categoryEditBtn, &QPushButton::clicked, this, &SettingsView::onCategoryEditClicked);
connect(m_categoryDeleteBtn, &QPushButton::clicked, this, &SettingsView::onCategoryDeleteClicked);
return widget;
}
QWidget* SettingsView::createRulesTab() {
QWidget *widget = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(widget);
layout->setContentsMargins(12, 12, 12, 12);
layout->setSpacing(8);
QLabel *title = new QLabel(tr("Rules"));
QFont titleFont = title->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
title->setFont(titleFont);
layout->addWidget(title);
m_ruleList = new QListWidget();
m_ruleList->setSelectionMode(QAbstractItemView::SingleSelection);
m_ruleList->setStyleSheet(
"QListWidget { background: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; }"
"QListWidget::item { padding: 12px; border-bottom: 1px solid #f0f0f0; }"
"QListWidget::item:selected { background: #e3f2fd; }"
);
m_ruleList->setSpacing(2);
layout->addWidget(m_ruleList);
QHBoxLayout *buttonLayout = new QHBoxLayout();
m_ruleAddBtn = new QPushButton(tr("New Rule"));
m_ruleAddBtn->setStyleSheet(
"QPushButton { background: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #1565C0; }"
);
m_ruleEditBtn = new QPushButton(tr("Edit"));
m_ruleEditBtn->setEnabled(false);
m_ruleEditBtn->setStyleSheet(
"QPushButton { background: #FFA726; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #FB8C00; }"
);
m_ruleDeleteBtn = new QPushButton(tr("Delete"));
m_ruleDeleteBtn->setEnabled(false);
m_ruleDeleteBtn->setStyleSheet(
"QPushButton { background: #EF5350; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #E53935; }"
);
m_ruleToggleBtn = new QPushButton(tr("Toggle"));
m_ruleToggleBtn->setEnabled(false);
m_ruleToggleBtn->setStyleSheet(
"QPushButton { background: #66BB6A; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #4CAF50; }"
);
buttonLayout->addWidget(m_ruleAddBtn);
buttonLayout->addWidget(m_ruleEditBtn);
buttonLayout->addWidget(m_ruleDeleteBtn);
buttonLayout->addWidget(m_ruleToggleBtn);
buttonLayout->addStretch();
layout->addLayout(buttonLayout);
connect(m_ruleList, &QListWidget::itemSelectionChanged, this, &SettingsView::onRuleSelectionChanged);
connect(m_ruleAddBtn, &QPushButton::clicked, this, &SettingsView::onRuleAddClicked);
connect(m_ruleEditBtn, &QPushButton::clicked, this, &SettingsView::onRuleEditClicked);
connect(m_ruleDeleteBtn, &QPushButton::clicked, this, &SettingsView::onRuleDeleteClicked);
connect(m_ruleToggleBtn, &QPushButton::clicked, this, &SettingsView::onRuleToggleEnabled);
return widget;
}
QWidget* SettingsView::createTemplatesTab() {
QWidget *widget = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(widget);
layout->setContentsMargins(12, 12, 12, 12);
layout->setSpacing(8);
QLabel *title = new QLabel(tr("Templates"));
QFont titleFont = title->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
title->setFont(titleFont);
layout->addWidget(title);
m_templateList = new QListWidget();
m_templateList->setSelectionMode(QAbstractItemView::SingleSelection);
m_templateList->setStyleSheet(
"QListWidget { background: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; }"
"QListWidget::item { padding: 12px; border-bottom: 1px solid #f0f0f0; }"
"QListWidget::item:selected { background: #e3f2fd; }"
);
m_templateList->setSpacing(2);
layout->addWidget(m_templateList);
QHBoxLayout *buttonLayout = new QHBoxLayout();
m_templateAddBtn = new QPushButton(tr("New Template"));
m_templateAddBtn->setStyleSheet(
"QPushButton { background: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #1565C0; }"
);
m_templateEditBtn = new QPushButton(tr("Edit"));
m_templateEditBtn->setEnabled(false);
m_templateEditBtn->setStyleSheet(
"QPushButton { background: #FFA726; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #FB8C00; }"
);
m_templateDeleteBtn = new QPushButton(tr("Delete"));
m_templateDeleteBtn->setEnabled(false);
m_templateDeleteBtn->setStyleSheet(
"QPushButton { background: #EF5350; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #E53935; }"
);
buttonLayout->addWidget(m_templateAddBtn);
buttonLayout->addWidget(m_templateEditBtn);
buttonLayout->addWidget(m_templateDeleteBtn);
buttonLayout->addStretch();
layout->addLayout(buttonLayout);
connect(m_templateList, &QListWidget::itemSelectionChanged, this, &SettingsView::onTemplateSelectionChanged);
connect(m_templateAddBtn, &QPushButton::clicked, this, &SettingsView::onTemplateAddClicked);
connect(m_templateEditBtn, &QPushButton::clicked, this, &SettingsView::onTemplateEditClicked);
connect(m_templateDeleteBtn, &QPushButton::clicked, this, &SettingsView::onTemplateDeleteClicked);
return widget;
}
QWidget* SettingsView::createSignaturesTab() {
QWidget *widget = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(widget);
layout->setContentsMargins(12, 12, 12, 12);
layout->setSpacing(8);
QLabel *title = new QLabel(tr("Signatures"));
QFont titleFont = title->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
title->setFont(titleFont);
layout->addWidget(title);
m_signatureList = new QListWidget();
m_signatureList->setSelectionMode(QAbstractItemView::SingleSelection);
m_signatureList->setStyleSheet(
"QListWidget { background: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; }"
"QListWidget::item { padding: 12px; border-bottom: 1px solid #f0f0f0; }"
"QListWidget::item:selected { background: #e3f2fd; }"
);
m_signatureList->setSpacing(2);
layout->addWidget(m_signatureList);
QHBoxLayout *buttonLayout = new QHBoxLayout();
m_signatureAddBtn = new QPushButton(tr("New Signature"));
m_signatureAddBtn->setStyleSheet(
"QPushButton { background: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #1565C0; }"
);
m_signatureEditBtn = new QPushButton(tr("Edit"));
m_signatureEditBtn->setEnabled(false);
m_signatureEditBtn->setStyleSheet(
"QPushButton { background: #FFA726; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #FB8C00; }"
);
m_signatureDeleteBtn = new QPushButton(tr("Delete"));
m_signatureDeleteBtn->setEnabled(false);
m_signatureDeleteBtn->setStyleSheet(
"QPushButton { background: #EF5350; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #E53935; }"
);
buttonLayout->addWidget(m_signatureAddBtn);
buttonLayout->addWidget(m_signatureEditBtn);
buttonLayout->addWidget(m_signatureDeleteBtn);
buttonLayout->addStretch();
layout->addLayout(buttonLayout);
connect(m_signatureList, &QListWidget::itemSelectionChanged, this, &SettingsView::onSignatureSelectionChanged);
connect(m_signatureAddBtn, &QPushButton::clicked, this, &SettingsView::onSignatureAddClicked);
connect(m_signatureEditBtn, &QPushButton::clicked, this, &SettingsView::onSignatureEditClicked);
connect(m_signatureDeleteBtn, &QPushButton::clicked, this, &SettingsView::onSignatureDeleteClicked);
return widget;
}
QWidget* SettingsView::createGeneralTab() { QWidget* SettingsView::createGeneralTab() {
QWidget *widget = new QWidget(); QWidget *widget = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(widget); QVBoxLayout *layout = new QVBoxLayout(widget);
layout->setContentsMargins(20, 20, 20, 20); layout->setContentsMargins(20, 20, 20, 20);
QLabel *title = new QLabel("General Settings"); QLabel *title = new QLabel(tr("General Settings"));
QFont titleFont = title->font(); QFont titleFont = title->font();
titleFont.setPointSize(16); titleFont.setPointSize(16);
titleFont.setBold(true);
title->setFont(titleFont); title->setFont(titleFont);
layout->addWidget(title); layout->addWidget(title);
QCheckBox *startOnLogin = new QCheckBox("Start application on login"); QCheckBox *startOnLogin = new QCheckBox(tr("Start application on login"));
startOnLogin->setChecked(true); startOnLogin->setChecked(true);
connect(startOnLogin, &QCheckBox::toggled, this, [this](bool checked) { connect(startOnLogin, &QCheckBox::toggled, this, [this](bool checked) {
emit settingChanged("start_on_login", QVariant(checked)); emit settingChanged("start_on_login", QVariant(checked));
}); });
layout->addWidget(startOnLogin); layout->addWidget(startOnLogin);
QCheckBox *enableNotifications = new QCheckBox("Enable notifications"); QCheckBox *enableNotifications = new QCheckBox(tr("Enable notifications"));
enableNotifications->setChecked(true); enableNotifications->setChecked(true);
connect(enableNotifications, &QCheckBox::toggled, this, [this](bool checked) { connect(enableNotifications, &QCheckBox::toggled, this, [this](bool checked) {
emit settingChanged("enable_notifications", QVariant(checked)); emit settingChanged("enable_notifications", QVariant(checked));
}); });
layout->addWidget(enableNotifications); layout->addWidget(enableNotifications);
QCheckBox *minimizeToTray = new QCheckBox("Minimize to tray on close"); QCheckBox *minimizeToTray = new QCheckBox(tr("Minimize to tray on close"));
minimizeToTray->setChecked(true); minimizeToTray->setChecked(true);
connect(minimizeToTray, &QCheckBox::toggled, this, [this](bool checked) { connect(minimizeToTray, &QCheckBox::toggled, this, [this](bool checked) {
emit settingChanged("minimize_to_tray", QVariant(checked)); emit settingChanged("minimize_to_tray", QVariant(checked));
@@ -126,22 +404,23 @@ QWidget* SettingsView::createAppearanceTab() {
QVBoxLayout *layout = new QVBoxLayout(widget); QVBoxLayout *layout = new QVBoxLayout(widget);
layout->setContentsMargins(20, 20, 20, 20); layout->setContentsMargins(20, 20, 20, 20);
QLabel *title = new QLabel("Appearance"); QLabel *title = new QLabel(tr("Appearance"));
QFont titleFont = title->font(); QFont titleFont = title->font();
titleFont.setPointSize(16); titleFont.setPointSize(16);
titleFont.setBold(true);
title->setFont(titleFont); title->setFont(titleFont);
layout->addWidget(title); layout->addWidget(title);
QLabel *themeLabel = new QLabel("Theme"); QLabel *themeLabel = new QLabel(tr("Theme"));
QFont labelFont = themeLabel->font(); QFont labelFont = themeLabel->font();
labelFont.setBold(true); labelFont.setBold(true);
themeLabel->setFont(labelFont); themeLabel->setFont(labelFont);
layout->addWidget(themeLabel); layout->addWidget(themeLabel);
m_themeGroup = new QButtonGroup(this); m_themeGroup = new QButtonGroup(this);
QRadioButton *lightRadio = new QRadioButton("Light"); QRadioButton *lightRadio = new QRadioButton(tr("Light"));
QRadioButton *darkRadio = new QRadioButton("Dark"); QRadioButton *darkRadio = new QRadioButton(tr("Dark"));
QRadioButton *systemRadio = new QRadioButton("System"); QRadioButton *systemRadio = new QRadioButton(tr("System"));
m_themeGroup->addButton(lightRadio, 0); m_themeGroup->addButton(lightRadio, 0);
m_themeGroup->addButton(darkRadio, 1); m_themeGroup->addButton(darkRadio, 1);
m_themeGroup->addButton(systemRadio, 2); m_themeGroup->addButton(systemRadio, 2);
@@ -155,14 +434,14 @@ QWidget* SettingsView::createAppearanceTab() {
connect(m_themeGroup, QOverload<int>::of(&QButtonGroup::idClicked), this, [this](int id) { connect(m_themeGroup, QOverload<int>::of(&QButtonGroup::idClicked), this, [this](int id) {
QString theme; QString theme;
switch (id) { switch (id) {
case 0: theme = "light"; break; case 0: theme = "light"; break;
case 1: theme = "dark"; break; case 1: theme = "dark"; break;
case 2: theme = "system"; break; case 2: theme = "system"; break;
} }
emit themeChanged(theme); emit themeChanged(theme);
}); });
QCheckBox *deepinTheme = new QCheckBox("Deepin theme (experimental)"); QCheckBox *deepinTheme = new QCheckBox(tr("Deepin theme (experimental)"));
deepinTheme->setChecked(true); deepinTheme->setChecked(true);
connect(deepinTheme, &QCheckBox::toggled, this, [this](bool checked) { connect(deepinTheme, &QCheckBox::toggled, this, [this](bool checked) {
emit settingChanged("deepin_theme", QVariant(checked)); emit settingChanged("deepin_theme", QVariant(checked));
@@ -173,6 +452,7 @@ QWidget* SettingsView::createAppearanceTab() {
return widget; return widget;
} }
// ========== Slots for Accounts ==========
void SettingsView::setAccountService(AccountService *service) { void SettingsView::setAccountService(AccountService *service) {
m_accountService = service; m_accountService = service;
if (m_accountService) { if (m_accountService) {
@@ -204,8 +484,8 @@ void SettingsView::loadAccounts() {
m_accountList->setCurrentRow(0); m_accountList->setCurrentRow(0);
} }
} }
void SettingsView::onAccountSelectionChanged()
{ void SettingsView::onAccountSelectionChanged() {
QList<QListWidgetItem*> items = m_accountList->selectedItems(); QList<QListWidgetItem*> items = m_accountList->selectedItems();
qDebug() << "SettingsView::onAccountSelectionChanged, selected items count:" << items.size(); qDebug() << "SettingsView::onAccountSelectionChanged, selected items count:" << items.size();
if (items.isEmpty()) { if (items.isEmpty()) {
@@ -231,10 +511,10 @@ void SettingsView::onEditClicked() {
void SettingsView::onDeleteClicked() { void SettingsView::onDeleteClicked() {
if (m_selectedAccountId != -1 && m_accountService) { if (m_selectedAccountId != -1 && m_accountService) {
QMessageBox msgBox(this); QMessageBox msgBox(this);
msgBox.setWindowTitle("Delete Account"); msgBox.setWindowTitle(tr("Delete Account"));
Account* account = m_accountService->findAccountById(m_selectedAccountId); Account* account = m_accountService->findAccountById(m_selectedAccountId);
QString accountName = account ? account->email() : QString::number(m_selectedAccountId); QString accountName = account ? account->email() : QString::number(m_selectedAccountId);
msgBox.setText(QString("Are you sure you want to delete the account \"%1\"?").arg(accountName)); msgBox.setText(tr("Are you sure you want to delete the account \"%1\"?").arg(accountName));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No);
if (msgBox.exec() == QMessageBox::Yes) { if (msgBox.exec() == QMessageBox::Yes) {
@@ -242,3 +522,430 @@ void SettingsView::onDeleteClicked() {
} }
} }
} }
// ========== Category Slots ==========
void SettingsView::onCategorySelectionChanged() {
bool hasSelection = !m_categoryTree->selectedItems().isEmpty();
m_categoryEditBtn->setEnabled(hasSelection);
m_categoryDeleteBtn->setEnabled(hasSelection);
}
void SettingsView::onCategoryAddClicked() {
QDialog dlg(this);
dlg.setWindowTitle(tr("New Category"));
dlg.setMinimumWidth(400);
QVBoxLayout *layout = new QVBoxLayout(&dlg);
QFormLayout *form = new QFormLayout();
QLineEdit *nameEdit = new QLineEdit();
form->addRow(tr("Name:"), nameEdit);
QPushButton *colorBtn = new QPushButton();
colorBtn->setFixedSize(24, 24);
QColor catColor = QColor("#1976D2");
colorBtn->setStyleSheet(QString("background-color: %1; border: 1px solid #ccc; border-radius: 4px;").arg(catColor.name()));
connect(colorBtn, &QPushButton::clicked, [&catColor, colorBtn]() {
QColor c = QColorDialog::getColor(catColor);
if (c.isValid()) {
catColor = c;
colorBtn->setStyleSheet(QString("background-color: %1; border: 1px solid #ccc; border-radius: 4px;").arg(c.name()));
}
});
QHBoxLayout *colorLayout = new QHBoxLayout();
colorLayout->addWidget(colorBtn);
colorLayout->addStretch();
form->addRow(tr("Color:"), colorLayout);
QComboBox *parentCombo = new QComboBox();
parentCombo->addItem(tr("(None - Top Level)"), QVariant());
form->addRow(tr("Parent:"), parentCombo);
layout->addLayout(form);
QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
layout->addWidget(buttons);
if (dlg.exec() == QDialog::Accepted) {
Category cat;
cat.name = nameEdit->text().trimmed();
cat.color = catColor;
cat.accountId = -1; // Global by default
cat.parentCategoryId = parentCombo->currentData().toLongLong();
if (!cat.name.isEmpty()) {
emit categoryAddRequested(cat);
}
}
}
void SettingsView::onCategoryEditClicked() {
QTreeWidgetItem *item = m_categoryTree->currentItem();
if (!item) return;
qint64 catId = item->data(0, Qt::UserRole).toLongLong();
std::optional<Category> catOpt = CategoryDao::findById(catId);
if (!catOpt.has_value()) return;
Category cat = *catOpt;
QDialog dlg(this);
dlg.setWindowTitle(tr("Edit Category"));
dlg.setMinimumWidth(400);
QVBoxLayout *layout = new QVBoxLayout(&dlg);
QFormLayout *form = new QFormLayout();
QLineEdit *nameEdit = new QLineEdit(cat.name);
form->addRow(tr("Name:"), nameEdit);
QPushButton *colorBtn = new QPushButton();
colorBtn->setFixedSize(24, 24);
colorBtn->setStyleSheet(QString("background-color: %1; border: 1px solid #ccc; border-radius: 4px;").arg(cat.color.name()));
QColor catColor = cat.color;
connect(colorBtn, &QPushButton::clicked, [&catColor, colorBtn]() {
QColor c = QColorDialog::getColor(catColor);
if (c.isValid()) {
catColor = c;
colorBtn->setStyleSheet(QString("background-color: %1; border: 1px solid #ccc; border-radius: 4px;").arg(c.name()));
}
});
QHBoxLayout *colorLayout = new QHBoxLayout();
colorLayout->addWidget(colorBtn);
colorLayout->addStretch();
form->addRow(tr("Color:"), colorLayout);
layout->addLayout(form);
QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
layout->addWidget(buttons);
if (dlg.exec() == QDialog::Accepted) {
cat.name = nameEdit->text().trimmed();
cat.color = catColor;
if (!cat.name.isEmpty()) {
emit categoryEditRequested(cat);
}
}
}
void SettingsView::onCategoryDeleteClicked() {
QTreeWidgetItem *item = m_categoryTree->currentItem();
if (!item) return;
qint64 catId = item->data(0, Qt::UserRole).toLongLong();
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Delete Category"));
msgBox.setText(tr("Are you sure you want to delete this category?"));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
if (msgBox.exec() == QMessageBox::Yes) {
emit categoryDeleteRequested(catId);
}
}
void SettingsView::loadCategories() {
refreshCategoryTree();
}
void SettingsView::refreshCategoryTree() {
m_categoryTree->clear();
// Load global categories
QVector<Category> globalCats = CategoryDao::findGlobal();
for (const Category &cat : globalCats) {
QTreeWidgetItem *item = createCategoryTreeItem(cat);
m_categoryTree->addTopLevelItem(item);
}
// Load account-specific categories for selected account
if (m_selectedAccountId > 0) {
QVector<Category> accountCats = CategoryDao::findByAccount(m_selectedAccountId);
for (const Category &cat : accountCats) {
QTreeWidgetItem *item = createCategoryTreeItem(cat);
m_categoryTree->addTopLevelItem(item);
}
}
m_categoryTree->expandAll();
}
QTreeWidgetItem* SettingsView::createCategoryTreeItem(const Category &cat, QTreeWidgetItem *parent) {
QTreeWidgetItem *item = parent ? new QTreeWidgetItem(parent) : new QTreeWidgetItem();
item->setText(0, cat.name);
item->setData(0, Qt::UserRole, cat.id);
// Color indicator
QTableWidgetItem *colorItem = new QTableWidgetItem();
colorItem->setBackground(cat.color);
colorItem->setFlags(colorItem->flags() & ~Qt::ItemIsEditable);
// Account info
QString accountInfo = cat.accountId >= 0 ? QString("Account %1").arg(cat.accountId) : tr("Global");
return item;
}
// ========== Rule Slots ==========
void SettingsView::onRuleSelectionChanged() {
bool hasSelection = m_ruleList->currentRow() >= 0;
m_ruleEditBtn->setEnabled(hasSelection);
m_ruleDeleteBtn->setEnabled(hasSelection);
m_ruleToggleBtn->setEnabled(hasSelection);
}
void SettingsView::onRuleAddClicked() {
RuleEditorDialog dlg(Rule(), -1, this);
connect(&dlg, &RuleEditorDialog::ruleSaved, this, [this](const Rule& rule) {
emit ruleAddRequested(rule);
});
dlg.exec();
}
void SettingsView::onRuleEditClicked() {
int row = m_ruleList->currentRow();
if (row < 0) return;
QListWidgetItem *item = m_ruleList->item(row);
qint64 ruleId = item->data(Qt::UserRole).toLongLong();
std::optional<Rule> ruleOpt = RuleDao::findById(ruleId);
if (!ruleOpt.has_value()) return;
RuleEditorDialog dlg(*ruleOpt, -1, this);
connect(&dlg, &RuleEditorDialog::ruleSaved, this, [this](const Rule& rule) {
emit ruleEditRequested(rule);
});
dlg.exec();
}
void SettingsView::onRuleDeleteClicked() {
int row = m_ruleList->currentRow();
if (row < 0) return;
QListWidgetItem *item = m_ruleList->item(row);
qint64 ruleId = item->data(Qt::UserRole).toLongLong();
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Delete Rule"));
msgBox.setText(tr("Are you sure you want to delete this rule?"));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
if (msgBox.exec() == QMessageBox::Yes) {
emit ruleDeleteRequested(ruleId);
}
}
void SettingsView::onRuleToggleEnabled() {
int row = m_ruleList->currentRow();
if (row < 0) return;
QListWidgetItem *item = m_ruleList->item(row);
qint64 ruleId = item->data(Qt::UserRole).toLongLong();
std::optional<Rule> ruleOpt = RuleDao::findById(ruleId);
if (!ruleOpt.has_value()) return;
Rule rule = *ruleOpt;
rule.enabled = !rule.enabled;
emit ruleEditRequested(rule);
}
void SettingsView::loadRules() {
m_ruleList->clear();
// Load global rules
QVector<Rule> globalRules = RuleDao::findGlobal();
for (const Rule &rule : globalRules) {
QListWidgetItem *item = new QListWidgetItem(ruleToString(rule));
item->setData(Qt::UserRole, rule.id);
if (!rule.enabled) {
item->setForeground(Qt::gray);
}
m_ruleList->addItem(item);
}
// Load account-specific rules for selected account
if (m_selectedAccountId > 0) {
QVector<Rule> accountRules = RuleDao::findByAccount(m_selectedAccountId);
for (const Rule &rule : accountRules) {
QListWidgetItem *item = new QListWidgetItem(ruleToString(rule));
item->setData(Qt::UserRole, rule.id);
if (!rule.enabled) {
item->setForeground(Qt::gray);
}
m_ruleList->addItem(item);
}
}
}
QString SettingsView::ruleToString(const Rule &rule) const {
QString status = rule.enabled ? "" : "";
return QString("%1 %2 (Priority: %3)").arg(status, rule.name, QString::number(rule.priority));
}
// ========== Template Slots ==========
void SettingsView::onTemplateSelectionChanged() {
bool hasSelection = m_templateList->currentRow() >= 0;
m_templateEditBtn->setEnabled(hasSelection);
m_templateDeleteBtn->setEnabled(hasSelection);
}
void SettingsView::onTemplateAddClicked() {
TemplateEditorDialog dlg(Template(), -1, this);
connect(&dlg, &TemplateEditorDialog::templateSaved, this, [this](const Template& tmpl) {
emit templateAddRequested(tmpl);
});
dlg.exec();
}
void SettingsView::onTemplateEditClicked() {
int row = m_templateList->currentRow();
if (row < 0) return;
QListWidgetItem *item = m_templateList->item(row);
qint64 tmplId = item->data(Qt::UserRole).toLongLong();
std::optional<Template> tmplOpt = TemplateDao::findById(tmplId);
if (!tmplOpt.has_value()) return;
TemplateEditorDialog dlg(*tmplOpt, -1, this);
connect(&dlg, &TemplateEditorDialog::templateSaved, this, [this](const Template& tmpl) {
emit templateEditRequested(tmpl);
});
dlg.exec();
}
void SettingsView::onTemplateDeleteClicked() {
int row = m_templateList->currentRow();
if (row < 0) return;
QListWidgetItem *item = m_templateList->item(row);
qint64 tmplId = item->data(Qt::UserRole).toLongLong();
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Delete Template"));
msgBox.setText(tr("Are you sure you want to delete this template?"));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
if (msgBox.exec() == QMessageBox::Yes) {
emit templateDeleteRequested(tmplId);
}
}
void SettingsView::loadTemplates() {
m_templateList->clear();
// Load global templates
QVector<Template> globalTmpls = TemplateDao::findGlobal();
for (const Template &tmpl : globalTmpls) {
QListWidgetItem *item = new QListWidgetItem(templateToString(tmpl));
item->setData(Qt::UserRole, tmpl.id);
if (tmpl.isDefault) {
item->setFont(QFont("Segoe UI", 9, QFont::Bold));
}
m_templateList->addItem(item);
}
// Load account-specific templates
if (m_selectedAccountId > 0) {
QVector<Template> accountTmpls = TemplateDao::findByAccount(m_selectedAccountId);
for (const Template &tmpl : accountTmpls) {
QListWidgetItem *item = new QListWidgetItem(templateToString(tmpl));
item->setData(Qt::UserRole, tmpl.id);
if (tmpl.isDefault) {
item->setFont(QFont("Segoe UI", 9, QFont::Bold));
}
m_templateList->addItem(item);
}
}
}
QString SettingsView::templateToString(const Template &tmpl) const {
QString status = tmpl.isDefault ? "" : "";
return QString("%1%2 (%3 vars)").arg(status, tmpl.name, QString::number(tmpl.variables.size()));
}
// ========== Signature Slots ==========
void SettingsView::onSignatureSelectionChanged() {
bool hasSelection = m_signatureList->currentRow() >= 0;
m_signatureEditBtn->setEnabled(hasSelection);
m_signatureDeleteBtn->setEnabled(hasSelection);
}
void SettingsView::onSignatureAddClicked() {
SignatureEditorDialog dlg(QString(), this);
if (dlg.exec() == QDialog::Accepted) {
Signature sig;
sig.name = dlg.getSignatureName();
sig.html = dlg.getSignatureHtml();
sig.accountId = -1; // Global by default
emit signatureAddRequested(sig);
}
}
void SettingsView::onSignatureEditClicked() {
int row = m_signatureList->currentRow();
if (row < 0) return;
QListWidgetItem *item = m_signatureList->item(row);
qint64 sigId = item->data(Qt::UserRole).toLongLong();
std::optional<Signature> sigOpt = SignatureDao::findById(sigId);
if (!sigOpt.has_value()) return;
Signature sig = *sigOpt;
SignatureEditorDialog dlg(sig.html, this);
if (dlg.exec() == QDialog::Accepted) {
sig.name = dlg.getSignatureName();
sig.html = dlg.getSignatureHtml();
emit signatureEditRequested(sig);
}
}
void SettingsView::onSignatureDeleteClicked() {
int row = m_signatureList->currentRow();
if (row < 0) return;
QListWidgetItem *item = m_signatureList->item(row);
qint64 sigId = item->data(Qt::UserRole).toLongLong();
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Delete Signature"));
msgBox.setText(tr("Are you sure you want to delete this signature?"));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
if (msgBox.exec() == QMessageBox::Yes) {
emit signatureDeleteRequested(sigId);
}
}
void SettingsView::loadSignatures() {
m_signatureList->clear();
// Load global signatures
QVector<Signature> globalSigs = SignatureDao::findGlobal();
for (const Signature &sig : globalSigs) {
QListWidgetItem *item = new QListWidgetItem(sig.name);
item->setData(Qt::UserRole, sig.id);
m_signatureList->addItem(item);
}
// Load account-specific signatures
if (m_selectedAccountId > 0) {
QVector<Signature> accountSigs = SignatureDao::findByAccount(m_selectedAccountId);
for (const Signature &sig : accountSigs) {
QListWidgetItem *item = new QListWidgetItem(sig.name);
item->setData(Qt::UserRole, sig.id);
m_signatureList->addItem(item);
}
}
}
QString SettingsView::signatureToString(const Signature &sig) const {
return sig.name;
}
+96
View File
@@ -12,7 +12,12 @@
#include <QRadioButton> #include <QRadioButton>
#include <QButtonGroup> #include <QButtonGroup>
#include <QVariant> #include <QVariant>
#include <QTreeWidget>
#include <QLineEdit>
#include <QDialog>
#include <QColorDialog>
#include "services/accountservice.h" #include "services/accountservice.h"
#include "db/dao/common_structs.h"
class SettingsView : public QWidget { class SettingsView : public QWidget {
Q_OBJECT Q_OBJECT
@@ -29,6 +34,26 @@ signals:
void accountDeleteRequested(int accountId); void accountDeleteRequested(int accountId);
void themeChanged(const QString &theme); void themeChanged(const QString &theme);
void settingChanged(const QString &key, const QVariant &value); void settingChanged(const QString &key, const QVariant &value);
// Categories
void categoryAddRequested(const Category &cat);
void categoryEditRequested(const Category &cat);
void categoryDeleteRequested(qint64 categoryId);
// Rules
void ruleAddRequested(const Rule &rule);
void ruleEditRequested(const Rule &rule);
void ruleDeleteRequested(qint64 ruleId);
// Templates
void templateAddRequested(const Template &tmpl);
void templateEditRequested(const Template &tmpl);
void templateDeleteRequested(qint64 templateId);
// Signatures
void signatureAddRequested(const Signature &sig);
void signatureEditRequested(const Signature &sig);
void signatureDeleteRequested(qint64 signatureId);
private slots: private slots:
void onAccountListChanged(); void onAccountListChanged();
@@ -36,12 +61,58 @@ private slots:
void onEditClicked(); void onEditClicked();
void onDeleteClicked(); void onDeleteClicked();
void onAccountSelectionChanged(); void onAccountSelectionChanged();
// Categories
void onCategorySelectionChanged();
void onCategoryAddClicked();
void onCategoryEditClicked();
void onCategoryDeleteClicked();
// Rules
void onRuleSelectionChanged();
void onRuleAddClicked();
void onRuleEditClicked();
void onRuleDeleteClicked();
void onRuleToggleEnabled();
// Templates
void onTemplateSelectionChanged();
void onTemplateAddClicked();
void onTemplateEditClicked();
void onTemplateDeleteClicked();
// Signatures
void onSignatureSelectionChanged();
void onSignatureAddClicked();
void onSignatureEditClicked();
void onSignatureDeleteClicked();
private: private:
void setupUI(); void setupUI();
QWidget* createAccountsTab(); QWidget* createAccountsTab();
QWidget* createGeneralTab(); QWidget* createGeneralTab();
QWidget* createAppearanceTab(); QWidget* createAppearanceTab();
QWidget* createCategoriesTab();
QWidget* createRulesTab();
QWidget* createTemplatesTab();
QWidget* createSignaturesTab();
// Category helpers
void loadCategories();
void refreshCategoryTree();
QTreeWidgetItem* createCategoryTreeItem(const Category &cat, QTreeWidgetItem *parent = nullptr);
// Rule helpers
void loadRules();
QString ruleToString(const Rule &rule) const;
// Template helpers
void loadTemplates();
QString templateToString(const Template &tmpl) const;
// Signature helpers
void loadSignatures();
QString signatureToString(const Signature &sig) const;
QTabWidget *m_tabWidget; QTabWidget *m_tabWidget;
QListWidget *m_accountList; QListWidget *m_accountList;
@@ -53,4 +124,29 @@ private:
AccountService *m_accountService; AccountService *m_accountService;
int m_selectedAccountId; int m_selectedAccountId;
// Categories
QTreeWidget *m_categoryTree;
QPushButton *m_categoryAddBtn;
QPushButton *m_categoryEditBtn;
QPushButton *m_categoryDeleteBtn;
// Rules
QListWidget *m_ruleList;
QPushButton *m_ruleAddBtn;
QPushButton *m_ruleEditBtn;
QPushButton *m_ruleDeleteBtn;
QPushButton *m_ruleToggleBtn;
// Templates
QListWidget *m_templateList;
QPushButton *m_templateAddBtn;
QPushButton *m_templateEditBtn;
QPushButton *m_templateDeleteBtn;
// Signatures
QListWidget *m_signatureList;
QPushButton *m_signatureAddBtn;
QPushButton *m_signatureEditBtn;
QPushButton *m_signatureDeleteBtn;
}; };