2026-05-28 10:50:52 +08:00
|
|
|
|
"""主题管理:注册 Calman 风格深色主题 + 提供运行时切换。
|
|
|
|
|
|
|
|
|
|
|
|
主题在启动时通过 ``apply_initial_theme(root_style)`` 注入到 ttkbootstrap,
|
|
|
|
|
|
当前选择持久化到 ``settings/ui_preferences.json``。运行时调用
|
|
|
|
|
|
``toggle_theme(root_style)`` / ``set_theme(root_style, name)`` 可即时切换,
|
|
|
|
|
|
并自动重新调用 ``apply_modern_styles()`` 让自定义样式跟上新色板。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
|
|
from ttkbootstrap.style import Style, ThemeDefinition
|
|
|
|
|
|
|
|
|
|
|
|
from app.views.modern_styles import apply_modern_styles
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_PREFS_PATH = Path("settings/ui_preferences.json")
|
|
|
|
|
|
|
2026-06-04 10:36:15 +08:00
|
|
|
|
# 浅色主题:自定义轻量蓝灰色板,恢复旧版浅色观感
|
|
|
|
|
|
LIGHT_THEME = "calman_light"
|
2026-05-28 10:50:52 +08:00
|
|
|
|
# 深色主题:自定义 Calman 风格
|
|
|
|
|
|
DARK_THEME = "calman_dark"
|
|
|
|
|
|
|
2026-06-04 10:36:15 +08:00
|
|
|
|
_LEGACY_LIGHT_THEMES = {"yeti"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_CALMAN_LIGHT_COLORS = {
|
|
|
|
|
|
"primary": "#1755a6",
|
|
|
|
|
|
"secondary": "#2B6CB0",
|
|
|
|
|
|
"success": "#2F9E44",
|
|
|
|
|
|
"info": "#247BA0",
|
|
|
|
|
|
"warning": "#C98700",
|
|
|
|
|
|
"danger": "#CC3300",
|
|
|
|
|
|
"light": "#F7FAFC",
|
|
|
|
|
|
"dark": "#1F2A36",
|
|
|
|
|
|
"bg": "#F5F8FB",
|
|
|
|
|
|
"fg": "#1F2933",
|
|
|
|
|
|
"selectbg": "#2B6CB0",
|
|
|
|
|
|
"selectfg": "#FFFFFF",
|
|
|
|
|
|
"border": "#C8D4E3",
|
|
|
|
|
|
"inputfg": "#243240",
|
|
|
|
|
|
"inputbg": "#FFFFFF",
|
|
|
|
|
|
"active": "#D9E6F2",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 10:50:52 +08:00
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
2026-06-04 10:36:15 +08:00
|
|
|
|
# Calman 风格深色主题色板
|
2026-05-28 10:50:52 +08:00
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
_CALMAN_DARK_COLORS = {
|
2026-06-04 10:36:15 +08:00
|
|
|
|
# "primary": "#2A2F36",
|
|
|
|
|
|
# "secondary": "#444A51",
|
|
|
|
|
|
"primary": "#6FAFCC",
|
|
|
|
|
|
"secondary": "#AEAEAE",
|
2026-05-28 10:50:52 +08:00
|
|
|
|
"success": "#4FB960",
|
2026-06-04 10:36:15 +08:00
|
|
|
|
"info": "#6FAFCC",
|
2026-05-28 10:50:52 +08:00
|
|
|
|
"warning": "#F2A93B",
|
|
|
|
|
|
"danger": "#E0524A",
|
|
|
|
|
|
"light": "#BFC6CE", # 高亮文本
|
|
|
|
|
|
"dark": "#0D1014", # 最深背景(侧栏底色)
|
|
|
|
|
|
"bg": "#1B1F24", # 主窗口背景
|
|
|
|
|
|
"fg": "#E4E8EE", # 主文本颜色
|
|
|
|
|
|
"selectbg": "#5A6169",
|
|
|
|
|
|
"selectfg": "#E4E8EE",
|
|
|
|
|
|
"border": "#2A2F36",
|
|
|
|
|
|
"inputfg": "#E4E8EE",
|
|
|
|
|
|
"inputbg": "#24292F",
|
|
|
|
|
|
"active": "#2A2F36",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def register_themes() -> None:
|
|
|
|
|
|
"""把自定义深色主题注册到 ttkbootstrap(可重复调用,幂等)。"""
|
|
|
|
|
|
style = Style()
|
2026-06-04 10:36:15 +08:00
|
|
|
|
if LIGHT_THEME not in style.theme_names():
|
|
|
|
|
|
light_def = ThemeDefinition(
|
|
|
|
|
|
name=LIGHT_THEME,
|
|
|
|
|
|
themetype="light",
|
|
|
|
|
|
colors=_CALMAN_LIGHT_COLORS,
|
|
|
|
|
|
)
|
|
|
|
|
|
style.register_theme(light_def)
|
2026-05-28 10:50:52 +08:00
|
|
|
|
if DARK_THEME in style.theme_names():
|
|
|
|
|
|
return
|
2026-06-04 10:36:15 +08:00
|
|
|
|
dark_def = ThemeDefinition(
|
2026-05-28 10:50:52 +08:00
|
|
|
|
name=DARK_THEME,
|
|
|
|
|
|
themetype="dark",
|
|
|
|
|
|
colors=_CALMAN_DARK_COLORS,
|
|
|
|
|
|
)
|
2026-06-04 10:36:15 +08:00
|
|
|
|
style.register_theme(dark_def)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_theme_name(name: Optional[str]) -> str:
|
|
|
|
|
|
if not name or name in _LEGACY_LIGHT_THEMES:
|
|
|
|
|
|
return LIGHT_THEME
|
|
|
|
|
|
return name
|
2026-05-28 10:50:52 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
# 偏好持久化
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
def _read_prefs() -> dict:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return json.loads(_PREFS_PATH.read_text(encoding="utf-8"))
|
|
|
|
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_prefs(data: dict) -> None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
_PREFS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
_PREFS_PATH.write_text(
|
|
|
|
|
|
json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8"
|
|
|
|
|
|
)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
# 写入失败不应影响 UI
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_saved_theme() -> Optional[str]:
|
|
|
|
|
|
return _read_prefs().get("theme")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_theme(name: str) -> None:
|
|
|
|
|
|
prefs = _read_prefs()
|
|
|
|
|
|
prefs["theme"] = name
|
|
|
|
|
|
_write_prefs(prefs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
# 主题应用 / 切换
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
def apply_initial_theme() -> str:
|
|
|
|
|
|
"""启动时调用:注册主题 + 加载偏好 + 切到对应主题。
|
|
|
|
|
|
|
|
|
|
|
|
返回最终生效的主题名。
|
|
|
|
|
|
"""
|
|
|
|
|
|
register_themes()
|
2026-06-04 10:36:15 +08:00
|
|
|
|
name = _normalize_theme_name(get_saved_theme())
|
2026-05-28 10:50:52 +08:00
|
|
|
|
style = Style()
|
|
|
|
|
|
if name not in style.theme_names():
|
|
|
|
|
|
name = LIGHT_THEME
|
|
|
|
|
|
style.theme_use(name)
|
|
|
|
|
|
apply_modern_styles()
|
|
|
|
|
|
return name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_theme(name: str) -> str:
|
|
|
|
|
|
"""切换到指定主题,持久化偏好,并刷新自定义样式。"""
|
|
|
|
|
|
register_themes()
|
2026-06-04 10:36:15 +08:00
|
|
|
|
name = _normalize_theme_name(name)
|
2026-05-28 10:50:52 +08:00
|
|
|
|
style = Style()
|
|
|
|
|
|
if name not in style.theme_names():
|
|
|
|
|
|
name = LIGHT_THEME
|
|
|
|
|
|
style.theme_use(name)
|
|
|
|
|
|
apply_modern_styles()
|
|
|
|
|
|
save_theme(name)
|
|
|
|
|
|
return name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def toggle_theme() -> str:
|
|
|
|
|
|
"""在浅 / 深之间切换。返回新主题名。"""
|
|
|
|
|
|
style = Style()
|
|
|
|
|
|
current = style.theme.name
|
|
|
|
|
|
target = DARK_THEME if current != DARK_THEME else LIGHT_THEME
|
|
|
|
|
|
return set_theme(target)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_dark() -> bool:
|
|
|
|
|
|
return Style().theme.name == DARK_THEME
|