"""Менеджер тем и масштаба приложения с сохранением в localStorage браузера.""" import flet as ft import designer as ds # --------------------------------------------------------------------------- # Темы: каждый цвет surface должен КОНТРАСТИРОВАТЬ с background, # чтобы карточки, поля и кнопки не сливались с фоном. # --------------------------------------------------------------------------- THEMES: dict[str, dict] = { "default": { "name": "ЦОПП (оранжевая)", # фон страницы "background": "#F4F6F8", # карточки / формы — светлее фона "surface": "#FFFFFF", # альтернативные блоки — чуть темнее фона "surface_alt": "#E4E8ED", }, "warm": { "name": "Тёплая", "background": "#FFF3E6", "surface": "#FFFFFF", "surface_alt": "#FFD9B3", }, "blue": { "name": "Синяя", "background": "#E3F2FD", "surface": "#FFFFFF", "surface_alt": "#B8D9F5", }, "green": { "name": "Зелёная", "background": "#EDF7E7", "surface": "#FFFFFF", "surface_alt": "#BEE3B0", }, "dark": { "name": "Тёмная", "background": "#1E2A3A", "surface": "#2D3F54", "surface_alt": "#3D5068", }, } _SCALE_KEY = "ui_scale" _THEME_KEY = "ui_theme" # Допустимый диапазон масштаба SCALE_MIN = 0.8 SCALE_MAX = 2.5 SCALE_STEP = 0.1 SCALE_DEFAULT = 1.7 # Автомасштаб по ширине экрана _MOBILE_BREAKPOINT = 600 # px: меньше — телефон, больше — ПК/планшет SCALE_MOBILE = 1.0 SCALE_DESKTOP = 2.0 def auto_scale(page: ft.Page) -> float: """Вернуть масштаб на основе ширины viewport браузера.""" try: w = page.width or 0 return SCALE_MOBILE if w < _MOBILE_BREAKPOINT else SCALE_DESKTOP except Exception: return SCALE_DEFAULT # --------------------------------------------------------------------------- # Серверное хранилище настроек по user_id. # В памяти контейнера — переживает F5 в браузере, сбрасывается при рестарте. # --------------------------------------------------------------------------- _user_prefs: dict[str, dict] = {} # user_id -> {"theme": str, "scale": float} def _get_uid(page: ft.Page) -> str: return page.session.store.get("user_id") or "" def _prefs_for(user_id: str) -> dict: return _user_prefs.get(user_id, {"theme": "default", "scale": SCALE_DEFAULT}) # --------------------------------------------------------------------------- # Публичный API # --------------------------------------------------------------------------- def apply_theme(page: ft.Page, theme_id: str | None = None) -> None: """Применить тему: обновить ds.colors и page.bgcolor.""" if theme_id is None: uid = _get_uid(page) theme_id = _prefs_for(uid)["theme"] if uid else "default" if theme_id not in THEMES: theme_id = "default" t = THEMES[theme_id] ds.colors.background = t["background"] ds.colors.surface = t["surface"] ds.colors.surface_alt = t["surface_alt"] page.bgcolor = t["background"] # кешируем в session.store для быстрого доступа внутри сессии page.session.store.set(_THEME_KEY, theme_id) def apply_scale(page: ft.Page, scale: float | None = None) -> None: """Применить масштаб: обновить ds.SCALE. Если масштаб не задан явно и у пользователя нет сохранённого — определяется автоматически по ширине экрана (мобильный / ПК). """ if scale is None: uid = _get_uid(page) if uid and uid in _user_prefs: # есть явно сохранённая настройка пользователя scale = _user_prefs[uid]["scale"] else: # первый вход — автоопределение по ширине экрана scale = auto_scale(page) scale = max(SCALE_MIN, min(SCALE_MAX, float(scale))) ds.SCALE = scale page.session.store.set(_SCALE_KEY, str(scale)) def save_preferences(page: ft.Page, theme_id: str, scale: float, user_id: str | None = None) -> None: """Сохранить тему и масштаб. Хранятся в памяти сервера по user_id.""" uid = user_id or _get_uid(page) if uid: _user_prefs[uid] = {"theme": theme_id, "scale": float(scale)} # также пишем в session.store — нужно для apply_* внутри текущей сессии page.session.store.set(_THEME_KEY, theme_id) page.session.store.set(_SCALE_KEY, str(scale)) def load_theme_id(page: ft.Page) -> str: # сначала session.store (уже применено), потом serv dict v = page.session.store.get(_THEME_KEY) if v and v in THEMES: return v uid = _get_uid(page) v2 = _prefs_for(uid)["theme"] if uid else "default" return v2 if v2 in THEMES else "default" def load_scale(page: ft.Page) -> float: v = page.session.store.get(_SCALE_KEY) try: if v is not None: return float(v) except ValueError: pass uid = _get_uid(page) return _prefs_for(uid)["scale"] if uid else SCALE_DEFAULT def theme_list() -> list[dict]: return [{"id": k, "name": v["name"]} for k, v in THEMES.items()] # --------------------------------------------------------------------------- # Устаревший алиас (оставлен для совместимости с роутером) # --------------------------------------------------------------------------- class ThemeManager: THEMES = THEMES @staticmethod def get_theme_list(): return theme_list() @staticmethod def load_theme_preference(page): return load_theme_id(page) @staticmethod def save_theme_preference(page, theme_id): save_preferences(page, theme_id, load_scale(page))