diff --git a/YACReader/CMakeLists.txt b/YACReader/CMakeLists.txt index 51c55b61..62cf5864 100644 --- a/YACReader/CMakeLists.txt +++ b/YACReader/CMakeLists.txt @@ -56,9 +56,13 @@ target_compile_definitions(YACReader PRIVATE YACREADER) # Resources qt_add_resources(yacreader_images_rcc "${CMAKE_CURRENT_SOURCE_DIR}/yacreader_images.qrc") qt_add_resources(yacreader_files_rcc "${CMAKE_CURRENT_SOURCE_DIR}/yacreader_files.qrc") +qt_add_resources(yacreader_themes_rcc "${CMAKE_CURRENT_SOURCE_DIR}/themes/themes.qrc") +qt_add_resources(yacreader_common_images_rcc "${CMAKE_SOURCE_DIR}/common/themes/appearance_config_images.qrc") target_sources(YACReader PRIVATE ${yacreader_images_rcc} ${yacreader_files_rcc} + ${yacreader_themes_rcc} + ${yacreader_common_images_rcc} ) # Translations diff --git a/YACReader/main.cpp b/YACReader/main.cpp index e71f6791..d1c98e22 100644 --- a/YACReader/main.cpp +++ b/YACReader/main.cpp @@ -7,7 +7,10 @@ #include "main_window_viewer.h" #include "configuration.h" #include "exit_check.h" +#include "appearance_configuration.h" #include "theme_manager.h" +#include "theme_repository.h" +#include "yacreader_global.h" #include "QsLog.h" #include "QsLogDest.h" @@ -114,7 +117,11 @@ int main(int argc, char *argv[]) app.setApplicationName("YACReader"); app.setOrganizationName("YACReader"); - ThemeManager::instance().initialize(); + auto *appearanceConfig = new AppearanceConfiguration( + YACReader::getSettingsPath() + "/YACReader.ini", qApp); + auto *themeRepo = new ThemeRepository( + ":/themes", YACReader::getSettingsPath() + "/themes/user"); + ThemeManager::instance().initialize(appearanceConfig, themeRepo); if (QIcon::hasThemeIcon("YACReader")) { app.setWindowIcon(QIcon::fromTheme("YACReader")); diff --git a/YACReader/options_dialog.cpp b/YACReader/options_dialog.cpp index 4df071c3..c39e7115 100644 --- a/YACReader/options_dialog.cpp +++ b/YACReader/options_dialog.cpp @@ -12,7 +12,10 @@ #include #include #include +#include #include "theme_manager.h" +#include "theme_factory.h" +#include "appearance_tab_widget.h" #include "yacreader_spin_slider_widget.h" #include "yacreader_3d_flow_config_widget.h" @@ -203,9 +206,20 @@ OptionsDialog::OptionsDialog(QWidget *parent) pageFlow->setLayout(layoutFlow); pageImage->setLayout(layoutImageV); + // APPEARANCE ---------------------------------------- + + auto *pageAppearance = new AppearanceTabWidget( + ThemeManager::instance().getAppearanceConfiguration(), + []() { return ThemeManager::instance().getCurrentTheme().sourceJson; }, + [](const QJsonObject &json) { ThemeManager::instance().setTheme(makeTheme(json)); }, + this); + + // APPEARANCE END ------------------------------------ + tabWidget->addTab(pageGeneral, tr("General")); tabWidget->addTab(pageFlow, tr("Page Flow")); tabWidget->addTab(pageImage, tr("Image adjustment")); + tabWidget->addTab(pageAppearance, tr("Appearance")); layout->addWidget(tabWidget); diff --git a/YACReader/options_dialog.h b/YACReader/options_dialog.h index 7c2ef6cc..20467f77 100644 --- a/YACReader/options_dialog.h +++ b/YACReader/options_dialog.h @@ -4,6 +4,8 @@ #include "yacreader_options_dialog.h" #include "themable.h" +#include + class QDialog; class QLabel; class QLineEdit; diff --git a/YACReader/render.cpp b/YACReader/render.cpp index 595df27c..59996fea 100644 --- a/YACReader/render.cpp +++ b/YACReader/render.cpp @@ -1000,7 +1000,7 @@ void Render::fillBuffer() pageRenders[currentPageBufferedIndex + i]->start(); } - if ((currentIndex - i > 0) && + if ((currentIndex - i >= 0) && buffer[currentPageBufferedIndex - i]->isNull() && i <= numLeftPages && pageRenders[currentPageBufferedIndex - i] == 0 && diff --git a/YACReader/themes/builtin_classic.json b/YACReader/themes/builtin_classic.json new file mode 100644 index 00000000..74dff6b4 --- /dev/null +++ b/YACReader/themes/builtin_classic.json @@ -0,0 +1,51 @@ +{ + "meta": { + "id": "builtin/classic", + "displayName": "Default Classic", + "variant": "dark" + }, + "toolbar": { + "iconColor": "#404040", + "iconDisabledColor": "#858585", + "iconCheckedColor": "#5a5a5a", + "backgroundColor": "#f3f3f3", + "separatorColor": "#cccccc", + "checkedButtonColor": "#cccccc", + "menuIndicatorColor": "#404040" + }, + "viewer": { + "defaultBackgroundColor": "#282828", + "defaultTextColor": "#ffffff", + "infoBackgroundColor": "#bb000000", + "infoTextColor": "#ffffff" + }, + "goToFlowWidget": { + "flowBackgroundColor": "#282828", + "flowTextColor": "#ffffff", + "toolbarBackgroundColor": "#99000000", + "sliderBorderColor": "#22ffffff", + "sliderGrooveColor": "#77000000", + "sliderHandleColor": "#55ffffff", + "editBorderColor": "#77000000", + "editBackgroundColor": "#55000000", + "editTextColor": "#ffffff", + "labelTextColor": "#ffffff", + "iconColor": "#ffffff" + }, + "helpAboutDialog": { + "headingColor": "#302f2d", + "linkColor": "#c19441" + }, + "whatsNewDialog": { + "backgroundColor": "#ffffff", + "headerTextColor": "#0a0a0a", + "versionTextColor": "#858585", + "contentTextColor": "#0a0a0a", + "linkColor": "#e8b800", + "closeButtonColor": "#444444", + "headerDecorationColor": "#e8b800" + }, + "shortcutsIcons": { + "iconColor": "#404040" + } +} diff --git a/YACReader/themes/builtin_dark.json b/YACReader/themes/builtin_dark.json new file mode 100644 index 00000000..01085ca9 --- /dev/null +++ b/YACReader/themes/builtin_dark.json @@ -0,0 +1,51 @@ +{ + "meta": { + "id": "builtin/dark", + "displayName": "Default Dark", + "variant": "dark" + }, + "toolbar": { + "iconColor": "#cccccc", + "iconDisabledColor": "#444444", + "iconCheckedColor": "#dadada", + "backgroundColor": "#202020", + "separatorColor": "#444444", + "checkedButtonColor": "#3a3a3a", + "menuIndicatorColor": "#cccccc" + }, + "viewer": { + "defaultBackgroundColor": "#282828", + "defaultTextColor": "#ffffff", + "infoBackgroundColor": "#bb000000", + "infoTextColor": "#b0b0b0" + }, + "goToFlowWidget": { + "flowBackgroundColor": "#282828", + "flowTextColor": "#ffffff", + "toolbarBackgroundColor": "#99000000", + "sliderBorderColor": "#22ffffff", + "sliderGrooveColor": "#77000000", + "sliderHandleColor": "#55ffffff", + "editBorderColor": "#77000000", + "editBackgroundColor": "#55000000", + "editTextColor": "#ffffff", + "labelTextColor": "#ffffff", + "iconColor": "#cccccc" + }, + "helpAboutDialog": { + "headingColor": "#e0e0e0", + "linkColor": "#d4a84b" + }, + "whatsNewDialog": { + "backgroundColor": "#2a2a2a", + "headerTextColor": "#e0e0e0", + "versionTextColor": "#858585", + "contentTextColor": "#e0e0e0", + "linkColor": "#e8b800", + "closeButtonColor": "#dddddd", + "headerDecorationColor": "#e8b800" + }, + "shortcutsIcons": { + "iconColor": "#d0d0d0" + } +} diff --git a/YACReader/themes/builtin_light.json b/YACReader/themes/builtin_light.json new file mode 100644 index 00000000..97905791 --- /dev/null +++ b/YACReader/themes/builtin_light.json @@ -0,0 +1,51 @@ +{ + "meta": { + "id": "builtin/light", + "displayName": "Default Light", + "variant": "light" + }, + "toolbar": { + "iconColor": "#404040", + "iconDisabledColor": "#b0b0b0", + "iconCheckedColor": "#5a5a5a", + "backgroundColor": "#f3f3f3", + "separatorColor": "#cccccc", + "checkedButtonColor": "#cccccc", + "menuIndicatorColor": "#404040" + }, + "viewer": { + "defaultBackgroundColor": "#f6f6f6", + "defaultTextColor": "#202020", + "infoBackgroundColor": "#bbffffff", + "infoTextColor": "#404040" + }, + "goToFlowWidget": { + "flowBackgroundColor": "#f6f6f6", + "flowTextColor": "#202020", + "toolbarBackgroundColor": "#bbffffff", + "sliderBorderColor": "#22000000", + "sliderGrooveColor": "#33000000", + "sliderHandleColor": "#55000000", + "editBorderColor": "#33000000", + "editBackgroundColor": "#22000000", + "editTextColor": "#202020", + "labelTextColor": "#202020", + "iconColor": "#404040" + }, + "helpAboutDialog": { + "headingColor": "#302f2d", + "linkColor": "#c19441" + }, + "whatsNewDialog": { + "backgroundColor": "#ffffff", + "headerTextColor": "#0a0a0a", + "versionTextColor": "#858585", + "contentTextColor": "#0a0a0a", + "linkColor": "#e8b800", + "closeButtonColor": "#444444", + "headerDecorationColor": "#e8b800" + }, + "shortcutsIcons": { + "iconColor": "#606060" + } +} diff --git a/YACReader/themes/theme.h b/YACReader/themes/theme.h index 2cc5e8cf..964e619f 100644 --- a/YACReader/themes/theme.h +++ b/YACReader/themes/theme.h @@ -2,9 +2,11 @@ #define THEME_H #include +#include #include "help_about_dialog_theme.h" #include "whats_new_dialog_theme.h" +#include "theme_meta.h" struct ToolbarThemeTemplates { QString toolbarQSS = "QToolBar { border: none; background: %1; }\n" @@ -142,6 +144,9 @@ struct DialogIconsTheme { }; struct Theme { + ThemeMeta meta; + QJsonObject sourceJson; + ToolbarTheme toolbar; ViewerTheme viewer; GoToFlowWidgetTheme goToFlowWidget; diff --git a/YACReader/themes/theme_factory.cpp b/YACReader/themes/theme_factory.cpp index d798a5a4..7a19d4f8 100644 --- a/YACReader/themes/theme_factory.cpp +++ b/YACReader/themes/theme_factory.cpp @@ -3,6 +3,7 @@ #include #include "icon_utils.h" +#include "theme_meta.h" struct ToolbarParams { ToolbarThemeTemplates t; @@ -56,7 +57,7 @@ struct ShortcutsIconsParams { }; struct ThemeParams { - QString themeName; + ThemeMeta meta; ToolbarParams toolbarParams; ViewerParams viewerParams; @@ -72,7 +73,7 @@ void setToolbarIconPair(QIcon &icon, const QColor &iconColor, const QColor &disabledColor, const QColor &checkedColor, - const QString &themeName) + const QString &themeId) { QString path18 = basePath; if (path18.endsWith(".svg")) @@ -81,14 +82,14 @@ void setToolbarIconPair(QIcon &icon, path18.append("_18x18"); // Normal - const QString normalPath = recoloredSvgToThemeFile(basePath, iconColor, themeName); - const QString normalPath18 = recoloredSvgToThemeFile(path18, iconColor, themeName); + const QString normalPath = recoloredSvgToThemeFile(basePath, iconColor, themeId); + const QString normalPath18 = recoloredSvgToThemeFile(path18, iconColor, themeId); // Disabled - const QString disabledPath = recoloredSvgToThemeFile(basePath, disabledColor, themeName, { .suffix = "_disabled" }); - const QString disabledPath18 = recoloredSvgToThemeFile(path18, disabledColor, themeName, { .suffix = "_disabled" }); + const QString disabledPath = recoloredSvgToThemeFile(basePath, disabledColor, themeId, { .suffix = "_disabled" }); + const QString disabledPath18 = recoloredSvgToThemeFile(path18, disabledColor, themeId, { .suffix = "_disabled" }); // Checked (On state) - const QString checkedPath = recoloredSvgToThemeFile(basePath, checkedColor, themeName, { .suffix = "_checked" }); - const QString checkedPath18 = recoloredSvgToThemeFile(path18, checkedColor, themeName, { .suffix = "_checked" }); + const QString checkedPath = recoloredSvgToThemeFile(basePath, checkedColor, themeId, { .suffix = "_checked" }); + const QString checkedPath18 = recoloredSvgToThemeFile(path18, checkedColor, themeId, { .suffix = "_checked" }); icon.addFile(normalPath, QSize(), QIcon::Normal, QIcon::Off); icon.addFile(disabledPath, QSize(), QIcon::Disabled, QIcon::Off); @@ -105,11 +106,13 @@ Theme makeTheme(const ThemeParams ¶ms) { Theme theme; + theme.meta = params.meta; + // Toolbar & actions - theme.toolbar.toolbarQSS = params.toolbarParams.t.toolbarQSS.arg(params.toolbarParams.backgroundColor.name(), params.toolbarParams.separatorColor.name(), params.toolbarParams.checkedButtonColor.name(), recoloredSvgToThemeFile(params.toolbarParams.t.menuArrowPath, params.toolbarParams.menuIndicatorColor, params.themeName)); + theme.toolbar.toolbarQSS = params.toolbarParams.t.toolbarQSS.arg(params.toolbarParams.backgroundColor.name(), params.toolbarParams.separatorColor.name(), params.toolbarParams.checkedButtonColor.name(), recoloredSvgToThemeFile(params.toolbarParams.t.menuArrowPath, params.toolbarParams.menuIndicatorColor, params.meta.id)); auto setToolbarIconPairT = [&](QIcon &icon, QIcon &icon18, const QString &basePath) { - setToolbarIconPair(icon, icon18, basePath, params.toolbarParams.iconColor, params.toolbarParams.iconDisabledColor, params.toolbarParams.iconCheckedColor, params.themeName); + setToolbarIconPair(icon, icon18, basePath, params.toolbarParams.iconColor, params.toolbarParams.iconDisabledColor, params.toolbarParams.iconCheckedColor, params.meta.id); }; setToolbarIconPairT(theme.toolbar.openAction, theme.toolbar.openAction18x18, ":/images/viewer_toolbar/open.svg"); @@ -166,8 +169,8 @@ Theme makeTheme(const ThemeParams ¶ms) theme.goToFlowWidget.buttonQSS = gotoParams.t.buttonQSS; theme.goToFlowWidget.labelQSS = gotoParams.t.labelQSS.arg(gotoParams.labelTextColor.name()); - const QString centerIconPath = recoloredSvgToThemeFile(":/images/centerFlow.svg", gotoParams.iconColor, params.themeName); - const QString goToIconPath = recoloredSvgToThemeFile(":/images/gotoFlow.svg", gotoParams.iconColor, params.themeName); + const QString centerIconPath = recoloredSvgToThemeFile(":/images/centerFlow.svg", gotoParams.iconColor, params.meta.id); + const QString goToIconPath = recoloredSvgToThemeFile(":/images/gotoFlow.svg", gotoParams.iconColor, params.meta.id); theme.goToFlowWidget.centerIcon = QIcon(centerIconPath); theme.goToFlowWidget.goToIcon = QIcon(goToIconPath); // end GoToFlowWidget @@ -183,14 +186,14 @@ Theme makeTheme(const ThemeParams ¶ms) theme.whatsNewDialog.versionTextColor = wn.versionTextColor; theme.whatsNewDialog.contentTextColor = wn.contentTextColor; theme.whatsNewDialog.linkColor = wn.linkColor; - theme.whatsNewDialog.closeButtonIcon = QPixmap(recoloredSvgToThemeFile(":/images/custom_dialog/custom_close_button.svg", wn.closeButtonColor, params.themeName)); - theme.whatsNewDialog.headerDecoration = QPixmap(recoloredSvgToThemeFile(":/images/whats_new/whatsnew_header.svg", wn.headerDecorationColor, params.themeName)); + theme.whatsNewDialog.closeButtonIcon = QPixmap(recoloredSvgToThemeFile(":/images/custom_dialog/custom_close_button.svg", wn.closeButtonColor, params.meta.id)); + theme.whatsNewDialog.headerDecoration = QPixmap(recoloredSvgToThemeFile(":/images/whats_new/whatsnew_header.svg", wn.headerDecorationColor, params.meta.id)); // end WhatsNewDialog // ShortcutsIcons const auto &sci = params.shortcutsIconsParams; auto makeShortcutsIcon = [&](const QString &basePath) { - const QString path = recoloredSvgToThemeFile(basePath, sci.iconColor, params.themeName); + const QString path = recoloredSvgToThemeFile(basePath, sci.iconColor, params.meta.id); return QIcon(path); }; @@ -203,7 +206,7 @@ Theme makeTheme(const ThemeParams ¶ms) // FindFolder icon (used in OptionsDialog) { - const QString path = recoloredSvgToThemeFile(":/images/find_folder.svg", params.toolbarParams.iconColor, params.themeName); + const QString path = recoloredSvgToThemeFile(":/images/find_folder.svg", params.toolbarParams.iconColor, params.meta.id); const qreal dpr = qApp->devicePixelRatio(); theme.dialogIcons.findFolderIcon = QIcon(renderSvgToPixmap(path, 13, 13, dpr)); } @@ -211,204 +214,92 @@ Theme makeTheme(const ThemeParams ¶ms) return theme; } -ThemeParams classicThemeParams(); -ThemeParams lightThemeParams(); -ThemeParams darkThemeParams(); +// JSON helpers --------------------------------------------------------------- -Theme makeTheme(ThemeId themeId) +static QColor colorFromJson(const QJsonObject &obj, const QString &key, const QColor &fallback) { - switch (themeId) { - case ThemeId::Classic: - return makeTheme(classicThemeParams()); - case ThemeId::Light: - return makeTheme(lightThemeParams()); - case ThemeId::Dark: - return makeTheme(darkThemeParams()); + if (!obj.contains(key)) + return fallback; + QColor c(obj[key].toString()); + return c.isValid() ? c : fallback; +} + +Theme makeTheme(const QJsonObject &json) +{ + ThemeParams p; + + if (json.contains("toolbar")) { + const auto t = json["toolbar"].toObject(); + auto &tp = p.toolbarParams; + tp.iconColor = colorFromJson(t, "iconColor", tp.iconColor); + tp.iconDisabledColor = colorFromJson(t, "iconDisabledColor", tp.iconDisabledColor); + tp.iconCheckedColor = colorFromJson(t, "iconCheckedColor", tp.iconCheckedColor); + tp.backgroundColor = colorFromJson(t, "backgroundColor", tp.backgroundColor); + tp.separatorColor = colorFromJson(t, "separatorColor", tp.separatorColor); + tp.checkedButtonColor = colorFromJson(t, "checkedButtonColor", tp.checkedButtonColor); + tp.menuIndicatorColor = colorFromJson(t, "menuIndicatorColor", tp.menuIndicatorColor); } -} - -ThemeParams classicThemeParams() -{ - ThemeParams params; - params.themeName = "classic"; - - ToolbarParams toolbarParams; - toolbarParams.iconColor = QColor(0x404040); - toolbarParams.iconDisabledColor = QColor(0x858585); - toolbarParams.iconCheckedColor = QColor(0x5A5A5A); - toolbarParams.backgroundColor = QColor(0xF3F3F3); - toolbarParams.separatorColor = QColor(0xCCCCCC); - toolbarParams.checkedButtonColor = QColor(0xCCCCCC); - toolbarParams.menuIndicatorColor = QColor(0x404040); - - params.toolbarParams = toolbarParams; - - ViewerParams viewerParams; - viewerParams.defaultBackgroundColor = QColor(0x282828); - viewerParams.defaultTextColor = Qt::white; - viewerParams.infoBackgroundColor = QColor::fromRgba(0xBB000000); - viewerParams.infoTextColor = Qt::white; - viewerParams.t = ViewerThemeTemplates(); - - params.viewerParams = viewerParams; - - GoToFlowWidgetParams goToFlowWidgetParams; - goToFlowWidgetParams.flowBackgroundColor = QColor(0x282828); - goToFlowWidgetParams.flowTextColor = Qt::white; - goToFlowWidgetParams.toolbarBackgroundColor = QColor::fromRgba(0x99000000); - goToFlowWidgetParams.sliderBorderColor = QColor::fromRgba(0x22FFFFFF); - goToFlowWidgetParams.sliderGrooveColor = QColor::fromRgba(0x77000000); - goToFlowWidgetParams.sliderHandleColor = QColor::fromRgba(0x55FFFFFF); - goToFlowWidgetParams.editBorderColor = QColor::fromRgba(0x77000000); - goToFlowWidgetParams.editBackgroundColor = QColor::fromRgba(0x55000000); - goToFlowWidgetParams.editTextColor = Qt::white; - goToFlowWidgetParams.labelTextColor = Qt::white; - goToFlowWidgetParams.iconColor = Qt::white; - goToFlowWidgetParams.t = GoToFlowWidgetThemeTemplates(); - - params.goToFlowWidgetParams = goToFlowWidgetParams; - - params.helpAboutDialogParams.headingColor = QColor(0x302f2d); - params.helpAboutDialogParams.linkColor = QColor(0xC19441); - - params.whatsNewDialogParams.backgroundColor = QColor(0xFFFFFF); - params.whatsNewDialogParams.headerTextColor = QColor(0x0A0A0A); - params.whatsNewDialogParams.versionTextColor = QColor(0x858585); - params.whatsNewDialogParams.contentTextColor = QColor(0x0A0A0A); - params.whatsNewDialogParams.linkColor = QColor(0xE8B800); - params.whatsNewDialogParams.closeButtonColor = QColor(0x444444); - params.whatsNewDialogParams.headerDecorationColor = QColor(0xE8B800); - - // ShortcutsIcons - ShortcutsIconsParams sci; - sci.iconColor = QColor(0x404040); // Dark icons for light background - params.shortcutsIconsParams = sci; - - return params; -} - -ThemeParams lightThemeParams() -{ - ThemeParams params; - params.themeName = "light"; - - ToolbarParams toolbarParams; - toolbarParams.iconColor = QColor(0x404040); - toolbarParams.iconDisabledColor = QColor(0xB0B0B0); - toolbarParams.iconCheckedColor = QColor(0x5A5A5A); - toolbarParams.backgroundColor = QColor(0xF3F3F3); - toolbarParams.separatorColor = QColor(0xCCCCCC); - toolbarParams.checkedButtonColor = QColor(0xCCCCCC); - toolbarParams.menuIndicatorColor = QColor(0x404040); - - params.toolbarParams = toolbarParams; - - ViewerParams viewerParams; - viewerParams.defaultBackgroundColor = QColor(0xF6F6F6); - viewerParams.defaultTextColor = QColor(0x202020); - viewerParams.infoBackgroundColor = QColor::fromRgba(0xBBFFFFFF); - viewerParams.infoTextColor = QColor(0x404040); - viewerParams.t = ViewerThemeTemplates(); - - params.viewerParams = viewerParams; - - GoToFlowWidgetParams goToFlowWidgetParams; - goToFlowWidgetParams.flowBackgroundColor = QColor(0xF6F6F6); - goToFlowWidgetParams.flowTextColor = QColor(0x202020); - goToFlowWidgetParams.toolbarBackgroundColor = QColor::fromRgba(0xBBFFFFFF); - goToFlowWidgetParams.sliderBorderColor = QColor::fromRgba(0x22000000); - goToFlowWidgetParams.sliderGrooveColor = QColor::fromRgba(0x33000000); - goToFlowWidgetParams.sliderHandleColor = QColor::fromRgba(0x55000000); - goToFlowWidgetParams.editBorderColor = QColor::fromRgba(0x33000000); - goToFlowWidgetParams.editBackgroundColor = QColor::fromRgba(0x22000000); - goToFlowWidgetParams.editTextColor = QColor(0x202020); - goToFlowWidgetParams.labelTextColor = QColor(0x202020); - goToFlowWidgetParams.iconColor = QColor(0x404040); - goToFlowWidgetParams.t = GoToFlowWidgetThemeTemplates(); - - params.goToFlowWidgetParams = goToFlowWidgetParams; - - params.helpAboutDialogParams.headingColor = QColor(0x302f2d); - params.helpAboutDialogParams.linkColor = QColor(0xC19441); - - params.whatsNewDialogParams.backgroundColor = QColor(0xFFFFFF); - params.whatsNewDialogParams.headerTextColor = QColor(0x0A0A0A); - params.whatsNewDialogParams.versionTextColor = QColor(0x858585); - params.whatsNewDialogParams.contentTextColor = QColor(0x0A0A0A); - params.whatsNewDialogParams.linkColor = QColor(0xE8B800); - params.whatsNewDialogParams.closeButtonColor = QColor(0x444444); - params.whatsNewDialogParams.headerDecorationColor = QColor(0xE8B800); - - // ShortcutsIcons - ShortcutsIconsParams sci; - sci.iconColor = QColor(0x606060); // Dark icons for light background - params.shortcutsIconsParams = sci; - - return params; -} - -ThemeParams darkThemeParams() -{ - ThemeParams params; - params.themeName = "dark"; - - ToolbarParams toolbarParams; - toolbarParams.iconColor = QColor(0xCCCCCC); - toolbarParams.iconDisabledColor = QColor(0x444444); - toolbarParams.iconCheckedColor = QColor(0xDADADA); - toolbarParams.backgroundColor = QColor(0x202020); - toolbarParams.separatorColor = QColor(0x444444); - toolbarParams.checkedButtonColor = QColor(0x3A3A3A); - toolbarParams.menuIndicatorColor = QColor(0xCCCCCC); - - params.toolbarParams = toolbarParams; - - ViewerParams viewerParams; - viewerParams.defaultBackgroundColor = QColor(40, 40, 40); - viewerParams.defaultTextColor = Qt::white; - viewerParams.infoBackgroundColor = QColor::fromRgba(0xBB000000); - viewerParams.infoTextColor = QColor(0xB0B0B0); - viewerParams.t = ViewerThemeTemplates(); - - params.viewerParams = viewerParams; - - GoToFlowWidgetParams goToFlowWidgetParams; - goToFlowWidgetParams.flowBackgroundColor = QColor(40, 40, 40); - goToFlowWidgetParams.flowTextColor = Qt::white; - goToFlowWidgetParams.toolbarBackgroundColor = QColor::fromRgba(0x99000000); - goToFlowWidgetParams.sliderBorderColor = QColor::fromRgba(0x22FFFFFF); - goToFlowWidgetParams.sliderGrooveColor = QColor::fromRgba(0x77000000); - goToFlowWidgetParams.sliderHandleColor = QColor::fromRgba(0x55FFFFFF); - goToFlowWidgetParams.editBorderColor = QColor::fromRgba(0x77000000); - goToFlowWidgetParams.editBackgroundColor = QColor::fromRgba(0x55000000); - goToFlowWidgetParams.editTextColor = Qt::white; - goToFlowWidgetParams.labelTextColor = Qt::white; - goToFlowWidgetParams.iconColor = QColor(0xCCCCCC); - goToFlowWidgetParams.t = GoToFlowWidgetThemeTemplates(); - - params.goToFlowWidgetParams = goToFlowWidgetParams; - - params.helpAboutDialogParams.headingColor = QColor(0xE0E0E0); - params.helpAboutDialogParams.linkColor = QColor(0xD4A84B); - - params.whatsNewDialogParams.backgroundColor = QColor(0x2A2A2A); - params.whatsNewDialogParams.headerTextColor = QColor(0xE0E0E0); - params.whatsNewDialogParams.versionTextColor = QColor(0x858585); - params.whatsNewDialogParams.contentTextColor = QColor(0xE0E0E0); - params.whatsNewDialogParams.linkColor = QColor(0xE8B800); - params.whatsNewDialogParams.closeButtonColor = QColor(0xDDDDDD); - params.whatsNewDialogParams.headerDecorationColor = QColor(0xE8B800); - - // ShortcutsIcons - ShortcutsIconsParams sci; - sci.iconColor = QColor(0xD0D0D0); // Light icons for dark background - params.shortcutsIconsParams = sci; - - return params; -} - -// TODO -ThemeParams paramsFromFile(const QString &filePath) -{ - return {}; + + if (json.contains("viewer")) { + const auto v = json["viewer"].toObject(); + auto &vp = p.viewerParams; + vp.defaultBackgroundColor = colorFromJson(v, "defaultBackgroundColor", vp.defaultBackgroundColor); + vp.defaultTextColor = colorFromJson(v, "defaultTextColor", vp.defaultTextColor); + vp.infoBackgroundColor = colorFromJson(v, "infoBackgroundColor", vp.infoBackgroundColor); + vp.infoTextColor = colorFromJson(v, "infoTextColor", vp.infoTextColor); + } + + if (json.contains("goToFlowWidget")) { + const auto g = json["goToFlowWidget"].toObject(); + auto &gp = p.goToFlowWidgetParams; + gp.flowBackgroundColor = colorFromJson(g, "flowBackgroundColor", gp.flowBackgroundColor); + gp.flowTextColor = colorFromJson(g, "flowTextColor", gp.flowTextColor); + gp.toolbarBackgroundColor = colorFromJson(g, "toolbarBackgroundColor", gp.toolbarBackgroundColor); + gp.sliderBorderColor = colorFromJson(g, "sliderBorderColor", gp.sliderBorderColor); + gp.sliderGrooveColor = colorFromJson(g, "sliderGrooveColor", gp.sliderGrooveColor); + gp.sliderHandleColor = colorFromJson(g, "sliderHandleColor", gp.sliderHandleColor); + gp.editBorderColor = colorFromJson(g, "editBorderColor", gp.editBorderColor); + gp.editBackgroundColor = colorFromJson(g, "editBackgroundColor", gp.editBackgroundColor); + gp.editTextColor = colorFromJson(g, "editTextColor", gp.editTextColor); + gp.labelTextColor = colorFromJson(g, "labelTextColor", gp.labelTextColor); + gp.iconColor = colorFromJson(g, "iconColor", gp.iconColor); + } + + if (json.contains("helpAboutDialog")) { + const auto h = json["helpAboutDialog"].toObject(); + p.helpAboutDialogParams.headingColor = colorFromJson(h, "headingColor", p.helpAboutDialogParams.headingColor); + p.helpAboutDialogParams.linkColor = colorFromJson(h, "linkColor", p.helpAboutDialogParams.linkColor); + } + + if (json.contains("whatsNewDialog")) { + const auto w = json["whatsNewDialog"].toObject(); + auto &wn = p.whatsNewDialogParams; + wn.backgroundColor = colorFromJson(w, "backgroundColor", wn.backgroundColor); + wn.headerTextColor = colorFromJson(w, "headerTextColor", wn.headerTextColor); + wn.versionTextColor = colorFromJson(w, "versionTextColor", wn.versionTextColor); + wn.contentTextColor = colorFromJson(w, "contentTextColor", wn.contentTextColor); + wn.linkColor = colorFromJson(w, "linkColor", wn.linkColor); + wn.closeButtonColor = colorFromJson(w, "closeButtonColor", wn.closeButtonColor); + wn.headerDecorationColor = colorFromJson(w, "headerDecorationColor",wn.headerDecorationColor); + } + + if (json.contains("shortcutsIcons")) { + const auto s = json["shortcutsIcons"].toObject(); + p.shortcutsIconsParams.iconColor = colorFromJson(s, "iconColor", p.shortcutsIconsParams.iconColor); + } + + if (json.contains("meta")) { + const auto o = json["meta"].toObject(); + p.meta.id = o["id"].toString(p.meta.id); + p.meta.displayName = o["displayName"].toString(p.meta.displayName); + const QString variantStr = o["variant"].toString(); + if (variantStr == "light") + p.meta.variant = ThemeVariant::Light; + else if (variantStr == "dark") + p.meta.variant = ThemeVariant::Dark; + } + + Theme theme = makeTheme(p); + theme.sourceJson = json; + return theme; } diff --git a/YACReader/themes/theme_factory.h b/YACReader/themes/theme_factory.h index 72cb4ca2..ebed9bbb 100644 --- a/YACReader/themes/theme_factory.h +++ b/YACReader/themes/theme_factory.h @@ -2,8 +2,9 @@ #define THEME_FACTORY_H #include "theme.h" -#include "theme_id.h" -Theme makeTheme(ThemeId themeId); +#include + +Theme makeTheme(const QJsonObject &json); #endif // THEME_FACTORY_H diff --git a/YACReader/themes/themes.qrc b/YACReader/themes/themes.qrc new file mode 100644 index 00000000..62ec21fe --- /dev/null +++ b/YACReader/themes/themes.qrc @@ -0,0 +1,7 @@ + + + builtin_classic.json + builtin_light.json + builtin_dark.json + + diff --git a/YACReaderLibrary/CMakeLists.txt b/YACReaderLibrary/CMakeLists.txt index cae71e34..485c1191 100644 --- a/YACReaderLibrary/CMakeLists.txt +++ b/YACReaderLibrary/CMakeLists.txt @@ -203,10 +203,14 @@ target_compile_definitions(YACReaderLibrary PRIVATE qt_add_resources(yacreaderlibrary_images_rcc "${CMAKE_CURRENT_SOURCE_DIR}/images.qrc") qt_add_resources(yacreaderlibrary_files_rcc "${CMAKE_CURRENT_SOURCE_DIR}/files.qrc") qt_add_resources(yacreaderlibrary_qml_rcc "${CMAKE_CURRENT_SOURCE_DIR}/qml.qrc") +qt_add_resources(yacreaderlibrary_themes_rcc "${CMAKE_CURRENT_SOURCE_DIR}/themes/themes.qrc") +qt_add_resources(yacreaderlibrary_common_images_rcc "${CMAKE_SOURCE_DIR}/common/themes/appearance_config_images.qrc") target_sources(YACReaderLibrary PRIVATE ${yacreaderlibrary_images_rcc} ${yacreaderlibrary_files_rcc} ${yacreaderlibrary_qml_rcc} + ${yacreaderlibrary_themes_rcc} + ${yacreaderlibrary_common_images_rcc} ) if(WIN32 OR (UNIX AND NOT APPLE)) qt_add_resources(yacreaderlibrary_images_win_rcc "${CMAKE_CURRENT_SOURCE_DIR}/images_win.qrc") diff --git a/YACReaderLibrary/info_comics_view.cpp b/YACReaderLibrary/info_comics_view.cpp index a93aeb16..bb58b5bf 100644 --- a/YACReaderLibrary/info_comics_view.cpp +++ b/YACReaderLibrary/info_comics_view.cpp @@ -227,10 +227,18 @@ void InfoComicsView::applyTheme(const Theme &theme) const auto &qv = theme.qmlView; // Info panel colors + // Cache-bust the SVG file URLs so QML's image cache doesn't serve stale + // files when the theme is updated (the same file path is rewritten each time). + const QString bust = QString::number(QDateTime::currentMSecsSinceEpoch()); + auto svgUrl = [&bust](const QString &path) { + QUrl url = QUrl::fromLocalFile(path); + url.setQuery(bust); + return url; + }; ctxt->setContextProperty("infoBackgroundColor", qv.infoBackgroundColor); - ctxt->setContextProperty("topShadow", QUrl::fromLocalFile(qv.topShadow)); - ctxt->setContextProperty("infoShadow", QUrl::fromLocalFile(qv.infoShadow)); - ctxt->setContextProperty("infoIndicator", QUrl::fromLocalFile(qv.infoIndicator)); + ctxt->setContextProperty("topShadow", svgUrl(qv.topShadow)); + ctxt->setContextProperty("infoShadow", svgUrl(qv.infoShadow)); + ctxt->setContextProperty("infoIndicator", svgUrl(qv.infoIndicator)); ctxt->setContextProperty("infoTextColor", qv.infoTextColor); ctxt->setContextProperty("infoTitleColor", qv.infoTitleColor); diff --git a/YACReaderLibrary/main.cpp b/YACReaderLibrary/main.cpp index 5507e25e..4d82ea86 100644 --- a/YACReaderLibrary/main.cpp +++ b/YACReaderLibrary/main.cpp @@ -21,7 +21,9 @@ #include "db_helper.h" #include "yacreader_libraries.h" #include "exit_check.h" +#include "appearance_configuration.h" #include "theme_manager.h" +#include "theme_repository.h" #ifdef Q_OS_MACOS #include "trayhandler.h" #endif @@ -140,7 +142,11 @@ int main(int argc, char **argv) app.setApplicationVersion(VERSION); // Theme initialization - ThemeManager::instance().initialize(); + auto *appearanceConfig = new AppearanceConfiguration( + YACReader::getSettingsPath() + "/YACReaderLibrary.ini", qApp); + auto *themeRepo = new ThemeRepository( + ":/themes", YACReader::getSettingsPath() + "/themes/user"); + ThemeManager::instance().initialize(appearanceConfig, themeRepo); // Set window icon according to Freedesktop icon specification // This is mostly relevant for Linux and other Unix systems diff --git a/YACReaderLibrary/options_dialog.cpp b/YACReaderLibrary/options_dialog.cpp index 53210ac8..1087d8ad 100644 --- a/YACReaderLibrary/options_dialog.cpp +++ b/YACReaderLibrary/options_dialog.cpp @@ -4,6 +4,11 @@ #include "api_key_dialog.h" #include "yacreader_global_gui.h" +#include "theme_manager.h" +#include "theme_factory.h" +#include "appearance_tab_widget.h" + +#include FlowType flowType = Strip; @@ -15,11 +20,14 @@ OptionsDialog::OptionsDialog(QWidget *parent) auto comicFlowW = createFlowTab(); auto gridViewW = createGridTab(); + auto appearanceW = createAppearanceTab(); + auto tabWidget = new QTabWidget(); tabWidget->addTab(generalW, tr("General")); tabWidget->addTab(librariesW, tr("Libraries")); tabWidget->addTab(comicFlowW, tr("Comic Flow")); tabWidget->addTab(gridViewW, tr("Grid view")); + tabWidget->addTab(appearanceW, tr("Appearance")); auto buttons = new QHBoxLayout(); buttons->addStretch(); @@ -406,3 +414,12 @@ QWidget *OptionsDialog::createGridTab() return gridViewW; } + +QWidget *OptionsDialog::createAppearanceTab() +{ + return new AppearanceTabWidget( + ThemeManager::instance().getAppearanceConfiguration(), + []() { return ThemeManager::instance().getCurrentTheme().sourceJson; }, + [](const QJsonObject &json) { ThemeManager::instance().setTheme(makeTheme(json)); }, + this); +} diff --git a/YACReaderLibrary/options_dialog.h b/YACReaderLibrary/options_dialog.h index ccf8a2ff..84caae53 100644 --- a/YACReaderLibrary/options_dialog.h +++ b/YACReaderLibrary/options_dialog.h @@ -61,6 +61,7 @@ private: QWidget *createLibrariesTab(); QWidget *createFlowTab(); QWidget *createGridTab(); + QWidget *createAppearanceTab(); }; #endif diff --git a/YACReaderLibrary/themes/builtin_classic.json b/YACReaderLibrary/themes/builtin_classic.json new file mode 100644 index 00000000..a365fbb8 --- /dev/null +++ b/YACReaderLibrary/themes/builtin_classic.json @@ -0,0 +1,238 @@ +{ + "meta": { + "id": "builtin/classic", + "displayName": "Default Classic", + "variant": "dark" + }, + "defaultContentBackgroundColor": "#2a2a2a", + "comicFlow": { + "backgroundColor": "#000000", + "textColor": "#4c4c4c" + }, + "comicVine": { + "contentTextColor": "#ffffff", + "contentBackgroundColor": "#2b2b2b", + "contentAltBackgroundColor": "#2b2b2b", + "dialogBackgroundColor": "#404040", + "tableBackgroundColor": "#2b2b2b", + "tableAltBackgroundColor": "#2e2e2e", + "tableBorderColor": "#242424", + "tableSelectedColor": "#555555", + "tableHeaderBackgroundColor": "#292929", + "tableHeaderGradientColor": "#292929", + "tableHeaderBorderColor": "#1f1f1f", + "tableHeaderTextColor": "#ebebeb", + "tableScrollHandleColor": "#dddddd", + "tableScrollBackgroundColor": "#404040", + "tableSectionBorderLight": "#fefefe", + "tableSectionBorderDark": "#dfdfdf", + "labelTextColor": "#ffffff", + "labelBackgroundColor": "#2b2b2b", + "hyperlinkColor": "#ffcc00", + "buttonBackgroundColor": "#2e2e2e", + "buttonTextColor": "#ffffff", + "buttonBorderColor": "#242424", + "radioUncheckedColor": "#e5e5e5", + "radioCheckedBackgroundColor": "#e5e5e5", + "radioCheckedIndicatorColor": "#5f5f5f", + "checkBoxTickColor": "#ffffff", + "toolButtonAccentColor": "#282828", + "downArrowColor": "#9f9f9f", + "upArrowColor": "#9f9f9f", + "busyIndicatorColor": "#ffffff", + "navIconColor": "#ffffff", + "rowIconColor": "#e5e5e5" + }, + "helpAboutDialog": { + "headingColor": "#302f2d", + "linkColor": "#c19441" + }, + "whatsNewDialog": { + "backgroundColor": "#2a2a2a", + "headerTextColor": "#e0e0e0", + "versionTextColor": "#858585", + "contentTextColor": "#e0e0e0", + "linkColor": "#e8b800", + "closeButtonColor": "#dddddd", + "headerDecorationColor": "#e8b800" + }, + "emptyContainer": { + "backgroundColor": "#2a2a2a", + "titleTextColor": "#cccccc", + "textColor": "#cccccc", + "descriptionTextColor": "#aaaaaa", + "searchIconColor": "#4c4c4c" + }, + "sidebar": { + "backgroundColor": "#454545", + "separatorColor": "#bdbfbf", + "sectionSeparatorColor": "#575757", + "uppercaseLabels": true, + "titleTextColor": "#bdbfbf", + "titleDropShadowColor": "#000000", + "busyIndicatorColor": "#ffffff" + }, + "sidebarIcons": { + "iconColor": "#e0e0e0", + "shadowColor": "#000000", + "useSystemFolderIcons": false + }, + "libraryItem": { + "textColor": "#dddfdf", + "libraryIconColor": "#dddfdf", + "libraryIconShadowColor": "#000000", + "selectedTextColor": "#ffffff", + "selectedBackgroundColor": "#2e2e2e", + "libraryIconSelectedColor": "#ffffff", + "libraryOptionsIconColor": "#ffffff" + }, + "importWidget": { + "backgroundColor": "#2a2a2a", + "titleTextColor": "#cccccc", + "descriptionTextColor": "#aaaaaa", + "currentComicTextColor": "#aaaaaa", + "coversViewBackgroundColor": "#3a3a3a", + "coversLabelColor": "#aaaaaa", + "coversDecorationBgColor": "#3a3a3a", + "coversDecorationShadowColor": "#1a1a1a", + "modeIconColor": "#4a4a4a", + "iconColor": "#cccccc", + "iconCheckedColor": "#aaaaaa" + }, + "serverConfigDialog": { + "backgroundColor": "#2a2a2a", + "titleTextColor": "#474747", + "qrMessageTextColor": "#a3a3a3", + "propagandaTextColor": "#4d4d4d", + "labelTextColor": "#575757", + "checkBoxTextColor": "#262626", + "qrBackgroundColor": "#2a2a2a", + "qrForegroundColor": "#ffffff", + "decorationColor": "#f7f7f7" + }, + "mainToolbar": { + "backgroundColor": "#f0f0f0", + "folderNameColor": "#404040", + "dividerColor": "#b8bdc4", + "iconColor": "#404040", + "iconDisabledColor": "#b0b0b0" + }, + "contentSplitter": { + "handleColor": "#b8b8b8", + "horizontalHandleHeight": 4, + "verticalHandleWidth": 4 + }, + "treeView": { + "textColor": "#dddfdf", + "selectionBackgroundColor": "#2e2e2e", + "scrollBackgroundColor": "#404040", + "scrollHandleColor": "#dddddd", + "selectedTextColor": "#ffffff", + "folderIndicatorColor": "#edc518", + "branchIndicatorColor": "#e0e0e0", + "branchIndicatorSelectedColor": "#ffffff", + "folderIconColor": "#e0e0e0", + "folderIconShadowColor": "#000000", + "folderIconSelectedColor": "#e0e0e0", + "folderIconSelectedShadowColor": "#000000", + "folderReadOverlayColor": "#464646", + "folderReadOverlaySelectedColor": "#464646" + }, + "tableView": { + "alternateBackgroundColor": "#f2f2f2", + "backgroundColor": "#fafafa", + "headerBackgroundColor": "#f5f5f5", + "headerBorderColor": "#b8bdc4", + "headerGradientColor": "#d1d1d1", + "itemBorderBottomColor": "#dfdfdf", + "itemBorderTopColor": "#fefefe", + "itemBorderBottomWidth": 1, + "itemBorderTopWidth": 1, + "itemTextColor": "#252626", + "selectedColor": "#d4d4d4", + "selectedTextColor": "#252626", + "headerTextColor": "#313232", + "starRatingColor": "#e9be0f", + "starRatingSelectedColor": "#ffffff" + }, + "qmlView": { + "backgroundColor": "#2a2a2a", + "cellColor": "#212121", + "cellColorWithBackground": "#99212121", + "selectedColor": "#121212", + "selectedBorderColor": "#ffcc00", + "borderColor": "#121212", + "titleColor": "#ffffff", + "textColor": "#a8a8a8", + "showDropShadow": true, + "infoBackgroundColor": "#2e2e2e", + "infoBorderColor": "#404040", + "infoShadowColor": "#000000", + "infoTextColor": "#b0b0b0", + "infoTitleColor": "#ffffff", + "ratingUnselectedColor": "#1c1c1c", + "ratingSelectedColor": "#ffffff", + "favUncheckedColor": "#1c1c1c", + "favCheckedColor": "#e84852", + "readTickUncheckedColor": "#1c1c1c", + "readTickCheckedColor": "#e84852", + "currentComicBackgroundColor": "#88000000", + "continueReadingBackgroundColor": "#88000000", + "continueReadingColor": "#ffffff", + "backgroundBlurOverlayColor": "#2a2a2a" + }, + "comicsViewToolbar": { + "backgroundColor": "#f0f0f0", + "separatorColor": "#cccccc", + "checkedBackgroundColor": "#cccccc", + "iconColor": "#404040" + }, + "searchLineEdit": { + "textColor": "#ababab", + "backgroundColor": "#404040", + "iconColor": "#f7f7f7" + }, + "readingListIcons": { + "labelColors": { + "red": "#f67a7b", + "orange": "#f5c240", + "yellow": "#f2e446", + "green": "#ade738", + "cyan": "#a0fddb", + "blue": "#82c7ff", + "violet": "#8f95ff", + "purple": "#d692fc", + "pink": "#fd9cda", + "white": "#fcfcfc", + "light": "#cbcbcb", + "dark": "#b7b7b7" + }, + "labelShadowColor": "#000000", + "labelShadowSelectedColor": "#000000", + "readingListMainColor": "#e7e7e7", + "readingListMainSelectedColor": "#e7e7e7", + "favoritesMainColor": "#e15055", + "favoritesMainSelectedColor": "#e15055", + "currentlyReadingMainColor": "#ffcc00", + "currentlyReadingMainSelectedColor": "#ffcc00", + "currentlyReadingOuterColor": "#000000", + "currentlyReadingOuterSelectedColor": "#000000", + "specialListShadowColor": "#000000", + "specialListShadowSelectedColor": "#000000", + "listMainColor": "#e7e7e7", + "listMainSelectedColor": "#e7e7e7", + "listShadowColor": "#000000", + "listShadowSelectedColor": "#000000", + "listDetailColor": "#464646", + "listDetailSelectedColor": "#464646" + }, + "dialogIcons": { + "iconColor": "#f7f7f7" + }, + "menuIcons": { + "iconColor": "#f7f7f7" + }, + "shortcutsIcons": { + "iconColor": "#f7f7f7" + } +} diff --git a/YACReaderLibrary/themes/builtin_dark.json b/YACReaderLibrary/themes/builtin_dark.json new file mode 100644 index 00000000..21855aca --- /dev/null +++ b/YACReaderLibrary/themes/builtin_dark.json @@ -0,0 +1,238 @@ +{ + "meta": { + "id": "builtin/dark", + "displayName": "Default Dark", + "variant": "dark" + }, + "defaultContentBackgroundColor": "#2a2a2a", + "comicFlow": { + "backgroundColor": "#111111", + "textColor": "#888888" + }, + "comicVine": { + "contentTextColor": "#ffffff", + "contentBackgroundColor": "#2b2b2b", + "contentAltBackgroundColor": "#2e2e2e", + "dialogBackgroundColor": "#404040", + "tableBackgroundColor": "#2b2b2b", + "tableAltBackgroundColor": "#2e2e2e", + "tableBorderColor": "#242424", + "tableSelectedColor": "#555555", + "tableHeaderBackgroundColor": "#292929", + "tableHeaderGradientColor": "#292929", + "tableHeaderBorderColor": "#1f1f1f", + "tableHeaderTextColor": "#ebebeb", + "tableScrollHandleColor": "#dddddd", + "tableScrollBackgroundColor": "#404040", + "tableSectionBorderLight": "#fefefe", + "tableSectionBorderDark": "#dfdfdf", + "labelTextColor": "#ffffff", + "labelBackgroundColor": "#2b2b2b", + "hyperlinkColor": "#ffcc00", + "buttonBackgroundColor": "#2e2e2e", + "buttonTextColor": "#ffffff", + "buttonBorderColor": "#242424", + "radioUncheckedColor": "#e5e5e5", + "radioCheckedBackgroundColor": "#e5e5e5", + "radioCheckedIndicatorColor": "#5f5f5f", + "checkBoxTickColor": "#ffffff", + "toolButtonAccentColor": "#282828", + "downArrowColor": "#9f9f9f", + "upArrowColor": "#9f9f9f", + "busyIndicatorColor": "#ffffff", + "navIconColor": "#ffffff", + "rowIconColor": "#e5e5e5" + }, + "helpAboutDialog": { + "headingColor": "#e0e0e0", + "linkColor": "#d4a84b" + }, + "whatsNewDialog": { + "backgroundColor": "#2a2a2a", + "headerTextColor": "#e0e0e0", + "versionTextColor": "#858585", + "contentTextColor": "#e0e0e0", + "linkColor": "#e8b800", + "closeButtonColor": "#dddddd", + "headerDecorationColor": "#e8b800" + }, + "emptyContainer": { + "backgroundColor": "#2a2a2a", + "titleTextColor": "#cccccc", + "textColor": "#cccccc", + "descriptionTextColor": "#aaaaaa", + "searchIconColor": "#4c4c4c" + }, + "sidebar": { + "backgroundColor": "#454545", + "separatorColor": "#bdbfbf", + "sectionSeparatorColor": "#575757", + "uppercaseLabels": true, + "titleTextColor": "#bdbfbf", + "titleDropShadowColor": "#000000", + "busyIndicatorColor": "#ffffff" + }, + "sidebarIcons": { + "iconColor": "#e0e0e0", + "shadowColor": "#000000", + "useSystemFolderIcons": false + }, + "libraryItem": { + "textColor": "#dddfdf", + "libraryIconColor": "#dddfdf", + "libraryIconShadowColor": "#000000", + "selectedTextColor": "#ffffff", + "selectedBackgroundColor": "#2e2e2e", + "libraryIconSelectedColor": "#ffffff", + "libraryOptionsIconColor": "#ffffff" + }, + "importWidget": { + "backgroundColor": "#2a2a2a", + "titleTextColor": "#cccccc", + "descriptionTextColor": "#aaaaaa", + "currentComicTextColor": "#aaaaaa", + "coversViewBackgroundColor": "#3a3a3a", + "coversLabelColor": "#aaaaaa", + "coversDecorationBgColor": "#3a3a3a", + "coversDecorationShadowColor": "#1a1a1a", + "modeIconColor": "#4a4a4a", + "iconColor": "#cccccc", + "iconCheckedColor": "#aaaaaa" + }, + "serverConfigDialog": { + "backgroundColor": "#2a2a2a", + "titleTextColor": "#d0d0d0", + "qrMessageTextColor": "#a3a3a3", + "propagandaTextColor": "#b0b0b0", + "labelTextColor": "#c0c0c0", + "checkBoxTextColor": "#dddddd", + "qrBackgroundColor": "#2a2a2a", + "qrForegroundColor": "#ffffff", + "decorationColor": "#f7f7f7" + }, + "mainToolbar": { + "backgroundColor": "#2a2a2a", + "folderNameColor": "#dddddd", + "dividerColor": "#555555", + "iconColor": "#dddddd", + "iconDisabledColor": "#666666" + }, + "contentSplitter": { + "handleColor": "#1f1f1f", + "horizontalHandleHeight": 4, + "verticalHandleWidth": 4 + }, + "treeView": { + "textColor": "#dddfdf", + "selectionBackgroundColor": "#2e2e2e", + "scrollBackgroundColor": "#404040", + "scrollHandleColor": "#dddddd", + "selectedTextColor": "#ffffff", + "folderIndicatorColor": "#edc518", + "branchIndicatorColor": "#e0e0e0", + "branchIndicatorSelectedColor": "#ffffff", + "folderIconColor": "#e0e0e0", + "folderIconShadowColor": "#000000", + "folderIconSelectedColor": "#e0e0e0", + "folderIconSelectedShadowColor": "#000000", + "folderReadOverlayColor": "#222222", + "folderReadOverlaySelectedColor": "#222222" + }, + "tableView": { + "alternateBackgroundColor": "#2e2e2e", + "backgroundColor": "#2a2a2a", + "headerBackgroundColor": "#2a2a2a", + "headerBorderColor": "#1f1f1f", + "headerGradientColor": "#252525", + "itemBorderBottomColor": "#1f1f1f", + "itemBorderTopColor": "#353535", + "itemBorderBottomWidth": 1, + "itemBorderTopWidth": 1, + "itemTextColor": "#dddddd", + "selectedColor": "#555555", + "selectedTextColor": "#ffffff", + "headerTextColor": "#dddddd", + "starRatingColor": "#e9be0f", + "starRatingSelectedColor": "#ffffff" + }, + "qmlView": { + "backgroundColor": "#2a2a2a", + "cellColor": "#212121", + "cellColorWithBackground": "#99212121", + "selectedColor": "#121212", + "selectedBorderColor": "#ffcc00", + "borderColor": "#121212", + "titleColor": "#ffffff", + "textColor": "#a8a8a8", + "showDropShadow": true, + "infoBackgroundColor": "#2e2e2e", + "infoBorderColor": "#404040", + "infoShadowColor": "#000000", + "infoTextColor": "#b0b0b0", + "infoTitleColor": "#ffffff", + "ratingUnselectedColor": "#1c1c1c", + "ratingSelectedColor": "#ffffff", + "favUncheckedColor": "#1c1c1c", + "favCheckedColor": "#e84852", + "readTickUncheckedColor": "#1c1c1c", + "readTickCheckedColor": "#e84852", + "currentComicBackgroundColor": "#88000000", + "continueReadingBackgroundColor": "#88000000", + "continueReadingColor": "#ffffff", + "backgroundBlurOverlayColor": "#2a2a2a" + }, + "comicsViewToolbar": { + "backgroundColor": "#2a2a2a", + "separatorColor": "#444444", + "checkedBackgroundColor": "#555555", + "iconColor": "#dddddd" + }, + "searchLineEdit": { + "textColor": "#ababab", + "backgroundColor": "#404040", + "iconColor": "#f7f7f7" + }, + "readingListIcons": { + "labelColors": { + "red": "#f67a7b", + "orange": "#f5c240", + "yellow": "#f2e446", + "green": "#ade738", + "cyan": "#a0fddb", + "blue": "#82c7ff", + "violet": "#8f95ff", + "purple": "#d692fc", + "pink": "#fd9cda", + "white": "#fcfcfc", + "light": "#cbcbcb", + "dark": "#b7b7b7" + }, + "labelShadowColor": "#000000", + "labelShadowSelectedColor": "#000000", + "readingListMainColor": "#e7e7e7", + "readingListMainSelectedColor": "#e7e7e7", + "favoritesMainColor": "#e15055", + "favoritesMainSelectedColor": "#e15055", + "currentlyReadingMainColor": "#ffcc00", + "currentlyReadingMainSelectedColor": "#ffcc00", + "currentlyReadingOuterColor": "#000000", + "currentlyReadingOuterSelectedColor": "#000000", + "specialListShadowColor": "#000000", + "specialListShadowSelectedColor": "#000000", + "listMainColor": "#e7e7e7", + "listMainSelectedColor": "#e7e7e7", + "listShadowColor": "#000000", + "listShadowSelectedColor": "#000000", + "listDetailColor": "#464646", + "listDetailSelectedColor": "#464646" + }, + "dialogIcons": { + "iconColor": "#f7f7f7" + }, + "menuIcons": { + "iconColor": "#f7f7f7" + }, + "shortcutsIcons": { + "iconColor": "#f7f7f7" + } +} diff --git a/YACReaderLibrary/themes/builtin_light.json b/YACReaderLibrary/themes/builtin_light.json new file mode 100644 index 00000000..4d9c8744 --- /dev/null +++ b/YACReaderLibrary/themes/builtin_light.json @@ -0,0 +1,238 @@ +{ + "meta": { + "id": "builtin/light", + "displayName": "Default Light", + "variant": "light" + }, + "defaultContentBackgroundColor": "#ffffff", + "comicFlow": { + "backgroundColor": "#dcdcdc", + "textColor": "#303030" + }, + "comicVine": { + "contentTextColor": "#000000", + "contentBackgroundColor": "#ececec", + "contentAltBackgroundColor": "#e0e0e0", + "dialogBackgroundColor": "#fbfbfb", + "tableBackgroundColor": "#f4f4f4", + "tableAltBackgroundColor": "#fafafa", + "tableBorderColor": "#cccccc", + "tableSelectedColor": "#dddddd", + "tableHeaderBackgroundColor": "#e0e0e0", + "tableHeaderGradientColor": "#e0e0e0", + "tableHeaderBorderColor": "#c0c0c0", + "tableHeaderTextColor": "#333333", + "tableScrollHandleColor": "#888888", + "tableScrollBackgroundColor": "#d0d0d0", + "tableSectionBorderLight": "#ffffff", + "tableSectionBorderDark": "#cccccc", + "labelTextColor": "#000000", + "labelBackgroundColor": "#ececec", + "hyperlinkColor": "#ffcc00", + "buttonBackgroundColor": "#e0e0e0", + "buttonTextColor": "#000000", + "buttonBorderColor": "#cccccc", + "radioUncheckedColor": "#e0e0e0", + "radioCheckedBackgroundColor": "#e0e0e0", + "radioCheckedIndicatorColor": "#222222", + "checkBoxTickColor": "#000000", + "toolButtonAccentColor": "#a0a0a0", + "downArrowColor": "#222222", + "upArrowColor": "#222222", + "busyIndicatorColor": "#000000", + "navIconColor": "#222222", + "rowIconColor": "#222222" + }, + "helpAboutDialog": { + "headingColor": "#302f2d", + "linkColor": "#c19441" + }, + "whatsNewDialog": { + "backgroundColor": "#ffffff", + "headerTextColor": "#0a0a0a", + "versionTextColor": "#858585", + "contentTextColor": "#0a0a0a", + "linkColor": "#e8b800", + "closeButtonColor": "#444444", + "headerDecorationColor": "#e8b800" + }, + "emptyContainer": { + "backgroundColor": "#ffffff", + "titleTextColor": "#888888", + "textColor": "#495252", + "descriptionTextColor": "#565959", + "searchIconColor": "#cccccc" + }, + "sidebar": { + "backgroundColor": "#fbfbfb", + "separatorColor": "#808080", + "sectionSeparatorColor": "#e0e0e0", + "uppercaseLabels": true, + "titleTextColor": "#4a494a", + "titleDropShadowColor": "#ffffff", + "busyIndicatorColor": "#808080" + }, + "sidebarIcons": { + "iconColor": "#4f4e4f", + "shadowColor": "#fbfbfb", + "useSystemFolderIcons": false + }, + "libraryItem": { + "textColor": "#000000", + "libraryIconColor": "#606060", + "libraryIconShadowColor": "#ffffff", + "selectedTextColor": "#ffffff", + "selectedBackgroundColor": "#333133", + "libraryIconSelectedColor": "#ffffff", + "libraryOptionsIconColor": "#ffffff" + }, + "importWidget": { + "backgroundColor": "#fafafa", + "titleTextColor": "#495252", + "descriptionTextColor": "#565959", + "currentComicTextColor": "#565959", + "coversViewBackgroundColor": "#e6e6e6", + "coversLabelColor": "#565959", + "coversDecorationBgColor": "#e6e6e6", + "coversDecorationShadowColor": "#a1a1a1", + "modeIconColor": "#e6e6e6", + "iconColor": "#495252", + "iconCheckedColor": "#565959" + }, + "serverConfigDialog": { + "backgroundColor": "#ffffff", + "titleTextColor": "#474747", + "qrMessageTextColor": "#a3a3a3", + "propagandaTextColor": "#4d4d4d", + "labelTextColor": "#575757", + "checkBoxTextColor": "#262626", + "qrBackgroundColor": "#ffffff", + "qrForegroundColor": "#606060", + "decorationColor": "#606060" + }, + "mainToolbar": { + "backgroundColor": "#f0f0f0", + "folderNameColor": "#333133", + "dividerColor": "#b8bdc4", + "iconColor": "#333133", + "iconDisabledColor": "#b0b0b0" + }, + "contentSplitter": { + "handleColor": "#f0f0f0", + "horizontalHandleHeight": 4, + "verticalHandleWidth": 4 + }, + "treeView": { + "textColor": "#000000", + "selectionBackgroundColor": "#333133", + "scrollBackgroundColor": "#e0e0e0", + "scrollHandleColor": "#888888", + "selectedTextColor": "#ffffff", + "folderIndicatorColor": "#555f7f", + "branchIndicatorColor": "#606060", + "branchIndicatorSelectedColor": "#ffffff", + "folderIconColor": "#606060", + "folderIconShadowColor": "#ffffff", + "folderIconSelectedColor": "#ffffff", + "folderIconSelectedShadowColor": "#161616", + "folderReadOverlayColor": "#ffffff", + "folderReadOverlaySelectedColor": "#161616" + }, + "tableView": { + "alternateBackgroundColor": "#f2f2f2", + "backgroundColor": "#fafafa", + "headerBackgroundColor": "#f5f5f5", + "headerBorderColor": "#b8bdc4", + "headerGradientColor": "#f5f5f5", + "itemBorderBottomColor": "#dfdfdf", + "itemBorderTopColor": "#fefefe", + "itemBorderBottomWidth": 0, + "itemBorderTopWidth": 0, + "itemTextColor": "#252626", + "selectedColor": "#595959", + "selectedTextColor": "#ffffff", + "headerTextColor": "#313232", + "starRatingColor": "#e9be0f", + "starRatingSelectedColor": "#ffffff" + }, + "qmlView": { + "backgroundColor": "#f6f6f6", + "cellColor": "#ffffff", + "cellColorWithBackground": "#99ffffff", + "selectedColor": "#ffffff", + "selectedBorderColor": "#ffcc00", + "borderColor": "#dbdbdb", + "titleColor": "#121212", + "textColor": "#636363", + "showDropShadow": true, + "infoBackgroundColor": "#ffffff", + "infoBorderColor": "#808080", + "infoShadowColor": "#444444", + "infoTextColor": "#404040", + "infoTitleColor": "#2e2e2e", + "ratingUnselectedColor": "#dedede", + "ratingSelectedColor": "#2b2b2b", + "favUncheckedColor": "#dedede", + "favCheckedColor": "#e84852", + "readTickUncheckedColor": "#dedede", + "readTickCheckedColor": "#e84852", + "currentComicBackgroundColor": "#88ffffff", + "continueReadingBackgroundColor": "#e8e8e8", + "continueReadingColor": "#000000", + "backgroundBlurOverlayColor": "#9e9e9e" + }, + "comicsViewToolbar": { + "backgroundColor": "#f0f0f0", + "separatorColor": "#cccccc", + "checkedBackgroundColor": "#cccccc", + "iconColor": "#404040" + }, + "searchLineEdit": { + "textColor": "#ffffff", + "backgroundColor": "#333133", + "iconColor": "#efefef" + }, + "readingListIcons": { + "labelColors": { + "red": "#f67a7b", + "orange": "#f5c240", + "yellow": "#f2e446", + "green": "#ade738", + "cyan": "#a0fddb", + "blue": "#82c7ff", + "violet": "#8f95ff", + "purple": "#d692fc", + "pink": "#fd9cda", + "white": "#fcfcfc", + "light": "#cbcbcb", + "dark": "#b7b7b7" + }, + "labelShadowColor": "#8f8f8f", + "labelShadowSelectedColor": "#161616", + "readingListMainColor": "#808080", + "readingListMainSelectedColor": "#808080", + "favoritesMainColor": "#e15055", + "favoritesMainSelectedColor": "#e15055", + "currentlyReadingMainColor": "#ffcc00", + "currentlyReadingMainSelectedColor": "#ffcc00", + "currentlyReadingOuterColor": "#000000", + "currentlyReadingOuterSelectedColor": "#000000", + "specialListShadowColor": "#8f8f8f", + "specialListShadowSelectedColor": "#161616", + "listMainColor": "#808080", + "listMainSelectedColor": "#ffffff", + "listShadowColor": "#8f8f8f", + "listShadowSelectedColor": "#161616", + "listDetailColor": "#ffffff", + "listDetailSelectedColor": "#161616" + }, + "dialogIcons": { + "iconColor": "#606060" + }, + "menuIcons": { + "iconColor": "#606060" + }, + "shortcutsIcons": { + "iconColor": "#606060" + } +} diff --git a/YACReaderLibrary/themes/theme.h b/YACReaderLibrary/themes/theme.h index 0d98c0ad..b0254429 100644 --- a/YACReaderLibrary/themes/theme.h +++ b/YACReaderLibrary/themes/theme.h @@ -2,10 +2,12 @@ #define THEME_H #include +#include #include "yacreader_icon.h" #include "help_about_dialog_theme.h" #include "whats_new_dialog_theme.h" +#include "theme_meta.h" struct ComicVineThemeTemplates { QString defaultLabelQSS = "QLabel {color:%1; font-size:12px;font-family:Arial;}"; @@ -452,6 +454,9 @@ struct ComicVineTheme { }; struct Theme { + ThemeMeta meta; + QJsonObject sourceJson; + QColor defaultContentBackgroundColor; ComicFlowColors comicFlow; diff --git a/YACReaderLibrary/themes/theme_factory.cpp b/YACReaderLibrary/themes/theme_factory.cpp index b9e103e2..1e0818cd 100644 --- a/YACReaderLibrary/themes/theme_factory.cpp +++ b/YACReaderLibrary/themes/theme_factory.cpp @@ -300,7 +300,7 @@ struct WhatsNewDialogParams { }; struct ThemeParams { - QString themeName; + ThemeMeta meta; QColor defaultContentBackgroundColor; ComicFlowColors comicFlowColors; @@ -342,13 +342,13 @@ Theme makeTheme(const ThemeParams ¶ms) const auto &t = cv.t; auto recolor = [&](const QString &path, const QColor &color) { - return recoloredSvgToThemeFile(path, color, params.themeName); + return recoloredSvgToThemeFile(path, color, params.meta.id); }; theme.comicVine.defaultLabelQSS = t.defaultLabelQSS.arg(cv.labelTextColor.name()); theme.comicVine.titleLabelQSS = t.titleLabelQSS.arg(cv.labelTextColor.name()); theme.comicVine.coverLabelQSS = t.coverLabelQSS.arg(cv.labelBackgroundColor.name(), cv.labelTextColor.name()); - theme.comicVine.radioButtonQSS = t.radioButtonQSS.arg(cv.buttonTextColor.name(), recolor(":/images/comic_vine/radioUnchecked.svg", cv.radioUncheckedColor), recoloredSvgToThemeFile(":/images/comic_vine/radioChecked.svg", cv.radioCheckedBackgroundColor, cv.radioCheckedIndicatorColor, params.themeName)); + theme.comicVine.radioButtonQSS = t.radioButtonQSS.arg(cv.buttonTextColor.name(), recolor(":/images/comic_vine/radioUnchecked.svg", cv.radioUncheckedColor), recoloredSvgToThemeFile(":/images/comic_vine/radioChecked.svg", cv.radioCheckedBackgroundColor, cv.radioCheckedIndicatorColor, params.meta.id)); theme.comicVine.checkBoxQSS = t.checkBoxQSS.arg(cv.buttonTextColor.name(), cv.buttonBorderColor.name(), cv.buttonBackgroundColor.name(), recolor(":/images/comic_vine/checkBoxTick.svg", cv.checkBoxTickColor)); theme.comicVine.scraperLineEditTitleLabelQSS = t.scraperLineEditTitleLabelQSS.arg(cv.contentTextColor.name()); @@ -399,8 +399,8 @@ Theme makeTheme(const ThemeParams ¶ms) theme.whatsNewDialog.versionTextColor = wn.versionTextColor; theme.whatsNewDialog.contentTextColor = wn.contentTextColor; theme.whatsNewDialog.linkColor = wn.linkColor; - theme.whatsNewDialog.closeButtonIcon = QPixmap(recoloredSvgToThemeFile(":/images/custom_dialog/custom_close_button.svg", wn.closeButtonColor, params.themeName)); - theme.whatsNewDialog.headerDecoration = QPixmap(recoloredSvgToThemeFile(":/images/whats_new/whatsnew_header.svg", wn.headerDecorationColor, params.themeName)); + theme.whatsNewDialog.closeButtonIcon = QPixmap(recoloredSvgToThemeFile(":/images/custom_dialog/custom_close_button.svg", wn.closeButtonColor, params.meta.id)); + theme.whatsNewDialog.headerDecoration = QPixmap(recoloredSvgToThemeFile(":/images/whats_new/whatsnew_header.svg", wn.headerDecorationColor, params.meta.id)); // end WhatsNewDialog // EmptyContainer @@ -411,16 +411,16 @@ Theme makeTheme(const ThemeParams ¶ms) theme.emptyContainer.titleLabelQSS = ect.titleLabelQSS.arg(ec.titleTextColor.name()); theme.emptyContainer.textColor = ec.textColor; theme.emptyContainer.descriptionTextColor = ec.descriptionTextColor; - theme.emptyContainer.noLibrariesIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/noLibrariesIcon.svg", ec.searchIconColor, params.themeName), 165, 160, qApp->devicePixelRatio()); + theme.emptyContainer.noLibrariesIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/noLibrariesIcon.svg", ec.searchIconColor, params.meta.id), 165, 160, qApp->devicePixelRatio()); { const qreal dpr = qApp->devicePixelRatio(); - theme.emptyContainer.searchingIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/search_result.svg", ec.searchIconColor, params.themeName, { .suffix = "_searching" }), 97, dpr); - theme.emptyContainer.noSearchResultsIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/search_result.svg", ec.searchIconColor, params.themeName, { .suffix = "_no_results" }), 239, dpr); + theme.emptyContainer.searchingIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/search_result.svg", ec.searchIconColor, params.meta.id, { .suffix = "_searching" }), 97, dpr); + theme.emptyContainer.noSearchResultsIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/search_result.svg", ec.searchIconColor, params.meta.id, { .suffix = "_no_results" }), 239, dpr); - theme.emptyContainer.emptyFolderIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/empty_container/empty_folder.svg", ec.searchIconColor, params.themeName), 319, 243, dpr); - theme.emptyContainer.emptyFavoritesIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/empty_container/empty_favorites.svg", QColor(0xe84853), params.themeName), 238, 223, dpr); - theme.emptyContainer.emptyCurrentReadingsIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/empty_container/empty_current_readings.svg", ec.searchIconColor, params.themeName), 167, 214, dpr); - theme.emptyContainer.emptyReadingListIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/empty_container/empty_reading_list.svg", ec.searchIconColor, params.themeName), 248, 187, dpr); + theme.emptyContainer.emptyFolderIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/empty_container/empty_folder.svg", ec.searchIconColor, params.meta.id), 319, 243, dpr); + theme.emptyContainer.emptyFavoritesIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/empty_container/empty_favorites.svg", QColor(0xe84853), params.meta.id), 238, 223, dpr); + theme.emptyContainer.emptyCurrentReadingsIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/empty_container/empty_current_readings.svg", ec.searchIconColor, params.meta.id), 167, 214, dpr); + theme.emptyContainer.emptyReadingListIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/empty_container/empty_reading_list.svg", ec.searchIconColor, params.meta.id), 248, 187, dpr); // Generate empty label icons for each label color const auto &rli = params.readingListIconsParams; @@ -430,7 +430,7 @@ Theme makeTheme(const ThemeParams ¶ms) auto it = rli.labelColors.find(colorName); if (it != rli.labelColors.end()) { theme.emptyContainer.emptyLabelIcons[c] = renderSvgToPixmap( - recoloredSvgToThemeFile(":/images/empty_container/empty_label.svg", it.value(), params.themeName, { .suffix = "_" + colorName }), 243, 243, dpr); + recoloredSvgToThemeFile(":/images/empty_container/empty_label.svg", it.value(), params.meta.id, { .suffix = "_" + colorName }), 243, 243, dpr); } } } @@ -456,14 +456,14 @@ Theme makeTheme(const ThemeParams ¶ms) theme.importWidget.currentComicTextColor = iw.currentComicTextColor; theme.importWidget.coversViewBackgroundColor = iw.coversViewBackgroundColor; theme.importWidget.coversLabelColor = iw.coversLabelColor; - theme.importWidget.topCoversDecoration = QPixmap(recoloredSvgToThemeFile(":/images/import/importTopCoversDecoration.svg", iw.coversDecorationBgColor, iw.coversDecorationShadowColor, params.themeName)); - theme.importWidget.bottomCoversDecoration = QPixmap(recoloredSvgToThemeFile(":/images/import/importBottomCoversDecoration.svg", iw.coversDecorationBgColor, iw.coversDecorationShadowColor, params.themeName)); - theme.importWidget.importingIcon = QPixmap(recoloredSvgToThemeFile(":/images/import/importingIcon.svg", iw.modeIconColor, params.themeName)); - theme.importWidget.updatingIcon = QPixmap(recoloredSvgToThemeFile(":/images/import/updatingIcon.svg", iw.modeIconColor, params.themeName)); + theme.importWidget.topCoversDecoration = QPixmap(recoloredSvgToThemeFile(":/images/import/importTopCoversDecoration.svg", iw.coversDecorationBgColor, iw.coversDecorationShadowColor, params.meta.id)); + theme.importWidget.bottomCoversDecoration = QPixmap(recoloredSvgToThemeFile(":/images/import/importBottomCoversDecoration.svg", iw.coversDecorationBgColor, iw.coversDecorationShadowColor, params.meta.id)); + theme.importWidget.importingIcon = QPixmap(recoloredSvgToThemeFile(":/images/import/importingIcon.svg", iw.modeIconColor, params.meta.id)); + theme.importWidget.updatingIcon = QPixmap(recoloredSvgToThemeFile(":/images/import/updatingIcon.svg", iw.modeIconColor, params.meta.id)); { QIcon coversToggle; - const QString normalPath = recoloredSvgToThemeFile(":/images/import/coversToggle.svg", iw.iconColor, params.themeName); - const QString checkedPath = recoloredSvgToThemeFile(":/images/import/coversToggle.svg", iw.iconCheckedColor, params.themeName, { .suffix = "_checked" }); + const QString normalPath = recoloredSvgToThemeFile(":/images/import/coversToggle.svg", iw.iconColor, params.meta.id); + const QString checkedPath = recoloredSvgToThemeFile(":/images/import/coversToggle.svg", iw.iconCheckedColor, params.meta.id, { .suffix = "_checked" }); coversToggle.addFile(normalPath, QSize(), QIcon::Normal, QIcon::Off); coversToggle.addFile(checkedPath, QSize(), QIcon::Normal, QIcon::On); theme.importWidget.coversToggleIcon = coversToggle; @@ -503,9 +503,9 @@ Theme makeTheme(const ThemeParams ¶ms) theme.qmlView.textColor = qv.textColor; theme.qmlView.showDropShadow = qv.showDropShadow; theme.qmlView.infoBackgroundColor = qv.infoBackgroundColor; - theme.qmlView.topShadow = recoloredSvgToThemeFile(":/qml/info-top-shadow.svg", qv.infoBackgroundColor, qv.infoBorderColor, qv.infoShadowColor, params.themeName); - theme.qmlView.infoShadow = recoloredSvgToThemeFile(":/qml/info-shadow.svg", qv.infoBackgroundColor, qv.infoBorderColor, qv.infoShadowColor, params.themeName); - theme.qmlView.infoIndicator = recoloredSvgToThemeFile(":/qml/info-indicator.svg", qv.infoBackgroundColor, qv.infoBorderColor, qv.infoShadowColor, params.themeName); + theme.qmlView.topShadow = recoloredSvgToThemeFile(":/qml/info-top-shadow.svg", qv.infoBackgroundColor, qv.infoBorderColor, qv.infoShadowColor, params.meta.id); + theme.qmlView.infoShadow = recoloredSvgToThemeFile(":/qml/info-shadow.svg", qv.infoBackgroundColor, qv.infoBorderColor, qv.infoShadowColor, params.meta.id); + theme.qmlView.infoIndicator = recoloredSvgToThemeFile(":/qml/info-indicator.svg", qv.infoBackgroundColor, qv.infoBorderColor, qv.infoShadowColor, params.meta.id); theme.qmlView.infoTextColor = qv.infoTextColor; theme.qmlView.infoTitleColor = qv.infoTitleColor; theme.qmlView.ratingUnselectedColor = qv.ratingUnselectedColor; @@ -557,8 +557,8 @@ Theme makeTheme(const ThemeParams ¶ms) // Build icons with Normal and Disabled states auto makeToolbarIcon = [&](const QString &basePath) { QIcon icon; - const QString normalPath = recoloredSvgToThemeFile(basePath, mt.iconColor, params.themeName); - const QString disabledPath = recoloredSvgToThemeFile(basePath, mt.iconDisabledColor, params.themeName, { .suffix = "_disabled" }); + const QString normalPath = recoloredSvgToThemeFile(basePath, mt.iconColor, params.meta.id); + const QString disabledPath = recoloredSvgToThemeFile(basePath, mt.iconDisabledColor, params.meta.id, { .suffix = "_disabled" }); icon.addFile(normalPath, QSize(), QIcon::Normal, QIcon::Off); icon.addFile(disabledPath, QSize(), QIcon::Disabled, QIcon::Off); return icon; @@ -581,7 +581,7 @@ Theme makeTheme(const ThemeParams ¶ms) // Helper to create icons with shadow (two-color: #f0f main, #0ff shadow) // Adds both Normal and Selected modes to prevent Qt from applying a selection tint auto makeSidebarIcon = [&](const QString &basePath) { - const QString path = recoloredSvgToThemeFile(basePath, si.iconColor, si.shadowColor, params.themeName); + const QString path = recoloredSvgToThemeFile(basePath, si.iconColor, si.shadowColor, params.meta.id); QIcon icon; icon.addFile(path, QSize(), QIcon::Normal, QIcon::Off); icon.addFile(path, QSize(), QIcon::Selected, QIcon::Off); @@ -590,7 +590,7 @@ Theme makeTheme(const ThemeParams ¶ms) // Helper for single-color icons (only #f0f main) auto makeSingleColorIcon = [&](const QString &basePath) { - const QString path = recoloredSvgToThemeFile(basePath, si.iconColor, params.themeName); + const QString path = recoloredSvgToThemeFile(basePath, si.iconColor, params.meta.id); QIcon icon; icon.addFile(path, QSize(), QIcon::Normal, QIcon::Off); icon.addFile(path, QSize(), QIcon::Selected, QIcon::Off); @@ -600,14 +600,14 @@ Theme makeTheme(const ThemeParams ¶ms) // System folder icons flag and overlay theme.sidebarIcons.useSystemFolderIcons = si.useSystemFolderIcons; if (si.useSystemFolderIcons) { - const QString overlayPath = recoloredSvgToThemeFile(":/images/sidebar/folder_read_overlay.svg", params.treeViewParams.folderReadOverlayColor, params.themeName); + const QString overlayPath = recoloredSvgToThemeFile(":/images/sidebar/folder_read_overlay.svg", params.treeViewParams.folderReadOverlayColor, params.meta.id); theme.sidebarIcons.folderReadOverlay = QPixmap(overlayPath); } // Library icon (unselected state, two-color: #f0f main, #0ff shadow) { const auto &li = params.libraryItemParams; - const QString libraryIconPath = recoloredSvgToThemeFile(":/images/sidebar/libraryIcon.svg", li.libraryIconColor, li.libraryIconShadowColor, params.themeName); + const QString libraryIconPath = recoloredSvgToThemeFile(":/images/sidebar/libraryIcon.svg", li.libraryIconColor, li.libraryIconShadowColor, params.meta.id); QIcon icon; icon.addFile(libraryIconPath, QSize(), QIcon::Normal, QIcon::Off); icon.addFile(libraryIconPath, QSize(), QIcon::Selected, QIcon::Off); @@ -634,11 +634,11 @@ Theme makeTheme(const ThemeParams ¶ms) theme.libraryItem.selectedBackgroundColor = li.selectedBackgroundColor; // Library icon when selected (uses its own color to contrast with selected background) - const QString libraryIconSelectedPath = recoloredSvgToThemeFile(":/images/sidebar/libraryIconSelected.svg", li.libraryIconSelectedColor, params.themeName); + const QString libraryIconSelectedPath = recoloredSvgToThemeFile(":/images/sidebar/libraryIconSelected.svg", li.libraryIconSelectedColor, params.meta.id); theme.libraryItem.libraryIconSelected = QIcon(libraryIconSelectedPath); // Library options icon (shown only when selected, uses its own color) - const QString libraryOptionsPath = recoloredSvgToThemeFile(":/images/sidebar/libraryOptions.svg", li.libraryOptionsIconColor, params.themeName); + const QString libraryOptionsPath = recoloredSvgToThemeFile(":/images/sidebar/libraryOptions.svg", li.libraryOptionsIconColor, params.meta.id); theme.libraryItem.libraryOptionsIcon = QIcon(libraryOptionsPath); // end LibraryItem @@ -646,10 +646,10 @@ Theme makeTheme(const ThemeParams ¶ms) const auto &tv = params.treeViewParams; // Branch indicator icons — own colors, independent of the sidebar icon color - theme.treeView.branchClosedIconPath = recoloredSvgToThemeFile(":/images/sidebar/branch-closed.svg", tv.branchIndicatorColor, params.themeName); - theme.treeView.branchOpenIconPath = recoloredSvgToThemeFile(":/images/sidebar/branch-open.svg", tv.branchIndicatorColor, params.themeName); - theme.treeView.branchClosedIconSelectedPath = recoloredSvgToThemeFile(":/images/sidebar/branch-closed.svg", tv.branchIndicatorSelectedColor, params.themeName, { .suffix = "_selected" }); - theme.treeView.branchOpenIconSelectedPath = recoloredSvgToThemeFile(":/images/sidebar/branch-open.svg", tv.branchIndicatorSelectedColor, params.themeName, { .suffix = "_selected" }); + theme.treeView.branchClosedIconPath = recoloredSvgToThemeFile(":/images/sidebar/branch-closed.svg", tv.branchIndicatorColor, params.meta.id); + theme.treeView.branchOpenIconPath = recoloredSvgToThemeFile(":/images/sidebar/branch-open.svg", tv.branchIndicatorColor, params.meta.id); + theme.treeView.branchClosedIconSelectedPath = recoloredSvgToThemeFile(":/images/sidebar/branch-closed.svg", tv.branchIndicatorSelectedColor, params.meta.id, { .suffix = "_selected" }); + theme.treeView.branchOpenIconSelectedPath = recoloredSvgToThemeFile(":/images/sidebar/branch-open.svg", tv.branchIndicatorSelectedColor, params.meta.id, { .suffix = "_selected" }); theme.treeView.treeViewQSS = tv.t.styledTreeViewQSS .arg(tv.textColor.name(), @@ -665,16 +665,16 @@ Theme makeTheme(const ThemeParams ¶ms) // Folder icon — normal and selected states with independent colors { - const QString normalPath = recoloredSvgToThemeFile(":/images/sidebar/folder.svg", tv.folderIconColor, tv.folderIconShadowColor, params.themeName); - const QString selectedPath = recoloredSvgToThemeFile(":/images/sidebar/folder.svg", tv.folderIconSelectedColor, tv.folderIconSelectedShadowColor, params.themeName, { .suffix = "_selected" }); + const QString normalPath = recoloredSvgToThemeFile(":/images/sidebar/folder.svg", tv.folderIconColor, tv.folderIconShadowColor, params.meta.id); + const QString selectedPath = recoloredSvgToThemeFile(":/images/sidebar/folder.svg", tv.folderIconSelectedColor, tv.folderIconSelectedShadowColor, params.meta.id, { .suffix = "_selected" }); theme.treeView.folderIcon.addFile(normalPath, QSize(), QIcon::Normal, QIcon::Off); theme.treeView.folderIcon.addFile(selectedPath, QSize(), QIcon::Selected, QIcon::Off); } // Folder finished icon — same but with tick (#ff0) recolored independently for each state { - const QString normalPath = recoloredSvgToThemeFile(":/images/sidebar/folder_finished.svg", tv.folderIconColor, tv.folderIconShadowColor, tv.folderReadOverlayColor, params.themeName); - const QString selectedPath = recoloredSvgToThemeFile(":/images/sidebar/folder_finished.svg", tv.folderIconSelectedColor, tv.folderIconSelectedShadowColor, tv.folderReadOverlaySelectedColor, params.themeName, { .suffix = "_selected" }); + const QString normalPath = recoloredSvgToThemeFile(":/images/sidebar/folder_finished.svg", tv.folderIconColor, tv.folderIconShadowColor, tv.folderReadOverlayColor, params.meta.id); + const QString selectedPath = recoloredSvgToThemeFile(":/images/sidebar/folder_finished.svg", tv.folderIconSelectedColor, tv.folderIconSelectedShadowColor, tv.folderReadOverlaySelectedColor, params.meta.id, { .suffix = "_selected" }); theme.treeView.folderFinishedIcon.addFile(normalPath, QSize(), QIcon::Normal, QIcon::Off); theme.treeView.folderFinishedIcon.addFile(selectedPath, QSize(), QIcon::Selected, QIcon::Off); } @@ -695,7 +695,7 @@ Theme makeTheme(const ThemeParams ¶ms) // Helper to create single-color icons for comics view toolbar auto makeComicsViewIcon = [&](const QString &basePath) { - const QString path = recoloredSvgToThemeFile(basePath, cvt.iconColor, params.themeName); + const QString path = recoloredSvgToThemeFile(basePath, cvt.iconColor, params.meta.id); QIcon icon; icon.addFile(path, QSize(), QIcon::Normal, QIcon::Off); icon.addFile(path, QSize(), QIcon::Selected, QIcon::Off); @@ -734,8 +734,8 @@ Theme makeTheme(const ThemeParams ¶ms) theme.searchLineEdit.clearButtonQSS = sle.t.clearButtonQSS; const qreal dpr = qApp->devicePixelRatio(); - theme.searchLineEdit.searchIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/iconSearchNew.svg", sle.iconColor, params.themeName), 15, dpr); - theme.searchLineEdit.clearIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/clearSearchNew.svg", sle.iconColor, params.themeName), 12, dpr); + theme.searchLineEdit.searchIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/iconSearchNew.svg", sle.iconColor, params.meta.id), 15, dpr); + theme.searchLineEdit.clearIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/clearSearchNew.svg", sle.iconColor, params.meta.id), 12, dpr); // end SearchLineEdit // ReadingListIcons @@ -743,8 +743,8 @@ Theme makeTheme(const ThemeParams ¶ms) // Helper to create label icons from template (uses color name to generate label_.svg files) auto makeLabelIcon = [&](const QString &colorName, const QColor &mainColor) { - const QString normalPath = recoloredSvgToThemeFile(":/images/lists/label_template.svg", mainColor, rli.labelShadowColor, params.themeName, { .fileName = "label_" + colorName }); - const QString selectedPath = recoloredSvgToThemeFile(":/images/lists/label_template.svg", mainColor, rli.labelShadowSelectedColor, params.themeName, { .suffix = "_selected", .fileName = "label_" + colorName }); + const QString normalPath = recoloredSvgToThemeFile(":/images/lists/label_template.svg", mainColor, rli.labelShadowColor, params.meta.id, { .fileName = "label_" + colorName }); + const QString selectedPath = recoloredSvgToThemeFile(":/images/lists/label_template.svg", mainColor, rli.labelShadowSelectedColor, params.meta.id, { .suffix = "_selected", .fileName = "label_" + colorName }); QIcon icon; icon.addFile(normalPath, QSize(), QIcon::Normal, QIcon::Off); icon.addFile(selectedPath, QSize(), QIcon::Selected, QIcon::Off); @@ -757,8 +757,8 @@ Theme makeTheme(const ThemeParams ¶ms) // Special list icons auto makeSpecialIcon = [&](const QString &basePath, const QColor &mainColor, const QColor &mainSelectedColor) { - const QString normalPath = recoloredSvgToThemeFile(basePath, mainColor, rli.specialListShadowColor, params.themeName); - const QString selectedPath = recoloredSvgToThemeFile(basePath, mainSelectedColor, rli.specialListShadowSelectedColor, params.themeName, { .suffix = "_selected" }); + const QString normalPath = recoloredSvgToThemeFile(basePath, mainColor, rli.specialListShadowColor, params.meta.id); + const QString selectedPath = recoloredSvgToThemeFile(basePath, mainSelectedColor, rli.specialListShadowSelectedColor, params.meta.id, { .suffix = "_selected" }); QIcon icon; icon.addFile(normalPath, QSize(), QIcon::Normal, QIcon::Off); icon.addFile(selectedPath, QSize(), QIcon::Selected, QIcon::Off); @@ -770,8 +770,8 @@ Theme makeTheme(const ThemeParams ¶ms) // Currently reading has 3 colors { - const QString normalPath = recoloredSvgToThemeFile(":/images/lists/default_2.svg", rli.currentlyReadingMainColor, rli.specialListShadowColor, rli.currentlyReadingOuterColor, params.themeName); - const QString selectedPath = recoloredSvgToThemeFile(":/images/lists/default_2.svg", rli.currentlyReadingMainSelectedColor, rli.specialListShadowSelectedColor, rli.currentlyReadingOuterSelectedColor, params.themeName, { .suffix = "_selected" }); + const QString normalPath = recoloredSvgToThemeFile(":/images/lists/default_2.svg", rli.currentlyReadingMainColor, rli.specialListShadowColor, rli.currentlyReadingOuterColor, params.meta.id); + const QString selectedPath = recoloredSvgToThemeFile(":/images/lists/default_2.svg", rli.currentlyReadingMainSelectedColor, rli.specialListShadowSelectedColor, rli.currentlyReadingOuterSelectedColor, params.meta.id, { .suffix = "_selected" }); QIcon icon; icon.addFile(normalPath, QSize(), QIcon::Normal, QIcon::Off); icon.addFile(selectedPath, QSize(), QIcon::Selected, QIcon::Off); @@ -780,8 +780,8 @@ Theme makeTheme(const ThemeParams ¶ms) // List icon (3 colors) { - const QString normalPath = recoloredSvgToThemeFile(":/images/lists/list.svg", rli.listMainColor, rli.listShadowColor, rli.listDetailColor, params.themeName); - const QString selectedPath = recoloredSvgToThemeFile(":/images/lists/list.svg", rli.listMainSelectedColor, rli.listShadowSelectedColor, rli.listDetailSelectedColor, params.themeName, { .suffix = "_selected" }); + const QString normalPath = recoloredSvgToThemeFile(":/images/lists/list.svg", rli.listMainColor, rli.listShadowColor, rli.listDetailColor, params.meta.id); + const QString selectedPath = recoloredSvgToThemeFile(":/images/lists/list.svg", rli.listMainSelectedColor, rli.listShadowSelectedColor, rli.listDetailSelectedColor, params.meta.id, { .suffix = "_selected" }); QIcon icon; icon.addFile(normalPath, QSize(), QIcon::Normal, QIcon::Off); icon.addFile(selectedPath, QSize(), QIcon::Selected, QIcon::Off); @@ -792,7 +792,7 @@ Theme makeTheme(const ThemeParams ¶ms) // MenuIcons const auto &mi = params.menuIconsParams; auto makeMenuIcon = [&](const QString &basePath) { - const QString path = recoloredSvgToThemeFile(basePath, mi.iconColor, params.themeName); + const QString path = recoloredSvgToThemeFile(basePath, mi.iconColor, params.meta.id); return QIcon(path); }; @@ -811,7 +811,7 @@ Theme makeTheme(const ThemeParams ¶ms) // DialogIcons const auto &di = params.dialogIconsParams; auto makeDialogIcon = [&](const QString &basePath) { - const QString path = recoloredSvgToThemeFile(basePath, di.iconColor, params.themeName); + const QString path = recoloredSvgToThemeFile(basePath, di.iconColor, params.meta.id); return QPixmap(path); }; theme.dialogIcons.newLibraryIcon = makeDialogIcon(":/images/library_dialogs/new.svg"); @@ -822,7 +822,7 @@ Theme makeTheme(const ThemeParams ¶ms) theme.dialogIcons.exportLibraryIcon = makeDialogIcon(":/images/library_dialogs/exportLibrary.svg"); theme.dialogIcons.importLibraryIcon = makeDialogIcon(":/images/library_dialogs/importLibrary.svg"); { - const QString path = recoloredSvgToThemeFile(":/images/find_folder.svg", di.iconColor, params.themeName); + const QString path = recoloredSvgToThemeFile(":/images/find_folder.svg", di.iconColor, params.meta.id); const qreal dpr = qApp->devicePixelRatio(); theme.dialogIcons.findFolderIcon = QIcon(renderSvgToPixmap(path, 13, 13, dpr)); } @@ -831,7 +831,7 @@ Theme makeTheme(const ThemeParams ¶ms) // ShortcutsIcons const auto &sci = params.shortcutsIconsParams; auto makeShortcutsIcon = [&](const QString &basePath) { - const QString path = recoloredSvgToThemeFile(basePath, sci.iconColor, params.themeName); + const QString path = recoloredSvgToThemeFile(basePath, sci.iconColor, params.meta.id); return QIcon(path); }; @@ -852,842 +852,332 @@ Theme makeTheme(const ThemeParams ¶ms) theme.serverConfigDialog.checkBoxQSS = scd.t.checkBoxQSS.arg(scd.checkBoxTextColor.name()); theme.serverConfigDialog.qrBackgroundColor = scd.qrBackgroundColor; theme.serverConfigDialog.qrForegroundColor = scd.qrForegroundColor; - theme.serverConfigDialog.backgroundDecoration = QPixmap(recoloredSvgToThemeFile(":/images/serverConfigBackground.svg", scd.decorationColor, params.themeName)); + theme.serverConfigDialog.backgroundDecoration = QPixmap(recoloredSvgToThemeFile(":/images/serverConfigBackground.svg", scd.decorationColor, params.meta.id)); + + theme.meta = params.meta; return theme; } -ThemeParams classicThemeParams(); -ThemeParams lightThemeParams(); -ThemeParams darkThemeParams(); +// JSON helpers --------------------------------------------------------------- -Theme makeTheme(ThemeId themeId) +static QColor colorFromJson(const QJsonObject &obj, const QString &key, const QColor &fallback) { + if (!obj.contains(key)) + return fallback; + QColor c(obj[key].toString()); + return c.isValid() ? c : fallback; +} - switch (themeId) { - case ThemeId::Classic: - return makeTheme(classicThemeParams()); - case ThemeId::Light: - return makeTheme(lightThemeParams()); - case ThemeId::Dark: - return makeTheme(darkThemeParams()); +Theme makeTheme(const QJsonObject &json) +{ + ThemeParams p; + + if (json.contains("defaultContentBackgroundColor")) { + QColor c(json["defaultContentBackgroundColor"].toString()); + if (c.isValid()) + p.defaultContentBackgroundColor = c; } - return makeTheme(classicThemeParams()); -} - -ThemeParams classicThemeParams() -{ - ThemeParams params; - params.themeName = "classic"; - params.defaultContentBackgroundColor = QColor(0x2A2A2A); - - ComicFlowColors cf; - cf.backgroundColor = Qt::black; - cf.textColor = QColor(0x4C4C4C); - - ComicVineParams cv; - cv.contentTextColor = Qt::white; - cv.contentBackgroundColor = QColor(0x2B2B2B); - cv.contentAltBackgroundColor = QColor(0x2B2B2B); - cv.dialogBackgroundColor = QColor(0x404040); - - cv.tableBackgroundColor = QColor(0x2B2B2B); - cv.tableAltBackgroundColor = QColor(0x2E2E2E); - cv.tableBorderColor = QColor(0x242424); - cv.tableSelectedColor = QColor(0x555555); - cv.tableHeaderBackgroundColor = QColor(0x292929); - cv.tableHeaderGradientColor = QColor(0x292929); - cv.tableHeaderBorderColor = QColor(0x1F1F1F); - cv.tableHeaderTextColor = QColor(0xEBEBEB); - cv.tableScrollHandleColor = QColor(0xDDDDDD); - cv.tableScrollBackgroundColor = QColor(0x404040); - cv.tableSectionBorderLight = QColor(0xFEFEFE); - cv.tableSectionBorderDark = QColor(0xDFDFDF); - - cv.labelTextColor = Qt::white; - cv.labelBackgroundColor = QColor(0x2B2B2B); - cv.hyperlinkColor = QColor(0xFFCC00); - - cv.buttonBackgroundColor = QColor(0x2E2E2E); - cv.buttonTextColor = Qt::white; - cv.buttonBorderColor = QColor(0x242424); - - cv.radioUncheckedColor = QColor(0xE5E5E5); - - cv.radioCheckedBackgroundColor = QColor(0xE5E5E5); - cv.radioCheckedIndicatorColor = QColor(0x5F5F5F); - - cv.checkBoxTickColor = Qt::white; - - cv.toolButtonAccentColor = QColor(0x282828); - - cv.downArrowColor = QColor(0x9F9F9F); - cv.upArrowColor = QColor(0x9F9F9F); - - cv.busyIndicatorColor = Qt::white; - cv.navIconColor = Qt::white; - cv.rowIconColor = QColor(0xE5E5E5); - - cv.t = ComicVineThemeTemplates(); - - params.comicFlowColors = cf; - params.comicVineParams = cv; - - params.helpAboutDialogParams.headingColor = QColor(0x302f2d); - params.helpAboutDialogParams.linkColor = QColor(0xC19441); - - WhatsNewDialogParams wnp; - wnp.backgroundColor = QColor(0x2A2A2A); - wnp.headerTextColor = QColor(0xE0E0E0); - wnp.versionTextColor = QColor(0x858585); - wnp.contentTextColor = QColor(0xE0E0E0); - wnp.linkColor = QColor(0xE8B800); - wnp.closeButtonColor = QColor(0xDDDDDD); - wnp.headerDecorationColor = QColor(0xE8B800); - params.whatsNewDialogParams = wnp; - - EmptyContainerParams ec; - ec.backgroundColor = QColor(0x2A2A2A); - ec.titleTextColor = QColor(0xCCCCCC); - ec.textColor = QColor(0xCCCCCC); - ec.descriptionTextColor = QColor(0xAAAAAA); - ec.searchIconColor = QColor(0x4C4C4C); - ec.t = EmptyContainerThemeTemplates(); - params.emptyContainerParams = ec; - - SidebarParams sb; - sb.backgroundColor = QColor(0x454545); - sb.separatorColor = QColor(0xBDBFBF); - sb.sectionSeparatorColor = QColor(0x575757); - sb.uppercaseLabels = true; - sb.titleTextColor = QColor(0xBDBFBF); - sb.titleDropShadowColor = Qt::black; - sb.busyIndicatorColor = Qt::white; - params.sidebarParams = sb; - - ImportWidgetParams iw; - iw.backgroundColor = QColor(0x2A2A2A); - iw.titleTextColor = QColor(0xCCCCCC); - iw.descriptionTextColor = QColor(0xAAAAAA); - iw.currentComicTextColor = QColor(0xAAAAAA); - iw.coversViewBackgroundColor = QColor(0x3A3A3A); - iw.coversLabelColor = QColor(0xAAAAAA); - iw.coversDecorationBgColor = QColor(0x3A3A3A); - iw.coversDecorationShadowColor = QColor(0x1A1A1A); - iw.modeIconColor = QColor(0x4A4A4A); - iw.iconColor = QColor(0xCCCCCC); - iw.iconCheckedColor = QColor(0xAAAAAA); - params.importWidgetParams = iw; - - TreeViewParams tv; - tv.textColor = QColor(0xDDDFDF); - tv.selectionBackgroundColor = QColor(0x2E2E2E); - tv.scrollBackgroundColor = QColor(0x404040); - tv.scrollHandleColor = QColor(0xDDDDDD); - tv.selectedTextColor = Qt::white; - tv.folderIndicatorColor = QColor(237, 197, 24); - tv.branchIndicatorColor = QColor(0xE0E0E0); - tv.branchIndicatorSelectedColor = QColor(0xFFFFFF); - tv.folderIconColor = QColor(0xE0E0E0); - tv.folderIconShadowColor = QColor(0xFF000000); - tv.folderIconSelectedColor = QColor(0xE0E0E0); - tv.folderIconSelectedShadowColor = QColor(0xFF000000); - tv.folderReadOverlayColor = QColor(0x464646); - tv.folderReadOverlaySelectedColor = QColor(0x464646); - params.treeViewParams = tv; - - TableViewParams tav; - tav.alternateBackgroundColor = QColor(0xF2F2F2); - tav.backgroundColor = QColor(0xFAFAFA); - tav.headerBackgroundColor = QColor(0xF5F5F5); - tav.headerBorderColor = QColor(0xB8BDC4); - tav.headerGradientColor = QColor(0xD1D1D1); - tav.itemBorderBottomColor = QColor(0xDFDFDF); - tav.itemBorderTopColor = QColor(0xFEFEFE); - tav.itemBorderBottomWidth = 1; - tav.itemBorderTopWidth = 1; - tav.itemTextColor = QColor(0x252626); - tav.selectedColor = QColor(0xD4D4D4); - tav.selectedTextColor = QColor(0x252626); - tav.headerTextColor = QColor(0x313232); - tav.starRatingColor = QColor(0xE9BE0F); - tav.starRatingSelectedColor = QColor(0xFFFFFF); - tav.t = TableViewThemeTemplates(); - params.tableViewParams = tav; - - QmlViewParams qv; - qv.backgroundColor = QColor(0x2A2A2A); - qv.cellColor = QColor(0x212121); - qv.cellColorWithBackground = QColor(0x21, 0x21, 0x21, 0x99); - qv.selectedColor = QColor(0x121212); - qv.selectedBorderColor = QColor(0xFFCC00); - qv.borderColor = QColor(0x121212); - qv.titleColor = QColor(0xFFFFFF); - qv.textColor = QColor(0xA8A8A8); - qv.showDropShadow = true; - qv.infoBackgroundColor = QColor(0x2E2E2E); - qv.infoBorderColor = QColor(0x404040); - qv.infoShadowColor = Qt::black; - qv.infoTextColor = QColor(0xB0B0B0); - qv.infoTitleColor = QColor(0xFFFFFF); - qv.ratingUnselectedColor = QColor(0x1C1C1C); - qv.ratingSelectedColor = QColor(0xFFFFFF); - qv.favUncheckedColor = QColor(0x1C1C1C); - qv.favCheckedColor = QColor(0xE84852); - qv.readTickUncheckedColor = QColor(0x1C1C1C); - qv.readTickCheckedColor = QColor(0xE84852); - qv.currentComicBackgroundColor = QColor(0x00, 0x00, 0x00, 0x88); - qv.continueReadingBackgroundColor = QColor(0x00, 0x00, 0x00, 0x88); - qv.continueReadingColor = QColor(0xFFFFFF); - qv.backgroundBlurOverlayColor = QColor(0x2A2A2A); - params.qmlViewParams = qv; - - MainToolbarParams mt; - mt.backgroundColor = QColor(0xF0F0F0); - mt.folderNameColor = QColor(0x404040); - mt.dividerColor = QColor(0xB8BDC4); - mt.iconColor = QColor(0x404040); - mt.iconDisabledColor = QColor(0xB0B0B0); - params.mainToolbarParams = mt; - - ContentSplitterParams cs; - cs.handleColor = QColor(0xB8B8B8); - cs.horizontalHandleHeight = 4; - cs.verticalHandleWidth = 4; - params.contentSplitterParams = cs; - - SidebarIconsParams si; - si.iconColor = QColor(0xE0E0E0); - si.shadowColor = QColor(0xFF000000); - si.useSystemFolderIcons = false; - params.sidebarIconsParams = si; - - LibraryItemParams li; - li.textColor = QColor(0xDDDFDF); - li.libraryIconColor = QColor(0xDDDFDF); - li.libraryIconShadowColor = QColor(0xFF000000); - li.selectedTextColor = Qt::white; - li.selectedBackgroundColor = QColor(0x2E2E2E); - li.libraryIconSelectedColor = Qt::white; - li.libraryOptionsIconColor = Qt::white; - params.libraryItemParams = li; - - ComicsViewToolbarParams cvt; - cvt.backgroundColor = QColor(0xF0F0F0); - cvt.separatorColor = QColor(0xCCCCCC); - cvt.checkedBackgroundColor = QColor(0xCCCCCC); - cvt.iconColor = QColor(0x404040); - params.comicsViewToolbarParams = cvt; - - SearchLineEditParams sle; - sle.textColor = QColor(0xABABAB); - sle.backgroundColor = QColor(0x404040); - sle.iconColor = QColor(0xF7F7F7); - params.searchLineEditParams = sle; - - ReadingListIconsParams rli; - rli.labelColors = { - { "red", QColor(0xf67a7b) }, - { "orange", QColor(0xf5c240) }, - { "yellow", QColor(0xf2e446) }, - { "green", QColor(0xade738) }, - { "cyan", QColor(0xa0fddb) }, - { "blue", QColor(0x82c7ff) }, - { "violet", QColor(0x8f95ff) }, - { "purple", QColor(0xd692fc) }, - { "pink", QColor(0xfd9cda) }, - { "white", QColor(0xfcfcfc) }, - { "light", QColor(0xcbcbcb) }, - { "dark", QColor(0xb7b7b7) } - }; - rli.labelShadowColor = Qt::black; - rli.labelShadowSelectedColor = Qt::black; - rli.readingListMainColor = QColor(0xe7e7e7); - rli.readingListMainSelectedColor = QColor(0xe7e7e7); - rli.favoritesMainColor = QColor(0xe15055); - rli.favoritesMainSelectedColor = QColor(0xe15055); - rli.currentlyReadingMainColor = QColor(0xffcc00); - rli.currentlyReadingMainSelectedColor = QColor(0xffcc00); - rli.currentlyReadingOuterColor = Qt::black; - rli.currentlyReadingOuterSelectedColor = Qt::black; - rli.specialListShadowColor = Qt::black; - rli.specialListShadowSelectedColor = Qt::black; - rli.listMainColor = QColor(0xe7e7e7); - rli.listMainSelectedColor = QColor(0xe7e7e7); - rli.listShadowColor = Qt::black; - rli.listShadowSelectedColor = Qt::black; - rli.listDetailColor = QColor(0x464646); - rli.listDetailSelectedColor = QColor(0x464646); - params.readingListIconsParams = rli; - - MenuIconsParams mi; - mi.iconColor = QColor(0xF7F7F7); - params.menuIconsParams = mi; - - DialogIconsParams dip; - dip.iconColor = mi.iconColor; - params.dialogIconsParams = dip; - - ShortcutsIconsParams sci; - sci.iconColor = QColor(0xF7F7F7); - params.shortcutsIconsParams = sci; - - ServerConfigDialogParams scd; - scd.backgroundColor = QColor(0x2A2A2A); - scd.titleTextColor = QColor(0x474747); - scd.qrMessageTextColor = QColor(0xA3A3A3); - scd.propagandaTextColor = QColor(0x4D4D4D); - scd.labelTextColor = QColor(0x575757); - scd.checkBoxTextColor = QColor(0x262626); - scd.qrBackgroundColor = QColor(0x2A2A2A); - scd.qrForegroundColor = Qt::white; - scd.decorationColor = QColor(0xF7F7F7); - params.serverConfigDialogParams = scd; - - return params; -} - -ThemeParams lightThemeParams() -{ - ThemeParams params; - params.themeName = "light"; - params.defaultContentBackgroundColor = QColor(0xFFFFFF); - - ComicFlowColors cf; - cf.backgroundColor = QColor(0xDCDCDC); - cf.textColor = QColor(0x303030); - - ComicVineParams cv; - cv.contentTextColor = Qt::black; - cv.contentBackgroundColor = QColor(0xECECEC); - cv.contentAltBackgroundColor = QColor(0xE0E0E0); - cv.dialogBackgroundColor = QColor(0xFBFBFB); - - cv.tableBackgroundColor = QColor(0xF4F4F4); - cv.tableAltBackgroundColor = QColor(0xFAFAFA); - cv.tableBorderColor = QColor(0xCCCCCC); - cv.tableSelectedColor = QColor(0xDDDDDD); - cv.tableHeaderBackgroundColor = QColor(0xE0E0E0); - cv.tableHeaderGradientColor = QColor(0xE0E0E0); - cv.tableHeaderBorderColor = QColor(0xC0C0C0); - cv.tableHeaderTextColor = QColor(0x333333); - cv.tableScrollHandleColor = QColor(0x888888); - cv.tableScrollBackgroundColor = QColor(0xD0D0D0); - cv.tableSectionBorderLight = QColor(0xFFFFFF); - cv.tableSectionBorderDark = QColor(0xCCCCCC); - - cv.labelTextColor = Qt::black; - cv.labelBackgroundColor = QColor(0xECECEC); - cv.hyperlinkColor = QColor(0xFFCC00); - - cv.buttonBackgroundColor = QColor(0xE0E0E0); - cv.buttonTextColor = Qt::black; - cv.buttonBorderColor = QColor(0xCCCCCC); - - cv.radioUncheckedColor = QColor(0xE0E0E0); - - cv.radioCheckedBackgroundColor = QColor(0xE0E0E0); - cv.radioCheckedIndicatorColor = QColor(0x222222); - - cv.checkBoxTickColor = Qt::black; - - cv.toolButtonAccentColor = QColor(0xA0A0A0); - - cv.downArrowColor = QColor(0x222222); - cv.upArrowColor = QColor(0x222222); - - cv.busyIndicatorColor = Qt::black; - cv.navIconColor = QColor(0x222222); - cv.rowIconColor = QColor(0x222222); - - cv.t = ComicVineThemeTemplates(); - - params.comicFlowColors = cf; - params.comicVineParams = cv; - - params.helpAboutDialogParams.headingColor = QColor(0x302f2d); - params.helpAboutDialogParams.linkColor = QColor(0xC19441); - - WhatsNewDialogParams wnp; - wnp.backgroundColor = QColor(0xFFFFFF); - wnp.headerTextColor = QColor(0x0A0A0A); - wnp.versionTextColor = QColor(0x858585); - wnp.contentTextColor = QColor(0x0A0A0A); - wnp.linkColor = QColor(0xE8B800); - wnp.closeButtonColor = QColor(0x444444); - wnp.headerDecorationColor = QColor(0xE8B800); - params.whatsNewDialogParams = wnp; - - EmptyContainerParams ec; - ec.backgroundColor = QColor(0xFFFFFF); - ec.titleTextColor = QColor(0x888888); - ec.textColor = QColor(0x495252); - ec.descriptionTextColor = QColor(0x565959); - ec.searchIconColor = QColor(0xCCCCCC); - ec.t = EmptyContainerThemeTemplates(); - params.emptyContainerParams = ec; - - SidebarParams sb; - sb.backgroundColor = QColor(0xFBFBFB); - sb.separatorColor = QColor(0x808080); - sb.sectionSeparatorColor = QColor(0xE0E0E0); - sb.uppercaseLabels = true; - sb.titleTextColor = QColor(0x4A494A); - sb.titleDropShadowColor = QColor(0xFFFFFF); - sb.busyIndicatorColor = QColor(0x808080); - params.sidebarParams = sb; - - ImportWidgetParams iw; - iw.backgroundColor = QColor(0xFAFAFA); - iw.titleTextColor = QColor(0x495252); - iw.descriptionTextColor = QColor(0x565959); - iw.currentComicTextColor = QColor(0x565959); - iw.coversViewBackgroundColor = QColor(0xE6E6E6); - iw.coversLabelColor = QColor(0x565959); - iw.coversDecorationBgColor = QColor(0xE6E6E6); - iw.coversDecorationShadowColor = QColor(0xA1A1A1); - iw.modeIconColor = QColor(0xE6E6E6); - iw.iconColor = QColor(0x495252); - iw.iconCheckedColor = QColor(0x565959); - params.importWidgetParams = iw; - - TreeViewParams tv; - tv.textColor = Qt::black; - tv.selectionBackgroundColor = QColor(0x333133); - tv.scrollBackgroundColor = QColor(0xE0E0E0); - tv.scrollHandleColor = QColor(0x888888); - tv.selectedTextColor = QColor(0xFFFFFF); - tv.folderIndicatorColor = QColor(85, 95, 127); - tv.branchIndicatorColor = QColor(0x606060); - tv.branchIndicatorSelectedColor = QColor(0xFFFFFF); - tv.folderIconColor = QColor(0x606060); - tv.folderIconShadowColor = QColor(0xFFFFFF); - tv.folderIconSelectedColor = QColor(0xFFFFFF); - tv.folderIconSelectedShadowColor = QColor(0x161616); - tv.folderReadOverlayColor = QColor(0xFFFFFF); - tv.folderReadOverlaySelectedColor = QColor(0x161616); - params.treeViewParams = tv; - - TableViewParams tav; - tav.alternateBackgroundColor = QColor(0xF2F2F2); - tav.backgroundColor = QColor(0xFAFAFA); - tav.headerBackgroundColor = QColor(0xF5F5F5); - tav.headerBorderColor = QColor(0xB8BDC4); - tav.headerGradientColor = QColor(0xF5F5F5); - tav.itemBorderBottomColor = QColor(0xDFDFDF); - tav.itemBorderTopColor = QColor(0xFEFEFE); - tav.itemBorderBottomWidth = 0; - tav.itemBorderTopWidth = 0; - tav.itemTextColor = QColor(0x252626); - tav.selectedColor = QColor(0x595959); - tav.selectedTextColor = QColor(0xFFFFFF); - tav.headerTextColor = QColor(0x313232); - tav.starRatingColor = QColor(0xE9BE0F); - tav.starRatingSelectedColor = QColor(0xFFFFFF); - tav.t = TableViewThemeTemplates(); - params.tableViewParams = tav; - - QmlViewParams qv; - qv.backgroundColor = QColor(0xF6F6F6); - qv.cellColor = QColor(0xFFFFFF); - qv.cellColorWithBackground = QColor(0xFF, 0xFF, 0xFF, 0x99); - qv.selectedColor = QColor(0xFFFFFF); - qv.selectedBorderColor = QColor(0xFFCC00); - qv.borderColor = QColor(0xDBDBDB); - qv.titleColor = QColor(0x121212); - qv.textColor = QColor(0x636363); - qv.showDropShadow = true; - qv.infoBackgroundColor = QColor(0xFFFFFF); - qv.infoBorderColor = QColor(0x808080); - qv.infoShadowColor = QColor(0x444444); - qv.infoTextColor = QColor(0x404040); - qv.infoTitleColor = QColor(0x2E2E2E); - qv.ratingUnselectedColor = QColor(0xDEDEDE); - qv.ratingSelectedColor = QColor(0x2B2B2B); - qv.favUncheckedColor = QColor(0xDEDEDE); - qv.favCheckedColor = QColor(0xE84852); - qv.readTickUncheckedColor = QColor(0xDEDEDE); - qv.readTickCheckedColor = QColor(0xE84852); - qv.currentComicBackgroundColor = QColor(0xFF, 0xFF, 0xFF, 0x88); - qv.continueReadingBackgroundColor = QColor(0xE8E8E8); - qv.continueReadingColor = QColor::fromRgb(0x000000); - qv.backgroundBlurOverlayColor = QColor(0x9E9E9E); - params.qmlViewParams = qv; - - MainToolbarParams mt; - mt.backgroundColor = QColor(0xF0F0F0); - mt.folderNameColor = QColor(0x333133); - mt.dividerColor = QColor(0xB8BDC4); - mt.iconColor = QColor(0x333133); - mt.iconDisabledColor = QColor(0xB0B0B0); - params.mainToolbarParams = mt; - - ContentSplitterParams cs; - cs.handleColor = QColor(0xF0F0F0); - cs.horizontalHandleHeight = 4; - cs.verticalHandleWidth = 4; - params.contentSplitterParams = cs; - - SidebarIconsParams si; - si.iconColor = QColor(0x4F4E4F); - si.shadowColor = QColor(0xFBFBFB); - si.useSystemFolderIcons = false; - params.sidebarIconsParams = si; - - LibraryItemParams li; - li.textColor = Qt::black; - li.libraryIconColor = QColor(0x606060); - li.libraryIconShadowColor = QColor(0xFFFFFF); - li.selectedTextColor = QColor(0xFFFFFF); - li.selectedBackgroundColor = QColor(0x333133); - li.libraryIconSelectedColor = QColor(0xFFFFFF); - li.libraryOptionsIconColor = QColor(0xFFFFFF); - params.libraryItemParams = li; - - ComicsViewToolbarParams cvt; - cvt.backgroundColor = QColor(0xF0F0F0); - cvt.separatorColor = QColor(0xCCCCCC); - cvt.checkedBackgroundColor = QColor(0xCCCCCC); - cvt.iconColor = QColor(0x404040); - params.comicsViewToolbarParams = cvt; - - SearchLineEditParams sle; - sle.textColor = QColor(0xFFFFFF); - sle.backgroundColor = QColor(0x333133); - sle.iconColor = QColor(0xEFEFEF); - params.searchLineEditParams = sle; - - ReadingListIconsParams rli; - rli.labelColors = { - { "red", QColor(0xf67a7b) }, - { "orange", QColor(0xf5c240) }, - { "yellow", QColor(0xf2e446) }, - { "green", QColor(0xade738) }, - { "cyan", QColor(0xa0fddb) }, - { "blue", QColor(0x82c7ff) }, - { "violet", QColor(0x8f95ff) }, - { "purple", QColor(0xd692fc) }, - { "pink", QColor(0xfd9cda) }, - { "white", QColor(0xfcfcfc) }, - { "light", QColor(0xcbcbcb) }, - { "dark", QColor(0xb7b7b7) } - }; - rli.labelShadowColor = QColor(0x8F8F8F); - rli.labelShadowSelectedColor = QColor(0x161616); - rli.readingListMainColor = QColor(0x808080); - rli.readingListMainSelectedColor = QColor(0x808080); - rli.favoritesMainColor = QColor(0xe15055); - rli.favoritesMainSelectedColor = QColor(0xe15055); - rli.currentlyReadingMainColor = QColor(0xffcc00); - rli.currentlyReadingMainSelectedColor = QColor(0xffcc00); - rli.currentlyReadingOuterColor = Qt::black; - rli.currentlyReadingOuterSelectedColor = Qt::black; - rli.specialListShadowColor = QColor(0x8F8F8F); - rli.specialListShadowSelectedColor = QColor(0x161616); - rli.listMainColor = QColor(0x808080); - rli.listMainSelectedColor = QColor(0xFFFFFF); - rli.listShadowColor = QColor(0x8F8F8F); - rli.listShadowSelectedColor = QColor(0x161616); - rli.listDetailColor = QColor(0xFFFFFF); - rli.listDetailSelectedColor = QColor(0x161616); - params.readingListIconsParams = rli; - - MenuIconsParams mi; - mi.iconColor = QColor(0x606060); - params.menuIconsParams = mi; - - DialogIconsParams dip; - dip.iconColor = mi.iconColor; - params.dialogIconsParams = dip; - - ShortcutsIconsParams sci; - sci.iconColor = QColor(0x606060); - params.shortcutsIconsParams = sci; - - ServerConfigDialogParams scd; - scd.backgroundColor = QColor(0xFFFFFF); - scd.titleTextColor = QColor(0x474747); - scd.qrMessageTextColor = QColor(0xA3A3A3); - scd.propagandaTextColor = QColor(0x4D4D4D); - scd.labelTextColor = QColor(0x575757); - scd.checkBoxTextColor = QColor(0x262626); - scd.qrBackgroundColor = Qt::white; - scd.qrForegroundColor = QColor(0x606060); - scd.decorationColor = QColor(0x606060); - params.serverConfigDialogParams = scd; - - return params; -} - -ThemeParams darkThemeParams() -{ - ThemeParams params; - params.themeName = "dark"; - params.defaultContentBackgroundColor = QColor(0x2A2A2A); - - ComicFlowColors cf; - cf.backgroundColor = QColor(0x111111); - cf.textColor = QColor(0x888888); - - ComicVineParams cv; - cv.contentTextColor = Qt::white; - cv.contentBackgroundColor = QColor(0x2B2B2B); - cv.contentAltBackgroundColor = QColor(0x2E2E2E); - cv.dialogBackgroundColor = QColor(0x404040); - - cv.tableBackgroundColor = QColor(0x2B2B2B); - cv.tableAltBackgroundColor = QColor(0x2E2E2E); - cv.tableBorderColor = QColor(0x242424); - cv.tableSelectedColor = QColor(0x555555); - cv.tableHeaderBackgroundColor = QColor(0x292929); - cv.tableHeaderGradientColor = QColor(0x292929); - cv.tableHeaderBorderColor = QColor(0x1F1F1F); - cv.tableHeaderTextColor = QColor(0xEBEBEB); - cv.tableScrollHandleColor = QColor(0xDDDDDD); - cv.tableScrollBackgroundColor = QColor(0x404040); - cv.tableSectionBorderLight = QColor(0xFEFEFE); - cv.tableSectionBorderDark = QColor(0xDFDFDF); - - cv.labelTextColor = Qt::white; - cv.labelBackgroundColor = QColor(0x2B2B2B); - cv.hyperlinkColor = QColor(0xFFCC00); - - cv.buttonBackgroundColor = QColor(0x2E2E2E); - cv.buttonTextColor = Qt::white; - cv.buttonBorderColor = QColor(0x242424); - - cv.radioUncheckedColor = QColor(0xE5E5E5); - - cv.radioCheckedBackgroundColor = QColor(0xE5E5E5); - cv.radioCheckedIndicatorColor = QColor(0x5F5F5F); - - cv.checkBoxTickColor = Qt::white; - - cv.toolButtonAccentColor = QColor(0x282828); - - cv.downArrowColor = QColor(0x9F9F9F); - cv.upArrowColor = QColor(0x9F9F9F); - - cv.busyIndicatorColor = Qt::white; - cv.navIconColor = Qt::white; - cv.rowIconColor = QColor(0xE5E5E5); - - cv.t = ComicVineThemeTemplates(); - - params.comicFlowColors = cf; - params.comicVineParams = cv; - - params.helpAboutDialogParams.headingColor = QColor(0xE0E0E0); - params.helpAboutDialogParams.linkColor = QColor(0xD4A84B); - - WhatsNewDialogParams wnp; - wnp.backgroundColor = QColor(0x2A2A2A); - wnp.headerTextColor = QColor(0xE0E0E0); - wnp.versionTextColor = QColor(0x858585); - wnp.contentTextColor = QColor(0xE0E0E0); - wnp.linkColor = QColor(0xE8B800); - wnp.closeButtonColor = QColor(0xDDDDDD); - wnp.headerDecorationColor = QColor(0xE8B800); - params.whatsNewDialogParams = wnp; - - EmptyContainerParams ec; - ec.backgroundColor = QColor(0x2A2A2A); - ec.titleTextColor = QColor(0xCCCCCC); - ec.textColor = QColor(0xCCCCCC); - ec.descriptionTextColor = QColor(0xAAAAAA); - ec.searchIconColor = QColor(0x4C4C4C); - ec.t = EmptyContainerThemeTemplates(); - params.emptyContainerParams = ec; - - SidebarParams sb; - sb.backgroundColor = QColor(0x454545); - sb.separatorColor = QColor(0xBDBFBF); - sb.sectionSeparatorColor = QColor(0x575757); - sb.uppercaseLabels = true; - sb.titleTextColor = QColor(0xBDBFBF); - sb.titleDropShadowColor = Qt::black; - sb.busyIndicatorColor = Qt::white; - params.sidebarParams = sb; - - ImportWidgetParams iw; - iw.backgroundColor = QColor(0x2A2A2A); - iw.titleTextColor = QColor(0xCCCCCC); - iw.descriptionTextColor = QColor(0xAAAAAA); - iw.currentComicTextColor = QColor(0xAAAAAA); - iw.coversViewBackgroundColor = QColor(0x3A3A3A); - iw.coversLabelColor = QColor(0xAAAAAA); - iw.coversDecorationBgColor = QColor(0x3A3A3A); - iw.coversDecorationShadowColor = QColor(0x1A1A1A); - iw.modeIconColor = QColor(0x4A4A4A); - iw.iconColor = QColor(0xCCCCCC); - iw.iconCheckedColor = QColor(0xAAAAAA); - params.importWidgetParams = iw; - - TreeViewParams tv; - tv.textColor = QColor(0xDDDFDF); - tv.selectionBackgroundColor = QColor(0x2E2E2E); - tv.scrollBackgroundColor = QColor(0x404040); - tv.scrollHandleColor = QColor(0xDDDDDD); - tv.selectedTextColor = Qt::white; - tv.folderIndicatorColor = QColor(237, 197, 24); - tv.branchIndicatorColor = QColor(0xE0E0E0); - tv.branchIndicatorSelectedColor = QColor(0xFFFFFF); - tv.folderIconColor = QColor(0xE0E0E0); - tv.folderIconShadowColor = QColor(0xFF000000); - tv.folderIconSelectedColor = QColor(0xE0E0E0); - tv.folderIconSelectedShadowColor = QColor(0xFF000000); - tv.folderReadOverlayColor = QColor(0x222222); - tv.folderReadOverlaySelectedColor = QColor(0x222222); - params.treeViewParams = tv; - - TableViewParams tav; - tav.alternateBackgroundColor = QColor(0x2E2E2E); - tav.backgroundColor = QColor(0x2A2A2A); - tav.headerBackgroundColor = QColor(0x2A2A2A); - tav.headerBorderColor = QColor(0x1F1F1F); - tav.headerGradientColor = QColor(0x252525); - tav.itemBorderBottomColor = QColor(0x1F1F1F); - tav.itemBorderTopColor = QColor(0x353535); - tav.itemBorderBottomWidth = 1; - tav.itemBorderTopWidth = 1; - tav.itemTextColor = QColor(0xDDDDDD); - tav.selectedColor = QColor(0x555555); - tav.selectedTextColor = QColor(0xFFFFFF); - tav.headerTextColor = QColor(0xDDDDDD); - tav.starRatingColor = QColor(0xE9BE0F); - tav.starRatingSelectedColor = QColor(0xFFFFFF); - tav.t = TableViewThemeTemplates(); - params.tableViewParams = tav; - - QmlViewParams qv; - qv.backgroundColor = QColor(0x2A2A2A); - qv.cellColor = QColor(0x212121); - qv.cellColorWithBackground = QColor(0x21, 0x21, 0x21, 0x99); - qv.selectedColor = QColor(0x121212); - qv.selectedBorderColor = QColor(0xFFCC00); - qv.borderColor = QColor(0x121212); - qv.titleColor = QColor(0xFFFFFF); - qv.textColor = QColor(0xA8A8A8); - qv.showDropShadow = true; - qv.infoBackgroundColor = QColor(0x2E2E2E); - qv.infoBorderColor = QColor(0x404040); - qv.infoShadowColor = Qt::black; - qv.infoTextColor = QColor(0xB0B0B0); - qv.infoTitleColor = QColor(0xFFFFFF); - qv.ratingUnselectedColor = QColor(0x1C1C1C); - qv.ratingSelectedColor = QColor(0xFFFFFF); - qv.favUncheckedColor = QColor(0x1C1C1C); - qv.favCheckedColor = QColor(0xE84852); - qv.readTickUncheckedColor = QColor(0x1C1C1C); - qv.readTickCheckedColor = QColor(0xE84852); - qv.currentComicBackgroundColor = QColor(0x00, 0x00, 0x00, 0x88); - qv.continueReadingBackgroundColor = QColor(0x00, 0x00, 0x00, 0x88); - qv.continueReadingColor = QColor(0xFFFFFF); - qv.backgroundBlurOverlayColor = QColor(0x2A2A2A); - params.qmlViewParams = qv; - - MainToolbarParams mt; - mt.backgroundColor = QColor(0x2A2A2A); - mt.folderNameColor = QColor(0xDDDDDD); - mt.dividerColor = QColor(0x555555); - mt.iconColor = QColor(0xDDDDDD); - mt.iconDisabledColor = QColor(0x666666); - params.mainToolbarParams = mt; - - ContentSplitterParams cs; - cs.handleColor = QColor(0x1F1F1F); - cs.horizontalHandleHeight = 4; - cs.verticalHandleWidth = 4; - params.contentSplitterParams = cs; - - SidebarIconsParams si; - si.iconColor = QColor(0xE0E0E0); - si.shadowColor = QColor(0xFF000000); - si.useSystemFolderIcons = false; - params.sidebarIconsParams = si; - - LibraryItemParams li; - li.textColor = QColor(0xDDDFDF); - li.libraryIconColor = QColor(0xDDDFDF); - li.libraryIconShadowColor = QColor(0xFF000000); - li.selectedTextColor = Qt::white; - li.selectedBackgroundColor = QColor(0x2E2E2E); - li.libraryIconSelectedColor = Qt::white; - li.libraryOptionsIconColor = Qt::white; - params.libraryItemParams = li; - - ComicsViewToolbarParams cvt; - cvt.backgroundColor = QColor(0x2A2A2A); - cvt.separatorColor = QColor(0x444444); - cvt.checkedBackgroundColor = QColor(0x555555); - cvt.iconColor = QColor(0xDDDDDD); - params.comicsViewToolbarParams = cvt; - - SearchLineEditParams sle; - sle.textColor = QColor(0xABABAB); - sle.backgroundColor = QColor(0x404040); - sle.iconColor = QColor(0xF7F7F7); - params.searchLineEditParams = sle; - - ReadingListIconsParams rli; - rli.labelColors = { - { "red", QColor(0xf67a7b) }, - { "orange", QColor(0xf5c240) }, - { "yellow", QColor(0xf2e446) }, - { "green", QColor(0xade738) }, - { "cyan", QColor(0xa0fddb) }, - { "blue", QColor(0x82c7ff) }, - { "violet", QColor(0x8f95ff) }, - { "purple", QColor(0xd692fc) }, - { "pink", QColor(0xfd9cda) }, - { "white", QColor(0xfcfcfc) }, - { "light", QColor(0xcbcbcb) }, - { "dark", QColor(0xb7b7b7) } - }; - rli.labelShadowColor = Qt::black; - rli.labelShadowSelectedColor = Qt::black; - rli.readingListMainColor = QColor(0xe7e7e7); - rli.readingListMainSelectedColor = QColor(0xe7e7e7); - rli.favoritesMainColor = QColor(0xe15055); - rli.favoritesMainSelectedColor = QColor(0xe15055); - rli.currentlyReadingMainColor = QColor(0xffcc00); - rli.currentlyReadingMainSelectedColor = QColor(0xffcc00); - rli.currentlyReadingOuterColor = Qt::black; - rli.currentlyReadingOuterSelectedColor = Qt::black; - rli.specialListShadowColor = Qt::black; - rli.specialListShadowSelectedColor = Qt::black; - rli.listMainColor = QColor(0xe7e7e7); - rli.listMainSelectedColor = QColor(0xe7e7e7); - rli.listShadowColor = Qt::black; - rli.listShadowSelectedColor = Qt::black; - rli.listDetailColor = QColor(0x464646); - rli.listDetailSelectedColor = QColor(0x464646); - params.readingListIconsParams = rli; - - MenuIconsParams mi; - mi.iconColor = QColor(0xF7F7F7); - params.menuIconsParams = mi; - - DialogIconsParams dip; - dip.iconColor = mi.iconColor; - params.dialogIconsParams = dip; - - ShortcutsIconsParams sci; - sci.iconColor = QColor(0xF7F7F7); - params.shortcutsIconsParams = sci; - - ServerConfigDialogParams scd; - scd.backgroundColor = QColor(0x2A2A2A); - scd.titleTextColor = QColor(0xD0D0D0); - scd.qrMessageTextColor = QColor(0xA3A3A3); - scd.propagandaTextColor = QColor(0xB0B0B0); - scd.labelTextColor = QColor(0xC0C0C0); - scd.checkBoxTextColor = QColor(0xDDDDDD); - scd.qrBackgroundColor = QColor(0x2A2A2A); - scd.qrForegroundColor = Qt::white; - scd.decorationColor = QColor(0xF7F7F7); - params.serverConfigDialogParams = scd; - - return params; + if (json.contains("comicFlow")) { + const auto o = json["comicFlow"].toObject(); + p.comicFlowColors.backgroundColor = colorFromJson(o, "backgroundColor", p.comicFlowColors.backgroundColor); + p.comicFlowColors.textColor = colorFromJson(o, "textColor", p.comicFlowColors.textColor); + } + + if (json.contains("comicVine")) { + const auto o = json["comicVine"].toObject(); + auto &cv = p.comicVineParams; + cv.contentTextColor = colorFromJson(o, "contentTextColor", cv.contentTextColor); + cv.contentBackgroundColor = colorFromJson(o, "contentBackgroundColor", cv.contentBackgroundColor); + cv.contentAltBackgroundColor = colorFromJson(o, "contentAltBackgroundColor", cv.contentAltBackgroundColor); + cv.dialogBackgroundColor = colorFromJson(o, "dialogBackgroundColor", cv.dialogBackgroundColor); + cv.tableBackgroundColor = colorFromJson(o, "tableBackgroundColor", cv.tableBackgroundColor); + cv.tableAltBackgroundColor = colorFromJson(o, "tableAltBackgroundColor", cv.tableAltBackgroundColor); + cv.tableBorderColor = colorFromJson(o, "tableBorderColor", cv.tableBorderColor); + cv.tableSelectedColor = colorFromJson(o, "tableSelectedColor", cv.tableSelectedColor); + cv.tableHeaderBackgroundColor = colorFromJson(o, "tableHeaderBackgroundColor", cv.tableHeaderBackgroundColor); + cv.tableHeaderGradientColor = colorFromJson(o, "tableHeaderGradientColor", cv.tableHeaderGradientColor); + cv.tableHeaderBorderColor = colorFromJson(o, "tableHeaderBorderColor", cv.tableHeaderBorderColor); + cv.tableHeaderTextColor = colorFromJson(o, "tableHeaderTextColor", cv.tableHeaderTextColor); + cv.tableScrollHandleColor = colorFromJson(o, "tableScrollHandleColor", cv.tableScrollHandleColor); + cv.tableScrollBackgroundColor = colorFromJson(o, "tableScrollBackgroundColor", cv.tableScrollBackgroundColor); + cv.tableSectionBorderLight = colorFromJson(o, "tableSectionBorderLight", cv.tableSectionBorderLight); + cv.tableSectionBorderDark = colorFromJson(o, "tableSectionBorderDark", cv.tableSectionBorderDark); + cv.labelTextColor = colorFromJson(o, "labelTextColor", cv.labelTextColor); + cv.labelBackgroundColor = colorFromJson(o, "labelBackgroundColor", cv.labelBackgroundColor); + cv.hyperlinkColor = colorFromJson(o, "hyperlinkColor", cv.hyperlinkColor); + cv.buttonBackgroundColor = colorFromJson(o, "buttonBackgroundColor", cv.buttonBackgroundColor); + cv.buttonTextColor = colorFromJson(o, "buttonTextColor", cv.buttonTextColor); + cv.buttonBorderColor = colorFromJson(o, "buttonBorderColor", cv.buttonBorderColor); + cv.radioUncheckedColor = colorFromJson(o, "radioUncheckedColor", cv.radioUncheckedColor); + cv.radioCheckedBackgroundColor = colorFromJson(o, "radioCheckedBackgroundColor", cv.radioCheckedBackgroundColor); + cv.radioCheckedIndicatorColor = colorFromJson(o, "radioCheckedIndicatorColor", cv.radioCheckedIndicatorColor); + cv.checkBoxTickColor = colorFromJson(o, "checkBoxTickColor", cv.checkBoxTickColor); + cv.toolButtonAccentColor = colorFromJson(o, "toolButtonAccentColor", cv.toolButtonAccentColor); + cv.downArrowColor = colorFromJson(o, "downArrowColor", cv.downArrowColor); + cv.upArrowColor = colorFromJson(o, "upArrowColor", cv.upArrowColor); + cv.busyIndicatorColor = colorFromJson(o, "busyIndicatorColor", cv.busyIndicatorColor); + cv.navIconColor = colorFromJson(o, "navIconColor", cv.navIconColor); + cv.rowIconColor = colorFromJson(o, "rowIconColor", cv.rowIconColor); + } + + if (json.contains("helpAboutDialog")) { + const auto o = json["helpAboutDialog"].toObject(); + p.helpAboutDialogParams.headingColor = colorFromJson(o, "headingColor", p.helpAboutDialogParams.headingColor); + p.helpAboutDialogParams.linkColor = colorFromJson(o, "linkColor", p.helpAboutDialogParams.linkColor); + } + + if (json.contains("whatsNewDialog")) { + const auto o = json["whatsNewDialog"].toObject(); + auto &wn = p.whatsNewDialogParams; + wn.backgroundColor = colorFromJson(o, "backgroundColor", wn.backgroundColor); + wn.headerTextColor = colorFromJson(o, "headerTextColor", wn.headerTextColor); + wn.versionTextColor = colorFromJson(o, "versionTextColor", wn.versionTextColor); + wn.contentTextColor = colorFromJson(o, "contentTextColor", wn.contentTextColor); + wn.linkColor = colorFromJson(o, "linkColor", wn.linkColor); + wn.closeButtonColor = colorFromJson(o, "closeButtonColor", wn.closeButtonColor); + wn.headerDecorationColor = colorFromJson(o, "headerDecorationColor", wn.headerDecorationColor); + } + + if (json.contains("emptyContainer")) { + const auto o = json["emptyContainer"].toObject(); + auto &ec = p.emptyContainerParams; + ec.backgroundColor = colorFromJson(o, "backgroundColor", ec.backgroundColor); + ec.titleTextColor = colorFromJson(o, "titleTextColor", ec.titleTextColor); + ec.textColor = colorFromJson(o, "textColor", ec.textColor); + ec.descriptionTextColor = colorFromJson(o, "descriptionTextColor", ec.descriptionTextColor); + ec.searchIconColor = colorFromJson(o, "searchIconColor", ec.searchIconColor); + } + + if (json.contains("sidebar")) { + const auto o = json["sidebar"].toObject(); + auto &sb = p.sidebarParams; + sb.backgroundColor = colorFromJson(o, "backgroundColor", sb.backgroundColor); + sb.separatorColor = colorFromJson(o, "separatorColor", sb.separatorColor); + sb.sectionSeparatorColor = colorFromJson(o, "sectionSeparatorColor", sb.sectionSeparatorColor); + if (o.contains("uppercaseLabels")) + sb.uppercaseLabels = o["uppercaseLabels"].toBool(sb.uppercaseLabels); + sb.titleTextColor = colorFromJson(o, "titleTextColor", sb.titleTextColor); + sb.titleDropShadowColor = colorFromJson(o, "titleDropShadowColor", sb.titleDropShadowColor); + sb.busyIndicatorColor = colorFromJson(o, "busyIndicatorColor", sb.busyIndicatorColor); + } + + if (json.contains("sidebarIcons")) { + const auto o = json["sidebarIcons"].toObject(); + auto &si = p.sidebarIconsParams; + si.iconColor = colorFromJson(o, "iconColor", si.iconColor); + si.shadowColor = colorFromJson(o, "shadowColor", si.shadowColor); + if (o.contains("useSystemFolderIcons")) + si.useSystemFolderIcons = o["useSystemFolderIcons"].toBool(si.useSystemFolderIcons); + } + + if (json.contains("libraryItem")) { + const auto o = json["libraryItem"].toObject(); + auto &li = p.libraryItemParams; + li.textColor = colorFromJson(o, "textColor", li.textColor); + li.libraryIconColor = colorFromJson(o, "libraryIconColor", li.libraryIconColor); + li.libraryIconShadowColor = colorFromJson(o, "libraryIconShadowColor", li.libraryIconShadowColor); + li.selectedTextColor = colorFromJson(o, "selectedTextColor", li.selectedTextColor); + li.selectedBackgroundColor = colorFromJson(o, "selectedBackgroundColor", li.selectedBackgroundColor); + li.libraryIconSelectedColor = colorFromJson(o, "libraryIconSelectedColor", li.libraryIconSelectedColor); + li.libraryOptionsIconColor = colorFromJson(o, "libraryOptionsIconColor", li.libraryOptionsIconColor); + } + + if (json.contains("importWidget")) { + const auto o = json["importWidget"].toObject(); + auto &iw = p.importWidgetParams; + iw.backgroundColor = colorFromJson(o, "backgroundColor", iw.backgroundColor); + iw.titleTextColor = colorFromJson(o, "titleTextColor", iw.titleTextColor); + iw.descriptionTextColor = colorFromJson(o, "descriptionTextColor", iw.descriptionTextColor); + iw.currentComicTextColor = colorFromJson(o, "currentComicTextColor", iw.currentComicTextColor); + iw.coversViewBackgroundColor = colorFromJson(o, "coversViewBackgroundColor", iw.coversViewBackgroundColor); + iw.coversLabelColor = colorFromJson(o, "coversLabelColor", iw.coversLabelColor); + iw.coversDecorationBgColor = colorFromJson(o, "coversDecorationBgColor", iw.coversDecorationBgColor); + iw.coversDecorationShadowColor = colorFromJson(o, "coversDecorationShadowColor", iw.coversDecorationShadowColor); + iw.modeIconColor = colorFromJson(o, "modeIconColor", iw.modeIconColor); + iw.iconColor = colorFromJson(o, "iconColor", iw.iconColor); + iw.iconCheckedColor = colorFromJson(o, "iconCheckedColor", iw.iconCheckedColor); + } + + if (json.contains("serverConfigDialog")) { + const auto o = json["serverConfigDialog"].toObject(); + auto &scd2 = p.serverConfigDialogParams; + scd2.backgroundColor = colorFromJson(o, "backgroundColor", scd2.backgroundColor); + scd2.titleTextColor = colorFromJson(o, "titleTextColor", scd2.titleTextColor); + scd2.qrMessageTextColor = colorFromJson(o, "qrMessageTextColor", scd2.qrMessageTextColor); + scd2.propagandaTextColor = colorFromJson(o, "propagandaTextColor", scd2.propagandaTextColor); + scd2.labelTextColor = colorFromJson(o, "labelTextColor", scd2.labelTextColor); + scd2.checkBoxTextColor = colorFromJson(o, "checkBoxTextColor", scd2.checkBoxTextColor); + scd2.qrBackgroundColor = colorFromJson(o, "qrBackgroundColor", scd2.qrBackgroundColor); + scd2.qrForegroundColor = colorFromJson(o, "qrForegroundColor", scd2.qrForegroundColor); + scd2.decorationColor = colorFromJson(o, "decorationColor", scd2.decorationColor); + } + + if (json.contains("mainToolbar")) { + const auto o = json["mainToolbar"].toObject(); + auto &mt = p.mainToolbarParams; + mt.backgroundColor = colorFromJson(o, "backgroundColor", mt.backgroundColor); + mt.folderNameColor = colorFromJson(o, "folderNameColor", mt.folderNameColor); + mt.dividerColor = colorFromJson(o, "dividerColor", mt.dividerColor); + mt.iconColor = colorFromJson(o, "iconColor", mt.iconColor); + mt.iconDisabledColor = colorFromJson(o, "iconDisabledColor", mt.iconDisabledColor); + } + + if (json.contains("contentSplitter")) { + const auto o = json["contentSplitter"].toObject(); + auto &cs = p.contentSplitterParams; + cs.handleColor = colorFromJson(o, "handleColor", cs.handleColor); + if (o.contains("horizontalHandleHeight")) + cs.horizontalHandleHeight = o["horizontalHandleHeight"].toInt(cs.horizontalHandleHeight); + if (o.contains("verticalHandleWidth")) + cs.verticalHandleWidth = o["verticalHandleWidth"].toInt(cs.verticalHandleWidth); + } + + if (json.contains("treeView")) { + const auto o = json["treeView"].toObject(); + auto &tv = p.treeViewParams; + tv.textColor = colorFromJson(o, "textColor", tv.textColor); + tv.selectionBackgroundColor = colorFromJson(o, "selectionBackgroundColor", tv.selectionBackgroundColor); + tv.scrollBackgroundColor = colorFromJson(o, "scrollBackgroundColor", tv.scrollBackgroundColor); + tv.scrollHandleColor = colorFromJson(o, "scrollHandleColor", tv.scrollHandleColor); + tv.selectedTextColor = colorFromJson(o, "selectedTextColor", tv.selectedTextColor); + tv.folderIndicatorColor = colorFromJson(o, "folderIndicatorColor", tv.folderIndicatorColor); + tv.branchIndicatorColor = colorFromJson(o, "branchIndicatorColor", tv.branchIndicatorColor); + tv.branchIndicatorSelectedColor = colorFromJson(o, "branchIndicatorSelectedColor", tv.branchIndicatorSelectedColor); + tv.folderIconColor = colorFromJson(o, "folderIconColor", tv.folderIconColor); + tv.folderIconShadowColor = colorFromJson(o, "folderIconShadowColor", tv.folderIconShadowColor); + tv.folderIconSelectedColor = colorFromJson(o, "folderIconSelectedColor", tv.folderIconSelectedColor); + tv.folderIconSelectedShadowColor = colorFromJson(o, "folderIconSelectedShadowColor", tv.folderIconSelectedShadowColor); + tv.folderReadOverlayColor = colorFromJson(o, "folderReadOverlayColor", tv.folderReadOverlayColor); + tv.folderReadOverlaySelectedColor = colorFromJson(o, "folderReadOverlaySelectedColor", tv.folderReadOverlaySelectedColor); + } + + if (json.contains("tableView")) { + const auto o = json["tableView"].toObject(); + auto &tbv = p.tableViewParams; + tbv.alternateBackgroundColor = colorFromJson(o, "alternateBackgroundColor", tbv.alternateBackgroundColor); + tbv.backgroundColor = colorFromJson(o, "backgroundColor", tbv.backgroundColor); + tbv.headerBackgroundColor = colorFromJson(o, "headerBackgroundColor", tbv.headerBackgroundColor); + tbv.headerBorderColor = colorFromJson(o, "headerBorderColor", tbv.headerBorderColor); + tbv.headerGradientColor = colorFromJson(o, "headerGradientColor", tbv.headerGradientColor); + tbv.itemBorderBottomColor = colorFromJson(o, "itemBorderBottomColor", tbv.itemBorderBottomColor); + tbv.itemBorderTopColor = colorFromJson(o, "itemBorderTopColor", tbv.itemBorderTopColor); + tbv.itemBorderBottomWidth = o["itemBorderBottomWidth"].toInt(tbv.itemBorderBottomWidth); + tbv.itemBorderTopWidth = o["itemBorderTopWidth"].toInt(tbv.itemBorderTopWidth); + tbv.itemTextColor = colorFromJson(o, "itemTextColor", tbv.itemTextColor); + tbv.selectedColor = colorFromJson(o, "selectedColor", tbv.selectedColor); + tbv.selectedTextColor = colorFromJson(o, "selectedTextColor", tbv.selectedTextColor); + tbv.headerTextColor = colorFromJson(o, "headerTextColor", tbv.headerTextColor); + tbv.starRatingColor = colorFromJson(o, "starRatingColor", tbv.starRatingColor); + tbv.starRatingSelectedColor = colorFromJson(o, "starRatingSelectedColor", tbv.starRatingSelectedColor); + } + + if (json.contains("qmlView")) { + const auto o = json["qmlView"].toObject(); + auto &qv = p.qmlViewParams; + qv.backgroundColor = colorFromJson(o, "backgroundColor", qv.backgroundColor); + qv.cellColor = colorFromJson(o, "cellColor", qv.cellColor); + qv.cellColorWithBackground = colorFromJson(o, "cellColorWithBackground", qv.cellColorWithBackground); + qv.selectedColor = colorFromJson(o, "selectedColor", qv.selectedColor); + qv.selectedBorderColor = colorFromJson(o, "selectedBorderColor", qv.selectedBorderColor); + qv.borderColor = colorFromJson(o, "borderColor", qv.borderColor); + qv.titleColor = colorFromJson(o, "titleColor", qv.titleColor); + qv.textColor = colorFromJson(o, "textColor", qv.textColor); + if (o.contains("showDropShadow")) + qv.showDropShadow = o["showDropShadow"].toBool(qv.showDropShadow); + qv.infoBackgroundColor = colorFromJson(o, "infoBackgroundColor", qv.infoBackgroundColor); + qv.infoBorderColor = colorFromJson(o, "infoBorderColor", qv.infoBorderColor); + qv.infoShadowColor = colorFromJson(o, "infoShadowColor", qv.infoShadowColor); + qv.infoTextColor = colorFromJson(o, "infoTextColor", qv.infoTextColor); + qv.infoTitleColor = colorFromJson(o, "infoTitleColor", qv.infoTitleColor); + qv.ratingUnselectedColor = colorFromJson(o, "ratingUnselectedColor", qv.ratingUnselectedColor); + qv.ratingSelectedColor = colorFromJson(o, "ratingSelectedColor", qv.ratingSelectedColor); + qv.favUncheckedColor = colorFromJson(o, "favUncheckedColor", qv.favUncheckedColor); + qv.favCheckedColor = colorFromJson(o, "favCheckedColor", qv.favCheckedColor); + qv.readTickUncheckedColor = colorFromJson(o, "readTickUncheckedColor", qv.readTickUncheckedColor); + qv.readTickCheckedColor = colorFromJson(o, "readTickCheckedColor", qv.readTickCheckedColor); + qv.currentComicBackgroundColor = colorFromJson(o, "currentComicBackgroundColor", qv.currentComicBackgroundColor); + qv.continueReadingBackgroundColor = colorFromJson(o, "continueReadingBackgroundColor", qv.continueReadingBackgroundColor); + qv.continueReadingColor = colorFromJson(o, "continueReadingColor", qv.continueReadingColor); + qv.backgroundBlurOverlayColor = colorFromJson(o, "backgroundBlurOverlayColor", qv.backgroundBlurOverlayColor); + } + + if (json.contains("comicsViewToolbar")) { + const auto o = json["comicsViewToolbar"].toObject(); + auto &cvt = p.comicsViewToolbarParams; + cvt.backgroundColor = colorFromJson(o, "backgroundColor", cvt.backgroundColor); + cvt.separatorColor = colorFromJson(o, "separatorColor", cvt.separatorColor); + cvt.checkedBackgroundColor = colorFromJson(o, "checkedBackgroundColor", cvt.checkedBackgroundColor); + cvt.iconColor = colorFromJson(o, "iconColor", cvt.iconColor); + } + + if (json.contains("searchLineEdit")) { + const auto o = json["searchLineEdit"].toObject(); + auto &sle = p.searchLineEditParams; + sle.textColor = colorFromJson(o, "textColor", sle.textColor); + sle.backgroundColor = colorFromJson(o, "backgroundColor", sle.backgroundColor); + sle.iconColor = colorFromJson(o, "iconColor", sle.iconColor); + } + + if (json.contains("readingListIcons")) { + const auto o = json["readingListIcons"].toObject(); + auto &rli = p.readingListIconsParams; + if (o.contains("labelColors")) { + const auto lc = o["labelColors"].toObject(); + for (auto it = lc.constBegin(); it != lc.constEnd(); ++it) { + QColor c(it.value().toString()); + if (c.isValid()) + rli.labelColors[it.key()] = c; + } + } + rli.labelShadowColor = colorFromJson(o, "labelShadowColor", rli.labelShadowColor); + rli.labelShadowSelectedColor = colorFromJson(o, "labelShadowSelectedColor", rli.labelShadowSelectedColor); + rli.readingListMainColor = colorFromJson(o, "readingListMainColor", rli.readingListMainColor); + rli.readingListMainSelectedColor = colorFromJson(o, "readingListMainSelectedColor", rli.readingListMainSelectedColor); + rli.favoritesMainColor = colorFromJson(o, "favoritesMainColor", rli.favoritesMainColor); + rli.favoritesMainSelectedColor = colorFromJson(o, "favoritesMainSelectedColor", rli.favoritesMainSelectedColor); + rli.currentlyReadingMainColor = colorFromJson(o, "currentlyReadingMainColor", rli.currentlyReadingMainColor); + rli.currentlyReadingMainSelectedColor = colorFromJson(o, "currentlyReadingMainSelectedColor", rli.currentlyReadingMainSelectedColor); + rli.currentlyReadingOuterColor = colorFromJson(o, "currentlyReadingOuterColor", rli.currentlyReadingOuterColor); + rli.currentlyReadingOuterSelectedColor = colorFromJson(o, "currentlyReadingOuterSelectedColor", rli.currentlyReadingOuterSelectedColor); + rli.specialListShadowColor = colorFromJson(o, "specialListShadowColor", rli.specialListShadowColor); + rli.specialListShadowSelectedColor = colorFromJson(o, "specialListShadowSelectedColor", rli.specialListShadowSelectedColor); + rli.listMainColor = colorFromJson(o, "listMainColor", rli.listMainColor); + rli.listMainSelectedColor = colorFromJson(o, "listMainSelectedColor", rli.listMainSelectedColor); + rli.listShadowColor = colorFromJson(o, "listShadowColor", rli.listShadowColor); + rli.listShadowSelectedColor = colorFromJson(o, "listShadowSelectedColor", rli.listShadowSelectedColor); + rli.listDetailColor = colorFromJson(o, "listDetailColor", rli.listDetailColor); + rli.listDetailSelectedColor = colorFromJson(o, "listDetailSelectedColor", rli.listDetailSelectedColor); + } + + if (json.contains("dialogIcons")) { + const auto o = json["dialogIcons"].toObject(); + p.dialogIconsParams.iconColor = colorFromJson(o, "iconColor", p.dialogIconsParams.iconColor); + } + + if (json.contains("menuIcons")) { + const auto o = json["menuIcons"].toObject(); + p.menuIconsParams.iconColor = colorFromJson(o, "iconColor", p.menuIconsParams.iconColor); + } + + if (json.contains("shortcutsIcons")) { + const auto o = json["shortcutsIcons"].toObject(); + p.shortcutsIconsParams.iconColor = colorFromJson(o, "iconColor", p.shortcutsIconsParams.iconColor); + } + + if (json.contains("meta")) { + const auto o = json["meta"].toObject(); + p.meta.id = o["id"].toString(p.meta.id); + p.meta.displayName = o["displayName"].toString(p.meta.displayName); + const QString variantStr = o["variant"].toString(); + if (variantStr == "light") + p.meta.variant = ThemeVariant::Light; + else if (variantStr == "dark") + p.meta.variant = ThemeVariant::Dark; + } + + Theme theme = makeTheme(p); + theme.sourceJson = json; + return theme; } diff --git a/YACReaderLibrary/themes/theme_factory.h b/YACReaderLibrary/themes/theme_factory.h index 72cb4ca2..ebed9bbb 100644 --- a/YACReaderLibrary/themes/theme_factory.h +++ b/YACReaderLibrary/themes/theme_factory.h @@ -2,8 +2,9 @@ #define THEME_FACTORY_H #include "theme.h" -#include "theme_id.h" -Theme makeTheme(ThemeId themeId); +#include + +Theme makeTheme(const QJsonObject &json); #endif // THEME_FACTORY_H diff --git a/YACReaderLibrary/themes/themes.qrc b/YACReaderLibrary/themes/themes.qrc new file mode 100644 index 00000000..62ec21fe --- /dev/null +++ b/YACReaderLibrary/themes/themes.qrc @@ -0,0 +1,7 @@ + + + builtin_classic.json + builtin_light.json + builtin_dark.json + + diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index c754f552..ae0c1639 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -111,17 +111,27 @@ add_library(common_gui STATIC # themes infrastructure (does NOT depend on app-specific theme.h) themes/icon_utils.h themes/icon_utils.cpp - themes/theme_id.h + themes/appearance_configuration.h + themes/appearance_configuration.cpp + themes/theme_variant.h themes/themable.h themes/yacreader_icon.h themes/shared/help_about_dialog_theme.h themes/shared/whats_new_dialog_theme.h + themes/theme_editor_dialog.h + themes/theme_editor_dialog.cpp + themes/theme_meta.h + themes/theme_repository.h + themes/theme_repository.cpp + themes/appearance_tab_widget.h + themes/appearance_tab_widget.cpp ) target_include_directories(common_gui PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/themes ${CMAKE_CURRENT_SOURCE_DIR}/themes/shared ) + target_link_libraries(common_gui PUBLIC Qt::Core Qt::Core5Compat diff --git a/common/themes/appearance_config_images.qrc b/common/themes/appearance_config_images.qrc new file mode 100644 index 00000000..c274e563 --- /dev/null +++ b/common/themes/appearance_config_images.qrc @@ -0,0 +1,7 @@ + + + ../../images/appearance_config/theme-mode-system.svg + ../../images/appearance_config/theme-mode-light.svg + ../../images/appearance_config/theme-mode-dark.svg + + diff --git a/common/themes/appearance_configuration.cpp b/common/themes/appearance_configuration.cpp new file mode 100644 index 00000000..dade4c26 --- /dev/null +++ b/common/themes/appearance_configuration.cpp @@ -0,0 +1,96 @@ +#include "appearance_configuration.h" + +#include + +static constexpr auto kGroup = "Appearance"; +static constexpr auto kMode = "ThemeMode"; +static constexpr auto kLightId = "LightThemeId"; +static constexpr auto kDarkId = "DarkThemeId"; +static constexpr auto kFixedId = "FixedThemeId"; + +static QString themeModeToString(ThemeMode mode) +{ + switch (mode) { + case ThemeMode::FollowSystem: + return "FollowSystem"; + case ThemeMode::Light: + return "Light"; + case ThemeMode::Dark: + return "Dark"; + case ThemeMode::ForcedTheme: + return "ForcedTheme"; + } + return "FollowSystem"; +} + +static ThemeMode themeModeFromString(const QString &s) +{ + if (s == "Light") + return ThemeMode::Light; + if (s == "Dark") + return ThemeMode::Dark; + if (s == "ForcedTheme") + return ThemeMode::ForcedTheme; + return ThemeMode::FollowSystem; +} + +AppearanceConfiguration::AppearanceConfiguration(const QString &settingsFilePath, QObject *parent) + : QObject(parent), path(settingsFilePath) +{ + load(); +} + +void AppearanceConfiguration::load() +{ + QSettings s(path, QSettings::IniFormat); + s.beginGroup(kGroup); + sel.mode = themeModeFromString(s.value(kMode, "FollowSystem").toString()); + sel.lightThemeId = s.value(kLightId, sel.lightThemeId).toString(); + sel.darkThemeId = s.value(kDarkId, sel.darkThemeId).toString(); + sel.fixedThemeId = s.value(kFixedId, sel.fixedThemeId).toString(); + s.endGroup(); +} + +void AppearanceConfiguration::write(const QString &key, const QString &value) +{ + QSettings s(path, QSettings::IniFormat); + s.beginGroup(kGroup); + s.setValue(key, value); + s.endGroup(); +} + +void AppearanceConfiguration::setMode(ThemeMode mode) +{ + if (sel.mode == mode) + return; + sel.mode = mode; + write(kMode, themeModeToString(mode)); + emit selectionChanged(); +} + +void AppearanceConfiguration::setLightThemeId(const QString &id) +{ + if (sel.lightThemeId == id) + return; + sel.lightThemeId = id; + write(kLightId, id); + emit selectionChanged(); +} + +void AppearanceConfiguration::setDarkThemeId(const QString &id) +{ + if (sel.darkThemeId == id) + return; + sel.darkThemeId = id; + write(kDarkId, id); + emit selectionChanged(); +} + +void AppearanceConfiguration::setFixedThemeId(const QString &id) +{ + if (sel.fixedThemeId == id) + return; + sel.fixedThemeId = id; + write(kFixedId, id); + emit selectionChanged(); +} diff --git a/common/themes/appearance_configuration.h b/common/themes/appearance_configuration.h new file mode 100644 index 00000000..bcc4d501 --- /dev/null +++ b/common/themes/appearance_configuration.h @@ -0,0 +1,48 @@ +#ifndef APPEARANCE_CONFIGURATION_H +#define APPEARANCE_CONFIGURATION_H + +#include +#include + +enum class ThemeMode { + FollowSystem, + Light, + Dark, + ForcedTheme, +}; + +struct ThemeSelection { + ThemeMode mode = ThemeMode::FollowSystem; + QString lightThemeId = "builtin/light"; + QString darkThemeId = "builtin/dark"; + QString fixedThemeId = "builtin/classic"; +}; + +// Persists theme selection settings to a QSettings INI file under the +// [Appearance] group. All access is on-demand (no persistent QSettings handle) +// so the caller does not need to manage a QSettings lifetime. +class AppearanceConfiguration : public QObject +{ + Q_OBJECT +public: + explicit AppearanceConfiguration(const QString &settingsFilePath, QObject *parent = nullptr); + + ThemeSelection selection() const { return sel; } + + void setMode(ThemeMode mode); + void setLightThemeId(const QString &id); + void setDarkThemeId(const QString &id); + void setFixedThemeId(const QString &id); + +signals: + void selectionChanged(); + +private: + QString path; + ThemeSelection sel; + + void load(); + void write(const QString &key, const QString &value); +}; + +#endif // APPEARANCE_CONFIGURATION_H diff --git a/common/themes/appearance_tab_widget.cpp b/common/themes/appearance_tab_widget.cpp new file mode 100644 index 00000000..8563fb05 --- /dev/null +++ b/common/themes/appearance_tab_widget.cpp @@ -0,0 +1,98 @@ +#include "appearance_tab_widget.h" + +#include "appearance_configuration.h" +#include "theme_editor_dialog.h" + +#include +#include +#include +#include +#include +#include +#include + +AppearanceTabWidget::AppearanceTabWidget( + AppearanceConfiguration *config, + std::function currentThemeJson, + std::function applyTheme, + QWidget *parent) + : QWidget(parent), config(config), currentThemeJson(std::move(currentThemeJson)), applyTheme(std::move(applyTheme)) +{ + // Color scheme selector + auto *modeBox = new QGroupBox(tr("Color scheme"), this); + auto *modeLayout = new QHBoxLayout(); + + auto *sysBtn = new QToolButton(); + auto *lightBtn = new QToolButton(); + auto *darkBtn = new QToolButton(); + + sysBtn->setText(tr("System")); + lightBtn->setText(tr("Light")); + darkBtn->setText(tr("Dark")); + + sysBtn->setIcon(QIcon(":/images/appearance_config/theme-mode-system.svg")); + lightBtn->setIcon(QIcon(":/images/appearance_config/theme-mode-light.svg")); + darkBtn->setIcon(QIcon(":/images/appearance_config/theme-mode-dark.svg")); + + for (auto *btn : { sysBtn, lightBtn, darkBtn }) { + btn->setCheckable(true); + btn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + btn->setIconSize(QSize(93, 58)); + btn->setMinimumSize(115, 90); + } + + auto *modeGroup = new QButtonGroup(this); + modeGroup->addButton(sysBtn, static_cast(ThemeMode::FollowSystem)); + modeGroup->addButton(lightBtn, static_cast(ThemeMode::Light)); + modeGroup->addButton(darkBtn, static_cast(ThemeMode::Dark)); + modeGroup->setExclusive(true); + + if (this->config) { + const auto mode = this->config->selection().mode; + if (auto *btn = modeGroup->button(static_cast(mode))) + btn->setChecked(true); + } + + connect(modeGroup, &QButtonGroup::idClicked, this, [this](int id) { + if (this->config) + this->config->setMode(static_cast(id)); + }); + + modeLayout->addStretch(); + modeLayout->addWidget(sysBtn); + modeLayout->addWidget(lightBtn); + modeLayout->addWidget(darkBtn); + modeLayout->addStretch(); + modeBox->setLayout(modeLayout); + + // Theme editor + auto *themeEditorBox = new QGroupBox(tr("Theme editor"), this); + auto *themeEditorLayout = new QVBoxLayout(); + auto *openBtn = new QPushButton(tr("Open Theme Editor...")); + themeEditorLayout->addWidget(openBtn); + themeEditorBox->setLayout(themeEditorLayout); + + connect(openBtn, &QPushButton::clicked, this, [this]() { + if (!themeEditor) { + QJsonObject json = this->currentThemeJson(); + if (json.isEmpty()) { + QMessageBox::critical(this, + tr("Theme editor error"), + tr("The current theme JSON could not be loaded.")); + return; + } + themeEditor = new ThemeEditorDialog(json, this); + themeEditor->setAttribute(Qt::WA_DeleteOnClose); + connect(themeEditor, &ThemeEditorDialog::themeJsonChanged, this, + [this](const QJsonObject &json) { this->applyTheme(json); }); + } + themeEditor->show(); + themeEditor->raise(); + themeEditor->activateWindow(); + }); + + auto *layout = new QVBoxLayout(this); + layout->addWidget(modeBox); + layout->addWidget(themeEditorBox); + layout->addStretch(); +} diff --git a/common/themes/appearance_tab_widget.h b/common/themes/appearance_tab_widget.h new file mode 100644 index 00000000..cbccb88b --- /dev/null +++ b/common/themes/appearance_tab_widget.h @@ -0,0 +1,29 @@ +#ifndef APPEARANCE_TAB_WIDGET_H +#define APPEARANCE_TAB_WIDGET_H + +#include +#include +#include +#include + +class AppearanceConfiguration; +class ThemeEditorDialog; + +class AppearanceTabWidget : public QWidget +{ + Q_OBJECT +public: + explicit AppearanceTabWidget( + AppearanceConfiguration *config, + std::function currentThemeJson, + std::function applyTheme, + QWidget *parent = nullptr); + +private: + AppearanceConfiguration *config; + std::function currentThemeJson; + std::function applyTheme; + QPointer themeEditor; +}; + +#endif // APPEARANCE_TAB_WIDGET_H diff --git a/common/themes/theme_editor_dialog.cpp b/common/themes/theme_editor_dialog.cpp new file mode 100644 index 00000000..eaeed515 --- /dev/null +++ b/common/themes/theme_editor_dialog.cpp @@ -0,0 +1,477 @@ +#include "theme_editor_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Role used to store the JSON path (QStringList) on each leaf item. +static const int PathRole = Qt::UserRole; +// Role used to distinguish color items from others. +static const int IsColorRole = Qt::UserRole + 1; +// Role used to distinguish boolean items. +static const int IsBoolRole = Qt::UserRole + 2; +// Role used to distinguish numeric items. +static const int IsNumberRole = Qt::UserRole + 3; + +static bool isColorString(const QString &s) +{ + // Accepts #RGB, #RRGGBB, #AARRGGBB + if (!s.startsWith('#')) + return false; + const int len = s.length(); + return len == 4 || len == 7 || len == 9; +} + +ThemeEditorDialog::ThemeEditorDialog(const QJsonObject ¶ms, QWidget *parent) + : QDialog(parent), params(params) +{ + setWindowTitle(tr("Theme Editor")); + resize(520, 700); + + // --- top toolbar --- + auto *expandBtn = new QPushButton(tr("+"), this); + auto *collapseBtn = new QPushButton(tr("-"), this); + auto *identifyBtn = new QPushButton(tr("i"), this); + expandBtn->setFixedWidth(28); + collapseBtn->setFixedWidth(28); + identifyBtn->setFixedWidth(28); + expandBtn->setToolTip(tr("Expand all")); + collapseBtn->setToolTip(tr("Collapse all")); + identifyBtn->setToolTip(tr("Hold to flash the selected value in the UI (magenta / toggled / 0↔10). Releases restore the original.")); + // NoFocus so clicking the button doesn't steal the tree's current item + identifyBtn->setFocusPolicy(Qt::NoFocus); + + searchEdit = new QLineEdit(this); + searchEdit->setPlaceholderText(tr("Search…")); + searchEdit->setClearButtonEnabled(true); + + auto *toolbar = new QHBoxLayout(); + toolbar->addWidget(expandBtn); + toolbar->addWidget(collapseBtn); + toolbar->addWidget(identifyBtn); + toolbar->addStretch(); + toolbar->addWidget(searchEdit); + + connect(identifyBtn, &QPushButton::pressed, this, &ThemeEditorDialog::identifyPressed); + connect(identifyBtn, &QPushButton::released, this, &ThemeEditorDialog::identifyReleased); + + // --- meta section --- + idLabel = new QLabel(this); + idLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + nameEdit = new QLineEdit(this); + variantCombo = new QComboBox(this); + variantCombo->addItem(tr("Light"), "light"); + variantCombo->addItem(tr("Dark"), "dark"); + + auto *metaForm = new QFormLayout(); + metaForm->addRow(tr("ID:"), idLabel); + metaForm->addRow(tr("Display name:"), nameEdit); + metaForm->addRow(tr("Variant:"), variantCombo); + + auto *metaBox = new QGroupBox(tr("Theme info"), this); + metaBox->setLayout(metaForm); + + syncMetaFromParams(); + + connect(nameEdit, &QLineEdit::textEdited, this, [this](const QString &text) { + auto meta = this->params["meta"].toObject(); + meta["displayName"] = text; + this->params["meta"] = meta; + emit themeJsonChanged(this->params); + }); + connect(variantCombo, &QComboBox::currentIndexChanged, this, [this](int index) { + auto meta = this->params["meta"].toObject(); + meta["variant"] = variantCombo->itemData(index).toString(); + this->params["meta"] = meta; + emit themeJsonChanged(this->params); + }); + + // --- tree --- + tree = new QTreeWidget(this); + tree->setColumnCount(2); + tree->setHeaderLabels({ tr("Parameter"), tr("Value") }); + tree->header()->setSectionResizeMode(0, QHeaderView::Stretch); + tree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + tree->setRootIsDecorated(true); + tree->setUniformRowHeights(true); + tree->setAlternatingRowColors(true); + + populate(nullptr, params, {}); + tree->expandAll(); + + connect(expandBtn, &QPushButton::clicked, tree, &QTreeWidget::expandAll); + connect(collapseBtn, &QPushButton::clicked, tree, &QTreeWidget::collapseAll); + connect(searchEdit, &QLineEdit::textChanged, this, &ThemeEditorDialog::filterTree); + + connect(tree, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem *item, int) { + if (item->data(0, IsColorRole).toBool()) + editColorItem(item); + else if (item->data(0, IsBoolRole).toBool()) + toggleBoolItem(item); + else if (item->data(0, IsNumberRole).toBool()) + editNumberItem(item); + }); + + // --- bottom buttons --- + auto *saveBtn = new QPushButton(tr("Save to file..."), this); + auto *loadBtn = new QPushButton(tr("Load from file..."), this); + auto *closeBtn = new QPushButton(tr("Close"), this); + connect(saveBtn, &QPushButton::clicked, this, &ThemeEditorDialog::saveToFile); + connect(loadBtn, &QPushButton::clicked, this, &ThemeEditorDialog::loadFromFile); + connect(closeBtn, &QPushButton::clicked, this, &QDialog::close); + auto *buttons = new QHBoxLayout(); + buttons->addWidget(saveBtn); + buttons->addWidget(loadBtn); + buttons->addStretch(); + buttons->addWidget(closeBtn); + + auto *layout = new QVBoxLayout(this); + layout->addLayout(toolbar); + layout->addWidget(metaBox); + layout->addWidget(tree); + layout->addLayout(buttons); + + setLayout(layout); +} + +void ThemeEditorDialog::populate(QTreeWidgetItem *parent, const QJsonObject &obj, const QStringList &path) +{ + for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) { + const QString key = it.key(); + + // "meta" is handled by the dedicated UI above the tree + if (path.isEmpty() && key == "meta") + continue; + const QJsonValue val = it.value(); + const QStringList childPath = path + QStringList(key); + + if (val.isObject()) { + // Group row + QTreeWidgetItem *group = parent ? new QTreeWidgetItem(parent) + : new QTreeWidgetItem(tree); + QFont bold = group->font(0); + bold.setBold(true); + group->setFont(0, bold); + group->setText(0, key); + group->setFlags(group->flags() & ~Qt::ItemIsSelectable); + populate(group, val.toObject(), childPath); + } else { + // Leaf row + QTreeWidgetItem *item = parent ? new QTreeWidgetItem(parent) + : new QTreeWidgetItem(tree); + item->setText(0, key); + item->setData(0, PathRole, childPath); + + const QString strVal = val.toString(); + if (val.isString() && isColorString(strVal)) { + const QColor color(strVal); + item->setIcon(1, colorIcon(color)); + item->setText(1, strVal); + item->setData(0, IsColorRole, true); + item->setToolTip(1, tr("Double-click to edit color")); + } else if (val.isBool()) { + item->setText(1, val.toBool() ? tr("true") : tr("false")); + item->setData(0, IsColorRole, false); + item->setData(0, IsBoolRole, true); + item->setToolTip(1, tr("Double-click to toggle")); + } else if (val.isDouble()) { + item->setText(1, QString::number(val.toDouble())); + item->setData(0, IsNumberRole, true); + item->setToolTip(1, tr("Double-click to edit value")); + } else { + item->setText(1, strVal); + item->setData(0, IsColorRole, false); + } + } + } +} + +void ThemeEditorDialog::editColorItem(QTreeWidgetItem *item) +{ + const QColor current(item->text(1)); + QColorDialog dialog(current, this); + dialog.setOption(QColorDialog::ShowAlphaChannel, true); + dialog.setWindowTitle(tr("Edit: %1").arg(item->text(0))); + + // Live update as user drags the picker + connect(&dialog, &QColorDialog::currentColorChanged, this, [this, item](const QColor &color) { + applyColorToItem(item, color); + emit themeJsonChanged(params); + }); + + if (dialog.exec() == QDialog::Accepted) { + applyColorToItem(item, dialog.selectedColor()); + } else { + // Revert to original if cancelled + applyColorToItem(item, current); + } + emit themeJsonChanged(params); +} + +void ThemeEditorDialog::applyColorToItem(QTreeWidgetItem *item, const QColor &color) +{ + const QString hexStr = color.alpha() < 255 + ? color.name(QColor::HexArgb) + : color.name(QColor::HexRgb); + item->setText(1, hexStr); + item->setIcon(1, colorIcon(color)); + + const QStringList path = item->data(0, PathRole).toStringList(); + setJsonPath(params, path, hexStr); +} + +void ThemeEditorDialog::toggleBoolItem(QTreeWidgetItem *item) +{ + const bool newValue = item->text(1) != tr("true"); + item->setText(1, newValue ? tr("true") : tr("false")); + const QStringList path = item->data(0, PathRole).toStringList(); + setJsonPath(params, path, newValue); + emit themeJsonChanged(params); +} + +void ThemeEditorDialog::editNumberItem(QTreeWidgetItem *item) +{ + const double current = item->text(1).toDouble(); + // Use integer dialog when the stored value has no fractional part + const bool isInt = (current == std::floor(current)); + bool ok = false; + double newValue; + if (isInt) { + const int result = QInputDialog::getInt( + this, tr("Edit: %1").arg(item->text(0)), item->text(0), + static_cast(current), INT_MIN, INT_MAX, 1, &ok); + newValue = result; + } else { + newValue = QInputDialog::getDouble( + this, tr("Edit: %1").arg(item->text(0)), item->text(0), + current, -1e9, 1e9, 4, &ok); + } + if (!ok) + return; + item->setText(1, isInt ? QString::number(static_cast(newValue)) : QString::number(newValue)); + const QStringList path = item->data(0, PathRole).toStringList(); + setJsonPath(params, path, newValue); + emit themeJsonChanged(params); +} + +// Returns true if the item or any of its descendants should be visible. +static bool applyFilter(QTreeWidgetItem *item, const QString &query) +{ + if (query.isEmpty()) { + item->setHidden(false); + for (int i = 0; i < item->childCount(); ++i) + applyFilter(item->child(i), query); + return true; + } + + const bool selfMatch = item->text(0).contains(query, Qt::CaseInsensitive); + + if (item->childCount() == 0) { + // Leaf: match on key name or value text + const bool match = selfMatch || item->text(1).contains(query, Qt::CaseInsensitive); + item->setHidden(!match); + return match; + } + + // Group: if the group name itself matches, show all children + bool anyChildVisible = false; + for (int i = 0; i < item->childCount(); ++i) { + if (selfMatch) { + item->child(i)->setHidden(false); + anyChildVisible = true; + } else { + if (applyFilter(item->child(i), query)) + anyChildVisible = true; + } + } + item->setHidden(!anyChildVisible); + return anyChildVisible; +} + +void ThemeEditorDialog::filterTree(const QString &query) +{ + for (int i = 0; i < tree->topLevelItemCount(); ++i) + applyFilter(tree->topLevelItem(i), query); + + // Keep visible results expanded so they're reachable + if (!query.isEmpty()) + tree->expandAll(); +} + +QIcon ThemeEditorDialog::colorIcon(const QColor &color) +{ + const int size = qApp->style()->pixelMetric(QStyle::PM_SmallIconSize); + QPixmap pix(size, size); + pix.fill(color); + return QIcon(pix); +} + +void ThemeEditorDialog::setJsonPath(QJsonObject &root, const QStringList &path, const QJsonValue &value) +{ + if (path.isEmpty()) + return; + if (path.size() == 1) { + root[path[0]] = value; + return; + } + QJsonObject sub = root[path[0]].toObject(); + setJsonPath(sub, path.mid(1), value); + root[path[0]] = sub; +} + +void ThemeEditorDialog::syncMetaToParams() +{ + auto meta = params["meta"].toObject(); + meta["displayName"] = nameEdit->text(); + meta["variant"] = variantCombo->currentData().toString(); + params["meta"] = meta; +} + +void ThemeEditorDialog::syncMetaFromParams() +{ + const auto meta = params["meta"].toObject(); + idLabel->setText(meta["id"].toString()); + nameEdit->setText(meta["displayName"].toString()); + const QString variant = meta["variant"].toString("dark"); + variantCombo->setCurrentIndex(variant == "light" ? 0 : 1); +} + +void ThemeEditorDialog::saveToFile() +{ + // Assign a user-scoped UUID if the current id is builtin or empty + auto meta = params["meta"].toObject(); + const QString currentId = meta["id"].toString(); + if (currentId.isEmpty() || currentId.startsWith("builtin/")) { + const QString newId = "user/" + QUuid::createUuid().toString(QUuid::WithoutBraces); + meta["id"] = newId; + params["meta"] = meta; + idLabel->setText(newId); + } + + const QString path = QFileDialog::getSaveFileName( + this, tr("Save theme"), QString(), tr("JSON files (*.json);;All files (*)")); + if (path.isEmpty()) + return; + + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(this, tr("Save failed"), tr("Could not open file for writing:\n%1").arg(path)); + return; + } + file.write(QJsonDocument(params).toJson(QJsonDocument::Indented)); +} + +void ThemeEditorDialog::loadFromFile() +{ + const QString path = QFileDialog::getOpenFileName( + this, tr("Load theme"), QString(), tr("JSON files (*.json);;All files (*)")); + if (path.isEmpty()) + return; + + QFile file(path); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::warning(this, tr("Load failed"), tr("Could not open file:\n%1").arg(path)); + return; + } + + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &err); + if (doc.isNull()) { + QMessageBox::warning(this, tr("Load failed"), tr("Invalid JSON:\n%1").arg(err.errorString())); + return; + } + if (!doc.isObject()) { + QMessageBox::warning(this, tr("Load failed"), tr("Expected a JSON object.")); + return; + } + + params = doc.object(); + syncMetaFromParams(); + tree->clear(); + populate(nullptr, params, {}); + tree->expandAll(); + emit themeJsonChanged(params); +} + +void ThemeEditorDialog::identifyPressed() +{ + QTreeWidgetItem *item = tree->currentItem(); + if (!item) + return; + + const QStringList path = item->data(0, PathRole).toStringList(); + if (path.isEmpty()) + return; // group row, not a leaf + + if (item->data(0, IsColorRole).toBool()) { + identifySnapshot = QJsonValue(item->text(1)); + identifyItem = item; + identifyPath = path; + applyColorToItem(item, QColor(0xFA00FA)); + } else if (item->data(0, IsBoolRole).toBool()) { + const bool current = (item->text(1) == tr("true")); + identifySnapshot = QJsonValue(current); + identifyItem = item; + identifyPath = path; + const bool flipped = !current; + item->setText(1, flipped ? tr("true") : tr("false")); + setJsonPath(params, path, flipped); + } else if (item->data(0, IsNumberRole).toBool()) { + const double current = item->text(1).toDouble(); + identifySnapshot = QJsonValue(current); + identifyItem = item; + identifyPath = path; + const bool isInt = (current == std::floor(current)); + const double highlight = (current > 0.0) ? 0.0 : 10.0; + item->setText(1, isInt ? QString::number(static_cast(highlight)) : QString::number(highlight)); + setJsonPath(params, path, highlight); + } else { + return; // non-editable leaf (plain string), nothing to flash + } + emit themeJsonChanged(params); +} + +void ThemeEditorDialog::identifyReleased() +{ + if (!identifyItem) + return; + + QTreeWidgetItem *item = identifyItem; + const QStringList path = identifyPath; + identifyItem = nullptr; + identifyPath.clear(); + + if (item->data(0, IsColorRole).toBool()) { + applyColorToItem(item, QColor(identifySnapshot.toString())); + } else if (item->data(0, IsBoolRole).toBool()) { + const bool restored = identifySnapshot.toBool(); + item->setText(1, restored ? tr("true") : tr("false")); + setJsonPath(params, path, restored); + } else if (item->data(0, IsNumberRole).toBool()) { + const double restored = identifySnapshot.toDouble(); + const bool isInt = (restored == std::floor(restored)); + item->setText(1, isInt ? QString::number(static_cast(restored)) : QString::number(restored)); + setJsonPath(params, path, restored); + } + identifySnapshot = QJsonValue(); + emit themeJsonChanged(params); +} diff --git a/common/themes/theme_editor_dialog.h b/common/themes/theme_editor_dialog.h new file mode 100644 index 00000000..a2f5a5e2 --- /dev/null +++ b/common/themes/theme_editor_dialog.h @@ -0,0 +1,63 @@ +#ifndef THEME_EDITOR_DIALOG_H +#define THEME_EDITOR_DIALOG_H + +#include +#include + +class QComboBox; +class QLabel; +class QLineEdit; +class QTreeWidget; +class QTreeWidgetItem; + +// Generic theme parameter editor. +// Works entirely on QJsonObject — has no knowledge of app-specific ThemeParams. +// Connect to themeJsonChanged to receive live updates as the user edits colors. +class ThemeEditorDialog : public QDialog +{ + Q_OBJECT +public: + explicit ThemeEditorDialog(const QJsonObject ¶ms, QWidget *parent = nullptr); + + QJsonObject currentParams() const { return params; } + +signals: + void themeJsonChanged(const QJsonObject ¶ms); + +private: + void populate(QTreeWidgetItem *parent, const QJsonObject &obj, const QStringList &path); + void editColorItem(QTreeWidgetItem *item); + void applyColorToItem(QTreeWidgetItem *item, const QColor &color); + void toggleBoolItem(QTreeWidgetItem *item); + void editNumberItem(QTreeWidgetItem *item); + void filterTree(const QString &query); + void saveToFile(); + void loadFromFile(); + + // Identify feature: hold the (i) button to temporarily flash the UI element + // that uses the current item's value. The original value is restored on release. + void identifyPressed(); + void identifyReleased(); + + static QIcon colorIcon(const QColor &color); + static void setJsonPath(QJsonObject &root, const QStringList &path, const QJsonValue &value); + + QTreeWidget *tree; + QLineEdit *searchEdit; + QJsonObject params; + + // Meta UI + QLabel *idLabel; + QLineEdit *nameEdit; + QComboBox *variantCombo; + + void syncMetaToParams(); + void syncMetaFromParams(); + + // Identify state (null item = inactive) + QTreeWidgetItem *identifyItem = nullptr; + QStringList identifyPath; + QJsonValue identifySnapshot; // original value saved on press, restored on release +}; + +#endif // THEME_EDITOR_DIALOG_H diff --git a/common/themes/theme_id.h b/common/themes/theme_id.h deleted file mode 100644 index f42c3069..00000000 --- a/common/themes/theme_id.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef THEME_ID_H -#define THEME_ID_H - -enum class ThemeId { - Classic, - Light, - Dark, -}; - -#endif // THEME_ID_H diff --git a/common/themes/theme_manager.cpp b/common/themes/theme_manager.cpp index 094000c2..589b201e 100644 --- a/common/themes/theme_manager.cpp +++ b/common/themes/theme_manager.cpp @@ -1,14 +1,13 @@ #include "theme_manager.h" +#include "appearance_configuration.h" #include "theme.h" #include "theme_factory.h" +#include "theme_repository.h" #include -#include #include -// TODO: add API to force color scheme //styleHints->setColorScheme(Qt::ColorScheme::Dark); - ThemeManager::ThemeManager() { } @@ -19,36 +18,106 @@ ThemeManager &ThemeManager::instance() return instance; } -void ThemeManager::initialize() +void ThemeManager::initialize(AppearanceConfiguration *config, ThemeRepository *repository) { + this->config = config; + this->repository = repository; + auto *styleHints = qGuiApp->styleHints(); - auto colorScheme = styleHints->colorScheme(); + // Re-resolve when OS color scheme changes (relevant for FollowSystem mode) + connect(styleHints, &QStyleHints::colorSchemeChanged, this, &ThemeManager::resolveTheme, Qt::QueuedConnection); - // TODO: settings are needed to decide what theme to use - auto applyColorScheme = [this](Qt::ColorScheme scheme) { - setTheme(scheme == Qt::ColorScheme::Dark ? ThemeId::Dark : ThemeId::Light); - }; + // Re-resolve when the user changes any theme setting + connect(config, &AppearanceConfiguration::selectionChanged, this, &ThemeManager::resolveTheme); - applyColorScheme(colorScheme); - - connect(styleHints, &QStyleHints::colorSchemeChanged, this, applyColorScheme, Qt::QueuedConnection); + resolveTheme(); } -void ThemeManager::setTheme(ThemeId themeId) +void ThemeManager::setTheme(const Theme &theme) { - if (this->themeId == themeId) { - return; - } - - this->themeId = themeId; - - updateCurrentTheme(); - + currentTheme = theme; emit themeChanged(); } -void ThemeManager::updateCurrentTheme() +Theme ThemeManager::themeFromId(const QString &id, ThemeVariant fallbackVariant) { - currentTheme = makeTheme(themeId); + // Try the repository first (handles both builtin and user themes via JSON) + if (repository && repository->contains(id)) { + QJsonObject json = repository->loadThemeJson(id); + if (!json.isEmpty()) + return makeTheme(json); + } + + // Fallback to the builtin that matches the current dark/light intent. + const QString fallbackId = (fallbackVariant == ThemeVariant::Dark) + ? QStringLiteral("builtin/dark") + : QStringLiteral("builtin/light"); + if (repository && repository->contains(fallbackId)) { + QJsonObject json = repository->loadThemeJson(fallbackId); + if (!json.isEmpty()) + return makeTheme(json); + } + + return {}; +} + +void ThemeManager::resolveTheme() +{ + if (!config) + return; + + const auto &sel = config->selection(); + + QString id; + ThemeVariant fallbackVariant; + switch (sel.mode) { + case ThemeMode::FollowSystem: { + const bool isDark = (qGuiApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark); + id = isDark ? sel.darkThemeId : sel.lightThemeId; + fallbackVariant = isDark ? ThemeVariant::Dark : ThemeVariant::Light; + break; + } + case ThemeMode::Light: + id = sel.lightThemeId; + fallbackVariant = ThemeVariant::Light; + break; + case ThemeMode::Dark: + id = sel.darkThemeId; + fallbackVariant = ThemeVariant::Dark; + break; + case ThemeMode::ForcedTheme: + id = sel.fixedThemeId; + fallbackVariant = (qGuiApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark) + ? ThemeVariant::Dark + : ThemeVariant::Light; + break; + } + + const Theme theme = themeFromId(id, fallbackVariant); + + // Sync Qt's application-level color scheme so native widgets (menus, scrollbars, + // standard dialogs) use the correct palette before themeChanged() is emitted. +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + Qt::ColorScheme scheme; + switch (sel.mode) { + case ThemeMode::FollowSystem: + scheme = Qt::ColorScheme::Unknown; // delegate to OS + break; + case ThemeMode::Light: + scheme = Qt::ColorScheme::Light; + break; + case ThemeMode::Dark: + scheme = Qt::ColorScheme::Dark; + break; + case ThemeMode::ForcedTheme: + scheme = (theme.meta.variant == ThemeVariant::Dark) + ? Qt::ColorScheme::Dark + : Qt::ColorScheme::Light; + break; + } + qGuiApp->styleHints()->setColorScheme(scheme); +#endif + + setTheme(theme); } diff --git a/common/themes/theme_manager.h b/common/themes/theme_manager.h index 8bac2dad..42b8d0d0 100644 --- a/common/themes/theme_manager.h +++ b/common/themes/theme_manager.h @@ -1,11 +1,13 @@ #ifndef THEME_MANAGER_H #define THEME_MANAGER_H +#include "appearance_configuration.h" #include "theme.h" -#include "theme_id.h" #include +class ThemeRepository; + class ThemeManager : public QObject { Q_OBJECT @@ -17,21 +19,27 @@ public: ThemeManager(ThemeManager &&) = delete; ThemeManager &operator=(ThemeManager &&) = delete; - void initialize(); - - void setTheme(ThemeId themeId); + void initialize(AppearanceConfiguration *config, ThemeRepository *repository); + void setTheme(const Theme &theme); const Theme &getCurrentTheme() const { return currentTheme; } + AppearanceConfiguration *getAppearanceConfiguration() const { return config; } + signals: void themeChanged(); private: explicit ThemeManager(); - ThemeId themeId = ThemeId::Classic; + + AppearanceConfiguration *config = nullptr; + ThemeRepository *repository = nullptr; Theme currentTheme; - void updateCurrentTheme(); + Theme themeFromId(const QString &id, ThemeVariant fallbackVariant); + +private slots: + void resolveTheme(); }; #endif // THEME_MANAGER_H diff --git a/common/themes/theme_meta.h b/common/themes/theme_meta.h new file mode 100644 index 00000000..e3b9c6d8 --- /dev/null +++ b/common/themes/theme_meta.h @@ -0,0 +1,14 @@ +#ifndef THEME_META_H +#define THEME_META_H + +#include "theme_variant.h" + +#include + +struct ThemeMeta { + QString id; + QString displayName; + ThemeVariant variant; +}; + +#endif // THEME_META_H diff --git a/common/themes/theme_repository.cpp b/common/themes/theme_repository.cpp new file mode 100644 index 00000000..3290cd63 --- /dev/null +++ b/common/themes/theme_repository.cpp @@ -0,0 +1,206 @@ +#include "theme_repository.h" + +#include +#include +#include +#include +#include + +ThemeRepository::ThemeRepository(const QString &qrcPrefix, const QString &userThemesDir) + : qrcPrefix(qrcPrefix), userThemesDir(userThemesDir) +{ + scanBuiltins(); + scanUserThemes(); +} + +QList ThemeRepository::availableThemes() const +{ + QList result; + result.reserve(builtins.size() + userThemes.size()); + + for (const auto &b : builtins) + result.append({ b.meta.id, b.meta.displayName, b.meta.variant, true }); + + for (const auto &u : userThemes) + result.append({ u.meta.id, u.meta.displayName, u.meta.variant, false }); + + return result; +} + +bool ThemeRepository::contains(const QString &themeId) const +{ + for (const auto &b : builtins) + if (b.id == themeId) + return true; + + for (const auto &u : userThemes) + if (u.id == themeId) + return true; + + return false; +} + +QJsonObject ThemeRepository::loadThemeJson(const QString &themeId) const +{ + for (const auto &b : builtins) + if (b.id == themeId) + return readJsonFile(b.resourcePath); + + for (const auto &u : userThemes) + if (u.id == themeId) + return readJsonFile(u.filePath); + + return {}; +} + +QString ThemeRepository::saveUserTheme(QJsonObject themeJson) +{ + QDir().mkpath(userThemesDir); + + auto metaObj = themeJson["meta"].toObject(); + QString id = metaObj["id"].toString(); + + if (id.isEmpty() || id.startsWith("builtin/")) { + const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces); + id = "user/" + uuid; + metaObj["id"] = id; + themeJson["meta"] = metaObj; + } + + // Extract uuid from "user/" + const QString uuid = id.mid(5); // skip "user/" + const QString filePath = filePathForUserTheme(uuid); + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly)) { + file.write(QJsonDocument(themeJson).toJson(QJsonDocument::Indented)); + file.close(); + } + + // Update cache + refresh(); + + return id; +} + +bool ThemeRepository::deleteUserTheme(const QString &themeId) +{ + if (themeId.startsWith("builtin/")) + return false; + + for (const auto &u : userThemes) { + if (u.id == themeId) { + const bool removed = QFile::remove(u.filePath); + if (removed) + refresh(); + return removed; + } + } + + return false; +} + +QString ThemeRepository::importThemeFromFile(const QString &filePath) +{ + QJsonObject json = readJsonFile(filePath); + if (json.isEmpty()) + return {}; + + // Force a new user id regardless of what the file contains + auto metaObj = json["meta"].toObject(); + const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces); + const QString id = "user/" + uuid; + metaObj["id"] = id; + json["meta"] = metaObj; + + return saveUserTheme(json); +} + +void ThemeRepository::refresh() +{ + scanUserThemes(); +} + +// --- Private helpers --- + +void ThemeRepository::scanBuiltins() +{ + builtins.clear(); + + static const QStringList builtinNames = { "classic", "light", "dark" }; + + for (const auto &name : builtinNames) { + const QString resourcePath = qrcPrefix + "/builtin_" + name + ".json"; + const QJsonObject json = readJsonFile(resourcePath); + if (json.isEmpty()) + continue; + + BuiltinEntry entry; + entry.id = "builtin/" + name; + entry.resourcePath = resourcePath; + entry.meta = extractMeta(json); + // Ensure the id matches the canonical form + entry.meta.id = entry.id; + builtins.append(entry); + } +} + +void ThemeRepository::scanUserThemes() +{ + userThemes.clear(); + + QDir dir(userThemesDir); + if (!dir.exists()) + return; + + const auto entries = dir.entryList({ "*.json" }, QDir::Files); + for (const auto &fileName : entries) { + const QString filePath = dir.absoluteFilePath(fileName); + const QJsonObject json = readJsonFile(filePath); + if (json.isEmpty()) + continue; + + ThemeMeta meta = extractMeta(json); + if (meta.id.isEmpty()) { + // Derive id from filename (strip .json extension) + const QString baseName = fileName.chopped(5); // remove ".json" + meta.id = "user/" + baseName; + } + + UserEntry entry; + entry.id = meta.id; + entry.filePath = filePath; + entry.meta = meta; + userThemes.append(entry); + } +} + +ThemeMeta ThemeRepository::extractMeta(const QJsonObject &json) +{ + const auto meta = json["meta"].toObject(); + return ThemeMeta { + meta["id"].toString(), + meta["displayName"].toString(), + (meta["variant"].toString() == "light") ? ThemeVariant::Light : ThemeVariant::Dark + }; +} + +QJsonObject ThemeRepository::readJsonFile(const QString &path) +{ + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) + return {}; + + const QByteArray data = file.readAll(); + QJsonParseError error; + const QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) + return {}; + + return doc.object(); +} + +QString ThemeRepository::filePathForUserTheme(const QString &uuid) const +{ + return userThemesDir + "/" + uuid + ".json"; +} diff --git a/common/themes/theme_repository.h b/common/themes/theme_repository.h new file mode 100644 index 00000000..e9ec4a85 --- /dev/null +++ b/common/themes/theme_repository.h @@ -0,0 +1,57 @@ +#ifndef THEME_REPOSITORY_H +#define THEME_REPOSITORY_H + +#include "theme_meta.h" + +#include +#include +#include + +struct ThemeListEntry { + QString id; + QString displayName; + ThemeVariant variant; + bool isBuiltin; +}; + +class ThemeRepository +{ +public: + explicit ThemeRepository(const QString &qrcPrefix, const QString &userThemesDir); + + QList availableThemes() const; + bool contains(const QString &themeId) const; + QJsonObject loadThemeJson(const QString &themeId) const; + + QString saveUserTheme(QJsonObject themeJson); + bool deleteUserTheme(const QString &themeId); + QString importThemeFromFile(const QString &filePath); + + void refresh(); + +private: + QString qrcPrefix; + QString userThemesDir; + + struct BuiltinEntry { + QString id; + QString resourcePath; + ThemeMeta meta; + }; + QList builtins; + + struct UserEntry { + QString id; + QString filePath; + ThemeMeta meta; + }; + QList userThemes; + + void scanBuiltins(); + void scanUserThemes(); + static ThemeMeta extractMeta(const QJsonObject &json); + static QJsonObject readJsonFile(const QString &path); + QString filePathForUserTheme(const QString &uuid) const; +}; + +#endif // THEME_REPOSITORY_H diff --git a/common/themes/theme_variant.h b/common/themes/theme_variant.h new file mode 100644 index 00000000..adff0327 --- /dev/null +++ b/common/themes/theme_variant.h @@ -0,0 +1,9 @@ +#ifndef THEME_VARIANT_H +#define THEME_VARIANT_H + +enum class ThemeVariant { + Light, + Dark, +}; + +#endif // THEME_VARIANT_H diff --git a/images/appearance_config/theme-mode-dark.svg b/images/appearance_config/theme-mode-dark.svg new file mode 100644 index 00000000..a30277b9 --- /dev/null +++ b/images/appearance_config/theme-mode-dark.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/appearance_config/theme-mode-light.svg b/images/appearance_config/theme-mode-light.svg new file mode 100644 index 00000000..401b54c8 --- /dev/null +++ b/images/appearance_config/theme-mode-light.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/appearance_config/theme-mode-system.svg b/images/appearance_config/theme-mode-system.svg new file mode 100644 index 00000000..62fc4569 --- /dev/null +++ b/images/appearance_config/theme-mode-system.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file