diff --git a/CMakeLists.txt b/CMakeLists.txt index b1c220f..d8cc9d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ set(SRC_FILES src/db/dao/categorydao.cpp src/db/dao/ruledao.cpp src/db/dao/templatedao.cpp + src/db/dao/signaturedao.cpp src/db/dbchangeprocessor.cpp src/core/eventbus.cpp src/utils/notificationmanager.cpp diff --git a/icons.qrc b/icons.qrc index a936e65..c1fc62b 100644 --- a/icons.qrc +++ b/icons.qrc @@ -1,21 +1,55 @@ + resources/icons/app.svg resources/icons/sync.svg resources/icons/settings.svg - resources/icons/minimize.svg - resources/icons/maximize.svg - resources/icons/restore.svg - resources/icons/close.svg - resources/icons/SVG/Outline/Essentional, UI/Trash Bin Minimalistic.svg - resources/icons/SVG/Outline/Messages, Conversation/Paperclip.svg - resources/icons/SVG/Bold Duotone/Messages, Conversation/Plain.svg - resources/icons/SVG/Outline/Files/File Text.svg - resources/icons/SVG/Bold/Text Formatting/Text Bold.svg - resources/icons/SVG/Outline/Text Formatting/Text Italic.svg - resources/icons/SVG/Outline/Text Formatting/Text Underline.svg - resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg - resources/icons/SVG/Linear/Arrows Action/Square Top Up.svg - resources/icons/SVG/Linear/Essentional, UI/Close Square.svg + + + resources/icons/SVG/Linear/Arrows Action/Minimize Square Minimalistic.svg + resources/icons/SVG/Linear/Arrows Action/Maximize Square Minimalistic.svg + resources/icons/SVG/Linear/Arrows Action/Maximize Square 2.svg + resources/icons/SVG/Linear/Essentional, UI/Close Square.svg + + + resources/icons/SVG/Outline/Messages, Conversation/Paperclip.svg + resources/icons/SVG/Bold Duotone/Messages, Conversation/Plain.svg + resources/icons/SVG/Linear/Arrows Action/Reply.svg + resources/icons/SVG/Linear/Arrows Action/Forward.svg + resources/icons/SVG/Linear/Essentional, UI/Trash Bin Minimalistic.svg + resources/icons/SVG/Linear/Design, Tools/Filters.svg + resources/icons/SVG/Linear/Time/Stopwatch Play.svg + resources/icons/SVG/Linear/Files/File Text.svg + + + resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg + resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg + resources/icons/SVG/Linear/Arrows Action/Import.svg + resources/icons/SVG/Linear/Arrows Action/Export.svg + + + resources/icons/SVG/Bold/Text Formatting/Text Bold.svg + resources/icons/SVG/Outline/Text Formatting/Text Italic.svg + resources/icons/SVG/Outline/Text Formatting/Text Underline.svg + + + resources/icons/SVG/Linear/Users/User.svg + + + resources/icons/SVG/Linear/Time/Calendar.svg + resources/icons/SVG/Linear/Time/Calendar Add.svg + + + resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg + resources/icons/SVG/Linear/Arrows Action/Square Top Up.svg + + + resources/icons/SVG/Linear/Essentional, UI/Menu Dots.svg + + + resources/icons/SVG/Linear/Design, Tools/Palette.svg + + + resources/icons/SVG/Linear/Time/Calendar Add.svg - + \ No newline at end of file diff --git a/src/db/dao/categorydao.cpp b/src/db/dao/categorydao.cpp index 3be40dd..fc9cce0 100644 --- a/src/db/dao/categorydao.cpp +++ b/src/db/dao/categorydao.cpp @@ -10,19 +10,20 @@ bool CategoryDao::insert(Category& cat) QSqlQuery query(db); query.prepare( "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(":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(":sortOrder", cat.sortOrder); - query.bindValue(":createdAt", QDateTime::currentDateTime()); - + query.bindValue(":createdAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate)); + if (!query.exec()) { - qWarning() << "Failed to insert category:" << query.lastError().text(); + qDebug() << "[CategoryDao] Insert failed:" << query.lastError().text(); return false; } + cat.id = query.lastInsertId().toLongLong(); return true; } @@ -32,26 +33,23 @@ bool CategoryDao::update(const Category& cat) QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare( - "UPDATE Category SET " - "accountId = :accountId, " - "name = :name, " - "color = :color, " - "parentCategoryId = :parentCategoryId, " - "sortOrder = :sortOrder " - "WHERE id = :id" - ); + "UPDATE Category SET accountId = :accountId, name = :name, color = :color, " + "parentCategoryId = :parentCategoryId, sortOrder = :sortOrder " + "WHERE id = :id"); + query.bindValue(":id", cat.id); query.bindValue(":accountId", cat.accountId >= 0 ? cat.accountId : QVariant()); 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(":sortOrder", cat.sortOrder); - + if (!query.exec()) { - qWarning() << "Failed to update category:" << query.lastError().text(); + qDebug() << "[CategoryDao] Update failed:" << query.lastError().text(); return false; } - return query.numRowsAffected() > 0; + + return true; } bool CategoryDao::remove(qint64 id) @@ -60,12 +58,13 @@ bool CategoryDao::remove(qint64 id) QSqlQuery query(db); query.prepare("DELETE FROM Category WHERE id = :id"); query.bindValue(":id", id); - + if (!query.exec()) { - qWarning() << "Failed to delete category:" << query.lastError().text(); + qDebug() << "[CategoryDao] Remove failed:" << query.lastError().text(); return false; } - return query.numRowsAffected() > 0; + + return true; } std::optional CategoryDao::findById(qint64 id) @@ -74,19 +73,20 @@ std::optional CategoryDao::findById(qint64 id) QSqlQuery query(db); query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE id = :id"); query.bindValue(":id", id); - + if (!query.exec() || !query.next()) { return std::nullopt; } - + Category cat; - cat.id = query.value(0).toLongLong(); - cat.accountId = query.value(1).toLongLong(); - cat.name = query.value(2).toString(); - cat.color = QColor(query.value(3).toString()); - cat.parentCategoryId = query.value(4).toLongLong(); - cat.sortOrder = query.value(5).toInt(); - cat.createdAt = query.value(6).toDateTime(); + cat.id = query.value("id").toLongLong(); + cat.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong(); + 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); + return cat; } @@ -94,24 +94,20 @@ QVector CategoryDao::findByAccount(qint64 accountId) { QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); - if (accountId >= 0) { - 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); - } else { - query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE accountId IS NULL ORDER BY sortOrder, name"); - } - + query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE accountId = :accountId ORDER BY sortOrder, name"); + query.bindValue(":accountId", accountId); + QVector cats; if (query.exec()) { while (query.next()) { Category cat; - cat.id = query.value(0).toLongLong(); - cat.accountId = query.value(1).toLongLong(); - cat.name = query.value(2).toString(); - cat.color = QColor(query.value(3).toString()); - cat.parentCategoryId = query.value(4).toLongLong(); - cat.sortOrder = query.value(5).toInt(); - cat.createdAt = query.value(6).toDateTime(); + cat.id = query.value("id").toLongLong(); + cat.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong(); + 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); } } @@ -120,28 +116,42 @@ QVector CategoryDao::findByAccount(qint64 accountId) QVector 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 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 CategoryDao::findAll() { QSqlDatabase db = DatabaseManager::instance().database(); 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 cats; - if (query.exec()) { - while (query.next()) { - Category cat; - cat.id = query.value(0).toLongLong(); - cat.accountId = query.value(1).toLongLong(); - cat.name = query.value(2).toString(); - cat.color = QColor(query.value(3).toString()); - cat.parentCategoryId = query.value(4).toLongLong(); - cat.sortOrder = query.value(5).toInt(); - cat.createdAt = query.value(6).toDateTime(); - cats.append(cat); - } + while (query.next()) { + Category cat; + cat.id = query.value("id").toLongLong(); + cat.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong(); + 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; } @@ -152,103 +162,88 @@ QVector CategoryDao::findChildren(qint64 parentId) QSqlQuery query(db); query.prepare("SELECT id, accountId, name, color, parentCategoryId, sortOrder, createdAt FROM Category WHERE parentCategoryId = :parentId ORDER BY sortOrder, name"); query.bindValue(":parentId", parentId); - + QVector cats; if (query.exec()) { while (query.next()) { Category cat; - cat.id = query.value(0).toLongLong(); - cat.accountId = query.value(1).toLongLong(); - cat.name = query.value(2).toString(); - cat.color = QColor(query.value(3).toString()); - cat.parentCategoryId = query.value(4).toLongLong(); - cat.sortOrder = query.value(5).toInt(); - cat.createdAt = query.value(6).toDateTime(); + cat.id = query.value("id").toLongLong(); + cat.accountId = query.value("accountId").isNull() ? -1 : query.value("accountId").toLongLong(); + 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; } -bool CategoryDao::assignToMail(qint64 mailCopyId, qint64 categoryId) +bool CategoryDao::assignToMail(qint64 mailId, qint64 categoryId) { QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); - query.prepare( - "INSERT OR IGNORE INTO MailCategory (mailCopyId, categoryId, assignedAt) " - "VALUES (:mailCopyId, :categoryId, :assignedAt)" - ); - query.bindValue(":mailCopyId", mailCopyId); + query.prepare("INSERT OR IGNORE INTO MailCategory (mailCopyId, categoryId, assignedAt) VALUES (:mailId, :categoryId, :assignedAt)"); + query.bindValue(":mailId", mailId); query.bindValue(":categoryId", categoryId); - query.bindValue(":assignedAt", QDateTime::currentDateTime()); - + query.bindValue(":assignedAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate)); + if (!query.exec()) { - qWarning() << "Failed to assign category to mail:" << query.lastError().text(); + qDebug() << "[CategoryDao] assignToMail failed:" << query.lastError().text(); return false; } return true; } -bool CategoryDao::removeFromMail(qint64 mailCopyId, qint64 categoryId) +bool CategoryDao::removeFromMail(qint64 mailId, qint64 categoryId) { QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); - query.prepare("DELETE FROM MailCategory WHERE mailCopyId = :mailCopyId AND categoryId = :categoryId"); - query.bindValue(":mailCopyId", mailCopyId); + query.prepare("DELETE FROM MailCategory WHERE mailCopyId = :mailId AND categoryId = :categoryId"); + query.bindValue(":mailId", mailId); query.bindValue(":categoryId", categoryId); - + if (!query.exec()) { - qWarning() << "Failed to remove category from mail:" << query.lastError().text(); + qDebug() << "[CategoryDao] removeFromMail failed:" << query.lastError().text(); return false; } - return query.numRowsAffected() > 0; + return true; } -QVector CategoryDao::categoriesForMail(qint64 mailCopyId) +QVector CategoryDao::categoriesForMail(qint64 mailId) { QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); - query.prepare("SELECT categoryId FROM MailCategory WHERE mailCopyId = :mailCopyId"); - query.bindValue(":mailCopyId", mailCopyId); - - QVector ids; + query.prepare("SELECT categoryId FROM MailCategory WHERE mailCopyId = :mailId"); + query.bindValue(":mailId", mailId); + + QVector catIds; if (query.exec()) { while (query.next()) { - ids.append(query.value(0).toLongLong()); + catIds.append(query.value("categoryId").toLongLong()); } } - return ids; + return catIds; } -QVector CategoryDao::fullCategoriesForMail(qint64 mailCopyId) -{ - QVector ids = categoriesForMail(mailCopyId); - QVector cats; - for (qint64 id : ids) { - if (auto cat = findById(id)) { - cats.append(*cat); - } - } - return cats; -} - -bool CategoryDao::reorder(const QVector& idsInOrder) +bool CategoryDao::reorder(const QVector& ids) { QSqlDatabase db = DatabaseManager::instance().database(); db.transaction(); - - QSqlQuery query(db); - query.prepare("UPDATE Category SET sortOrder = :order WHERE id = :id"); - - for (int i = 0; i < idsInOrder.size(); ++i) { - query.bindValue(":id", idsInOrder[i]); - query.bindValue(":order", i); + + for (int i = 0; i < ids.size(); ++i) { + QSqlQuery query(db); + query.prepare("UPDATE Category SET sortOrder = :sortOrder WHERE id = :id"); + query.bindValue(":sortOrder", i); + query.bindValue(":id", ids[i]); if (!query.exec()) { db.rollback(); - qWarning() << "Failed to reorder category:" << query.lastError().text(); + qDebug() << "[CategoryDao] reorder failed:" << query.lastError().text(); return false; } } - - return db.commit(); + + db.commit(); + return true; } \ No newline at end of file diff --git a/src/db/dao/categorydao.h b/src/db/dao/categorydao.h index a510561..b4e7b1c 100644 --- a/src/db/dao/categorydao.h +++ b/src/db/dao/categorydao.h @@ -3,22 +3,7 @@ #include "../databasemanager.h" #include #include -#include -#include - -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(); } -}; +#include "common_structs.h" class CategoryDao { @@ -27,13 +12,12 @@ public: static bool update(const Category& cat); static bool remove(qint64 id); static std::optional findById(qint64 id); - static QVector findByAccount(qint64 accountId); // accountId = -1 for global + static QVector findByAccount(qint64 accountId); static QVector findGlobal(); static QVector findAll(); static QVector findChildren(qint64 parentId); - static bool assignToMail(qint64 mailCopyId, qint64 categoryId); - static bool removeFromMail(qint64 mailCopyId, qint64 categoryId); - static QVector categoriesForMail(qint64 mailCopyId); - static QVector fullCategoriesForMail(qint64 mailCopyId); - static bool reorder(const QVector& idsInOrder); + static bool assignToMail(qint64 mailId, qint64 categoryId); + static bool removeFromMail(qint64 mailId, qint64 categoryId); + static QVector categoriesForMail(qint64 mailId); // Returns category IDs + static bool reorder(const QVector& ids); }; \ No newline at end of file diff --git a/src/db/dao/common_structs.h b/src/db/dao/common_structs.h new file mode 100644 index 0000000..3d7a182 --- /dev/null +++ b/src/db/dao/common_structs.h @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +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 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 conditions; + QVector 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); +}; \ No newline at end of file diff --git a/src/db/dao/ruledao.h b/src/db/dao/ruledao.h index 94663f9..b43d12d 100644 --- a/src/db/dao/ruledao.h +++ b/src/db/dao/ruledao.h @@ -3,68 +3,7 @@ #include "../databasemanager.h" #include #include -#include -#include -#include - -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 conditions; - QVector 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); -}; +#include "common_structs.h" class RuleDao { @@ -75,7 +14,8 @@ public: static std::optional findById(qint64 id); static QVector findByAccount(qint64 accountId); // accountId = -1 for global static QVector findGlobal(); - static QVector findAllEnabled(); + static QVector findAll(); + static QVector findAllEnabled(); // only enabled rules static bool setEnabled(qint64 id, bool enabled); static bool updateLastRun(qint64 id, const QDateTime& when, qint64 runCount); }; \ No newline at end of file diff --git a/src/db/dao/signaturedao.cpp b/src/db/dao/signaturedao.cpp new file mode 100644 index 0000000..08c9d91 --- /dev/null +++ b/src/db/dao/signaturedao.cpp @@ -0,0 +1,220 @@ +#include "signaturedao.h" +#include +#include +#include +#include + +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 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 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 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 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 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 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 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 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; +} \ No newline at end of file diff --git a/src/db/dao/signaturedao.h b/src/db/dao/signaturedao.h new file mode 100644 index 0000000..88a72e6 --- /dev/null +++ b/src/db/dao/signaturedao.h @@ -0,0 +1,20 @@ +#pragma once + +#include "../databasemanager.h" +#include +#include +#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 findById(qint64 id); + static QVector findByAccount(qint64 accountId); + static QVector findGlobal(); + static QVector findAll(); + static std::optional findDefault(qint64 accountId); + static bool setDefault(qint64 id, bool isDefault); +}; \ No newline at end of file diff --git a/src/db/dao/templatedao.h b/src/db/dao/templatedao.h index 4d60f78..149d009 100644 --- a/src/db/dao/templatedao.h +++ b/src/db/dao/templatedao.h @@ -6,35 +6,7 @@ #include #include #include - -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 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); -}; +#include "common_structs.h" class TemplateDao { diff --git a/src/ui/readerview.cpp b/src/ui/readerview.cpp index f5fea97..c42a87f 100644 --- a/src/ui/readerview.cpp +++ b/src/ui/readerview.cpp @@ -30,9 +30,51 @@ ReaderView::ReaderView(QWidget *parent) : QWidget(parent) { void ReaderView::setupUI() { QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(12, 0, 12, 0); + mainLayout->setContentsMargins(10, 0, 10, 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) ===== m_toolbar = new QFrame(); m_toolbar->setFixedHeight(36); @@ -183,47 +225,6 @@ void ReaderView::setupUI() { mainLayout->addWidget(m_toolbar); 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 ===== m_headerWidget = new QWidget(); m_headerWidget->setStyleSheet("QWidget { background: white; border-bottom: 1px solid #e0e0e0; }"); diff --git a/src/ui/settingsview.cpp b/src/ui/settingsview.cpp index 20da9f0..5538e60 100644 --- a/src/ui/settingsview.cpp +++ b/src/ui/settingsview.cpp @@ -1,6 +1,14 @@ -#include "ui/settingsview.h" +#include "settingsview.h" #include "services/accountservice.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 #include @@ -13,6 +21,19 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include SettingsView::SettingsView(QWidget *parent) : QWidget(parent) { setupUI(); @@ -23,9 +44,13 @@ void SettingsView::setupUI() { mainLayout->setContentsMargins(0, 0, 0, 0); m_tabWidget = new QTabWidget(); - m_tabWidget->addTab(createAccountsTab(), "Accounts"); - m_tabWidget->addTab(createGeneralTab(), "General"); - m_tabWidget->addTab(createAppearanceTab(), "Appearance"); + m_tabWidget->addTab(createAccountsTab(), tr("Accounts")); + m_tabWidget->addTab(createCategoriesTab(), tr("Categories")); + 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); } @@ -35,9 +60,10 @@ QWidget* SettingsView::createAccountsTab() { QVBoxLayout *layout = new QVBoxLayout(widget); layout->setContentsMargins(20, 20, 20, 20); - QLabel *title = new QLabel("Email Accounts"); + QLabel *title = new QLabel(tr("Email Accounts")); QFont titleFont = title->font(); titleFont.setPointSize(16); + titleFont.setBold(true); title->setFont(titleFont); layout->addWidget(title); @@ -51,21 +77,22 @@ QWidget* SettingsView::createAccountsTab() { layout->addWidget(m_accountList); QHBoxLayout *buttonLayout = new QHBoxLayout(); - m_addBtn = new QPushButton("Add Account"); + m_addBtn = new QPushButton(tr("Add Account")); 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; }" - "QPushButton:hover { background: #FDD835; }" + "QPushButton:hover { background: #1565C0; }" ); buttonLayout->addWidget(m_addBtn); - m_editBtn = new QPushButton("Edit"); + + m_editBtn = new QPushButton(tr("Edit")); m_editBtn->setEnabled(false); m_editBtn->setStyleSheet( "QPushButton { background: #FFA726; color: white; border: none; border-radius: 4px; " "padding: 8px 16px; font-weight: bold; }" "QPushButton:hover { background: #FB8C00; }" ); - m_deleteBtn = new QPushButton("Delete"); + m_deleteBtn = new QPushButton(tr("Delete")); m_deleteBtn->setEnabled(false); m_deleteBtn->setStyleSheet( "QPushButton { background: #EF5350; color: white; border: none; border-radius: 4px; " @@ -85,32 +112,283 @@ QWidget* SettingsView::createAccountsTab() { 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 *widget = new QWidget(); QVBoxLayout *layout = new QVBoxLayout(widget); layout->setContentsMargins(20, 20, 20, 20); - QLabel *title = new QLabel("General Settings"); + QLabel *title = new QLabel(tr("General Settings")); QFont titleFont = title->font(); titleFont.setPointSize(16); + titleFont.setBold(true); title->setFont(titleFont); layout->addWidget(title); - QCheckBox *startOnLogin = new QCheckBox("Start application on login"); + QCheckBox *startOnLogin = new QCheckBox(tr("Start application on login")); startOnLogin->setChecked(true); connect(startOnLogin, &QCheckBox::toggled, this, [this](bool checked) { emit settingChanged("start_on_login", QVariant(checked)); }); layout->addWidget(startOnLogin); - QCheckBox *enableNotifications = new QCheckBox("Enable notifications"); + QCheckBox *enableNotifications = new QCheckBox(tr("Enable notifications")); enableNotifications->setChecked(true); connect(enableNotifications, &QCheckBox::toggled, this, [this](bool checked) { emit settingChanged("enable_notifications", QVariant(checked)); }); 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); connect(minimizeToTray, &QCheckBox::toggled, this, [this](bool checked) { emit settingChanged("minimize_to_tray", QVariant(checked)); @@ -126,22 +404,23 @@ QWidget* SettingsView::createAppearanceTab() { QVBoxLayout *layout = new QVBoxLayout(widget); layout->setContentsMargins(20, 20, 20, 20); - QLabel *title = new QLabel("Appearance"); + QLabel *title = new QLabel(tr("Appearance")); QFont titleFont = title->font(); titleFont.setPointSize(16); + titleFont.setBold(true); title->setFont(titleFont); layout->addWidget(title); - QLabel *themeLabel = new QLabel("Theme"); + QLabel *themeLabel = new QLabel(tr("Theme")); QFont labelFont = themeLabel->font(); labelFont.setBold(true); themeLabel->setFont(labelFont); layout->addWidget(themeLabel); m_themeGroup = new QButtonGroup(this); - QRadioButton *lightRadio = new QRadioButton("Light"); - QRadioButton *darkRadio = new QRadioButton("Dark"); - QRadioButton *systemRadio = new QRadioButton("System"); + QRadioButton *lightRadio = new QRadioButton(tr("Light")); + QRadioButton *darkRadio = new QRadioButton(tr("Dark")); + QRadioButton *systemRadio = new QRadioButton(tr("System")); m_themeGroup->addButton(lightRadio, 0); m_themeGroup->addButton(darkRadio, 1); m_themeGroup->addButton(systemRadio, 2); @@ -155,14 +434,14 @@ QWidget* SettingsView::createAppearanceTab() { connect(m_themeGroup, QOverload::of(&QButtonGroup::idClicked), this, [this](int id) { QString theme; switch (id) { - case 0: theme = "light"; break; - case 1: theme = "dark"; break; - case 2: theme = "system"; break; + case 0: theme = "light"; break; + case 1: theme = "dark"; break; + case 2: theme = "system"; break; } emit themeChanged(theme); }); - QCheckBox *deepinTheme = new QCheckBox("Deepin theme (experimental)"); + QCheckBox *deepinTheme = new QCheckBox(tr("Deepin theme (experimental)")); deepinTheme->setChecked(true); connect(deepinTheme, &QCheckBox::toggled, this, [this](bool checked) { emit settingChanged("deepin_theme", QVariant(checked)); @@ -173,6 +452,7 @@ QWidget* SettingsView::createAppearanceTab() { return widget; } +// ========== Slots for Accounts ========== void SettingsView::setAccountService(AccountService *service) { m_accountService = service; if (m_accountService) { @@ -204,8 +484,8 @@ void SettingsView::loadAccounts() { m_accountList->setCurrentRow(0); } } -void SettingsView::onAccountSelectionChanged() -{ + +void SettingsView::onAccountSelectionChanged() { QList items = m_accountList->selectedItems(); qDebug() << "SettingsView::onAccountSelectionChanged, selected items count:" << items.size(); if (items.isEmpty()) { @@ -231,10 +511,10 @@ void SettingsView::onEditClicked() { void SettingsView::onDeleteClicked() { if (m_selectedAccountId != -1 && m_accountService) { QMessageBox msgBox(this); - msgBox.setWindowTitle("Delete Account"); + msgBox.setWindowTitle(tr("Delete Account")); Account* account = m_accountService->findAccountById(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.setDefaultButton(QMessageBox::No); 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 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 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 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 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 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 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 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