diff --git a/src/boot/AppInitializer.py b/src/boot/AppInitializer.py index 42e6be1..c90cbbf 100644 --- a/src/boot/AppInitializer.py +++ b/src/boot/AppInitializer.py @@ -20,6 +20,7 @@ from interfaces.ConfigProvider import CfgKey from managers.config.ConfigManager import instance as configInstance from managers.driver.WebDriverManager import instance as webdriverInstance from managers.log.LogManager import instance as logInstance +from managers.theme.ThemeManager import instance as themeInstance def _initializeLogManager( @@ -82,12 +83,12 @@ def _initializeAppearance( saved_custom_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "") app.setStyle(saved_style) _setActiveStyleName(saved_style) + logger = logInstance().getLogger("AppInitializer") if saved_custom_theme: try: - from managers.theme.ThemeManager import instance as themeInstance themeInstance().applyTheme(saved_custom_theme) except Exception: - pass + logger.warning("无法应用自定义主题 '%s',回退到默认外观", saved_custom_theme) _applyTheme(saved_theme) def initializeApp( diff --git a/src/gui/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py index 95375a2..705e4fd 100644 --- a/src/gui/ALSettingsWidget.py +++ b/src/gui/ALSettingsWidget.py @@ -31,7 +31,10 @@ from PySide6.QtWidgets import ( ) import managers.config.ConfigManager as ConfigManager -from managers.theme.ThemeManager import instance as themeInstance +from managers.theme.ThemeManager import ( + ThemeManager, + instance as themeInstance +) from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget from interfaces.ConfigProvider import ( @@ -50,28 +53,6 @@ def _setActiveStyleName( global _active_style_name _active_style_name = name -def _clearCustomTheme( - theme: str -): - - app : QApplication | None = QApplication.instance() - if app: - app.setStyleSheet("") - _applyTheme(theme) - -def _applyCustomTheme( - name: str, - fallback_theme: str = "system" -): - - if not name: - _clearCustomTheme(fallback_theme) - return - try: - themeInstance().applyTheme(name) - except Exception: - _clearCustomTheme(fallback_theme) - def _applyTheme( theme: str ): @@ -94,19 +75,6 @@ def _restartApp( QApplication.instance().quit() QProcess.startDetached(sys.executable, sys.argv) -def _themeToReadable( - theme: str -) -> str: - - if theme == "dark": - return "深色" - elif theme == "light": - return "浅色" - elif theme == "both": - return "所有" - else: - return "未知" - class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): settingsWidgetIsClosed = Signal() @@ -136,8 +104,8 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): self.setNavigationIcons() self.ThemeInfoLabel.setTextFormat(Qt.TextFormat.RichText) self.ThemeInfoLabel.setStyleSheet( - "border: 1px solid #ccc; " \ - "border-radius: 2px;" \ + "border: 1px solid palette(mid);"\ + "border-radius: 2px;"\ "padding: 5px;" ) @@ -256,7 +224,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): need_theme = t.get("need_theme", "both") brief = t.get("brief", "没有相关简介") self.ThemeInfoLabel.setText( - f"{name} - 适用于 {_themeToReadable(need_theme)} 主题
" + f"{name} - 适用于 {ThemeManager.themeToReadable(need_theme)} 主题
" f"作者:{author}

" f"{brief}" ) @@ -299,7 +267,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): theme, style, custom_theme = self.collectSettings() self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.STYLE, style) self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, custom_theme) - _applyCustomTheme(custom_theme, theme) + themeInstance().applyThemeOrClear(custom_theme, theme) self.syncRadioFromNeedTheme(custom_theme) theme, _, _ = self.collectSettings() self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme) @@ -391,6 +359,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): self.DarkThemeRadio.setChecked(True) else: self.SystemThemeRadio.setChecked(True) + themeInstance().clearTheme(self.__original_theme) self.updateThemeInfo() @Slot() diff --git a/src/managers/theme/ThemeManager.py b/src/managers/theme/ThemeManager.py index dcf68da..8984e4a 100644 --- a/src/managers/theme/ThemeManager.py +++ b/src/managers/theme/ThemeManager.py @@ -20,7 +20,8 @@ from managers.config.ConfigManager import instance as configInstance from utils.ThemeUtils import ( packTheme, readThemeInfo, - unpackTheme + unpackTheme, + wrapQssToAtheme ) @@ -85,14 +86,10 @@ class ThemeManager: ext = os.path.splitext(source_path)[1].lower() if ext == ".qss": name = os.path.splitext(os.path.basename(source_path))[0] - info = { - "name": name, - "author": "未知", - "need_theme": "both", - "brief": "没有相关简介" - } dest_path = os.path.join(self.__themes_dir, name + ".altheme") - packTheme(source_path, info, dest_path) + if os.path.exists(dest_path): + raise ValueError(f"主题 '{name}' 已存在") + wrapQssToAtheme(source_path, dest_path, "both") return name elif ext == ".altheme": with zipfile.ZipFile(source_path, "r") as zf: @@ -102,6 +99,8 @@ class ThemeManager: name = info.get("name", os.path.splitext(os.path.basename(source_path))[0]) safe_name = os.path.basename(name) dest_path = os.path.join(self.__themes_dir, safe_name + ".altheme") + if os.path.exists(dest_path): + raise ValueError(f"主题 '{safe_name}' 已存在") shutil.copy2(source_path, dest_path) return safe_name else: @@ -176,25 +175,108 @@ class ThemeManager: filepath = os.path.join(self.__themes_dir, name + ".altheme") if not os.path.isfile(filepath): raise FileNotFoundError(filepath) - info = readThemeInfo(filepath) - with tempfile.TemporaryDirectory() as tmpdir: - unpackTheme(filepath, tmpdir) - qss_path = os.path.join(tmpdir, "theme.qss") - if os.path.isfile(qss_path): - with open(qss_path, "r", encoding="utf-8") as fh: - qss = fh.read() - app = QApplication.instance() - if app: - app.setStyleSheet(qss) + with self.__lock: + info = readThemeInfo(filepath) + with tempfile.TemporaryDirectory() as tmpdir: + unpackTheme(filepath, tmpdir) + qss_path = os.path.join(tmpdir, "theme.qss") + if os.path.isfile(qss_path): + with open(qss_path, "r", encoding="utf-8") as fh: + qss = fh.read() + app = QApplication.instance() + if app: + app.setStyleSheet(qss) + app = QApplication.instance() + if app: + need_theme = info.get("need_theme", "both") + if need_theme == "dark": + app.styleHints().setColorScheme(Qt.ColorScheme.Dark) + elif need_theme == "light": + app.styleHints().setColorScheme(Qt.ColorScheme.Light) + self.__current_theme_name = name + + def clearTheme( + self, + theme: str + ): + """ + Clear the current QSS stylesheet and apply the given color scheme. + + Args: + theme (str): The color scheme to apply after clearing + ("light", "dark", or "system"). + """ + app = QApplication.instance() if app: - need_theme = info.get("need_theme", "both") - if need_theme == "dark": - app.styleHints().setColorScheme(Qt.ColorScheme.Dark) - elif need_theme == "light": - app.styleHints().setColorScheme(Qt.ColorScheme.Light) - with self.__lock: - self.__current_theme_name = name + app.setStyleSheet("") + self._applyColorScheme(theme) + + def applyThemeOrClear( + self, + name: str, + fallback_theme: str = "system" + ): + """ + Apply a custom theme by name, or clear to fallback if empty. + + Args: + name (str): The theme name to apply, or empty to clear. + fallback_theme (str): Color scheme to use if name is empty + or if the theme fails to apply. + """ + + if not name: + self.clearTheme(fallback_theme) + return + try: + self.applyTheme(name) + except Exception: + self.clearTheme(fallback_theme) + + def _applyColorScheme( + self, + theme: str + ): + """ + Set the Qt application color scheme. + + Args: + theme (str): "dark", "light", or any other value for system default. + """ + + app = QApplication.instance() + if not app: + return + if theme == "dark": + app.styleHints().setColorScheme(Qt.ColorScheme.Dark) + elif theme == "light": + app.styleHints().setColorScheme(Qt.ColorScheme.Light) + else: + app.styleHints().setColorScheme(Qt.ColorScheme.Unknown) + + @staticmethod + def themeToReadable( + need_theme: str + ) -> str: + """ + Convert a need_theme code to human-readable Chinese text. + + Args: + need_theme (str): "dark", "light", "both", or other. + + Returns: + str: Readable Chinese label. + """ + + if need_theme == "dark": + return "深色" + elif need_theme == "light": + return "浅色" + elif need_theme == "both": + return "所有" + else: + return "未知" def currentThemeName( self diff --git a/src/utils/ThemeUtils.py b/src/utils/ThemeUtils.py index 5946ef6..5e40598 100644 --- a/src/utils/ThemeUtils.py +++ b/src/utils/ThemeUtils.py @@ -89,7 +89,10 @@ def readThemeInfo( if "info.json" not in zf.namelist(): raise ValueError("无效的 .altheme: 缺少 info.json") with zf.open("info.json") as fh: - return json.loads(fh.read().decode("utf-8")) + info = json.loads(fh.read().decode("utf-8")) + if "name" not in info: + raise ValueError("无效的 .altheme: info.json 缺少 'name' 字段") + return info def wrapQssToAtheme(