From b06b8cf94c83a0a415ed7e54d10035b5fa13956f Mon Sep 17 00:00:00 2001 From: Michael Sitz Date: Thu, 20 Aug 2026 20:09:58 -0400 Subject: [PATCH] Added additional themes --- offlineu_core.py | 119 ++++++++++++++++++++++++++++++++++++++-- templates/settings.html | 4 +- 2 files changed, 117 insertions(+), 6 deletions(-) diff --git a/offlineu_core.py b/offlineu_core.py index bb7e247..68df164 100644 --- a/offlineu_core.py +++ b/offlineu_core.py @@ -58,8 +58,11 @@ VIDEO_SIZE_BOUNDS = {'video_width': (200, 4000), 'video_height': (120, 3000)} # Allowed values for every setting except accent_color (validated separately # as a hex string). Anything outside these sets is rejected/ignored on save. SETTINGS_CHOICES = { - 'theme': {'dark', 'light'}, - 'font_family': {'system', 'sans', 'serif', 'monospace'}, + 'theme': { + 'dark', 'light', 'dracula', 'tokyo_night', 'catppuccin_mocha', 'ayu_dark', + 'github_dark', 'atom_one_dark', 'houston', 'night_owl', 'dainty', 'matcha', + }, + 'font_family': {'system', 'sans', 'serif', 'monospace', 'monaspace'}, 'font_size': {'small', 'medium', 'large', 'xlarge'}, 'layout_width': {'normal', 'wide', 'full'}, 'density': {'comfortable', 'compact'}, @@ -67,6 +70,89 @@ SETTINGS_CHOICES = { 'corner_radius': {'sharp', 'rounded', 'pill'}, } +# Display names for the theme dropdown - only needed where the raw key +# (e.g. 'tokyo_night') isn't already a clean label via .capitalize(). +THEME_DISPLAY_NAMES = { + 'dark': 'Dark', 'light': 'Light', 'dracula': 'Dracula', + 'tokyo_night': 'Tokyo Night', 'catppuccin_mocha': 'Catppuccin Mocha', + 'ayu_dark': 'Ayu Dark', 'github_dark': 'GitHub Dark', + 'atom_one_dark': 'Atom One Dark', 'houston': 'Houston', + 'night_owl': 'Night Owl', 'dainty': 'Dainty', 'matcha': 'Matcha', +} + +# Full color palette per theme. 'accent' here is only the *default* accent +# offered when a theme is first selected - the accent_color setting is what +# actually drives --accent afterward, so it stays independently editable. +# Sourced from each theme's official palette (Dracula, Tokyo Night, +# Catppuccin Mocha, Ayu, GitHub Dark, Atom One Dark, Houston all verified +# against upstream repos/specs). Dainty and Matcha don't have one single +# fixed official hex set (Dainty is a theme *generator*; Matcha's exact +# source values weren't available) - built in the spirit of their +# documented look (refined/minimal, earthy green-and-gray) rather than +# claimed as an exact match. +THEME_PALETTES = { + 'dark': { + 'bg-primary': '#1a1a1a', 'bg-secondary': '#2d2d2d', 'bg-tertiary': '#3d3d3d', + 'bg-tertiary-hover': '#404040', 'text-primary': '#e0e0e0', 'text-muted': '#999999', + 'border-color': '#555555', 'accent': '#007acc', 'accent-hover': '#005a9e', + }, + 'light': { + 'bg-primary': '#f2f2f2', 'bg-secondary': '#ffffff', 'bg-tertiary': '#eeeeee', + 'bg-tertiary-hover': '#e2e2e2', 'text-primary': '#222222', 'text-muted': '#666666', + 'border-color': '#cccccc', 'accent': '#007acc', 'accent-hover': '#005a9e', + }, + 'dracula': { + 'bg-primary': '#282a36', 'bg-secondary': '#343746', 'bg-tertiary': '#44475a', + 'bg-tertiary-hover': '#4d5066', 'text-primary': '#f8f8f2', 'text-muted': '#6272a4', + 'border-color': '#44475a', 'accent': '#bd93f9', 'accent-hover': '#a672f0', + }, + 'tokyo_night': { + 'bg-primary': '#1a1b26', 'bg-secondary': '#1f2335', 'bg-tertiary': '#283457', + 'bg-tertiary-hover': '#364a73', 'text-primary': '#c0caf5', 'text-muted': '#565f89', + 'border-color': '#292e42', 'accent': '#7aa2f7', 'accent-hover': '#6183bb', + }, + 'catppuccin_mocha': { + 'bg-primary': '#1e1e2e', 'bg-secondary': '#313244', 'bg-tertiary': '#45475a', + 'bg-tertiary-hover': '#585b70', 'text-primary': '#cdd6f4', 'text-muted': '#a6adc8', + 'border-color': '#585b70', 'accent': '#cba6f7', 'accent-hover': '#b48ee0', + }, + 'ayu_dark': { + 'bg-primary': '#0a0e14', 'bg-secondary': '#131721', 'bg-tertiary': '#1f2430', + 'bg-tertiary-hover': '#272d3b', 'text-primary': '#b3b1ad', 'text-muted': '#626d7a', + 'border-color': '#1f2430', 'accent': '#e6b450', 'accent-hover': '#ffb454', + }, + 'github_dark': { + 'bg-primary': '#0d1117', 'bg-secondary': '#161b22', 'bg-tertiary': '#21262d', + 'bg-tertiary-hover': '#30363d', 'text-primary': '#e6edf3', 'text-muted': '#8b949e', + 'border-color': '#30363d', 'accent': '#2f81f7', 'accent-hover': '#1f6feb', + }, + 'atom_one_dark': { + 'bg-primary': '#282c34', 'bg-secondary': '#2c313a', 'bg-tertiary': '#3b414d', + 'bg-tertiary-hover': '#454b59', 'text-primary': '#abb2bf', 'text-muted': '#5c6370', + 'border-color': '#3b414d', 'accent': '#61afef', 'accent-hover': '#528bff', + }, + 'houston': { + 'bg-primary': '#17191e', 'bg-secondary': '#2b2d33', 'bg-tertiary': '#34363d', + 'bg-tertiary-hover': '#3d3f47', 'text-primary': '#eef0f9', 'text-muted': '#79808f', + 'border-color': '#2b2d33', 'accent': '#acafff', 'accent-hover': '#54b9ff', + }, + 'night_owl': { + 'bg-primary': '#011627', 'bg-secondary': '#001122', 'bg-tertiary': '#0b2942', + 'bg-tertiary-hover': '#123a5c', 'text-primary': '#d6deeb', 'text-muted': '#5f7e97', + 'border-color': '#102a44', 'accent': '#82aaff', 'accent-hover': '#6690e0', + }, + 'dainty': { + 'bg-primary': '#1c1c22', 'bg-secondary': '#232329', 'bg-tertiary': '#2c2c34', + 'bg-tertiary-hover': '#35353e', 'text-primary': '#e8e6f0', 'text-muted': '#8a8894', + 'border-color': '#38383f', 'accent': '#c9a0dc', 'accent-hover': '#b285c7', + }, + 'matcha': { + 'bg-primary': '#1e2320', 'bg-secondary': '#262b27', 'bg-tertiary': '#333a34', + 'bg-tertiary-hover': '#3d4539', 'text-primary': '#d6ddd2', 'text-muted': '#7d8a7c', + 'border-color': '#3d443e', 'accent': '#a3c585', 'accent-hover': '#8fb86e', + }, +} + # How each choice resolves to an actual CSS value. Keeping this server-side # (rather than duplicating the mapping in JS) means the client just applies # whatever /api/settings hands back. @@ -75,6 +161,10 @@ FONT_FAMILY_CSS = { 'sans': "Arial, Helvetica, sans-serif", 'serif': "Georgia, 'Times New Roman', serif", 'monospace': "'Courier New', Consolas, monospace", + # Monaspace is GitHub's monospace font superfamily, not a color theme - + # falls back to Consolas/monospace if the font isn't installed locally, + # same as any other web font declaration. + 'monaspace': "'Monaspace Neon', 'Monaspace Argon', 'Cascadia Code', Consolas, monospace", } FONT_SIZE_CSS = {'small': '14px', 'medium': '16px', 'large': '18px', 'xlarge': '20px'} LAYOUT_WIDTH_CSS = {'normal': '1000px', 'wide': '1600px', 'full': '100%'} @@ -113,6 +203,13 @@ def save_settings(new_settings: Dict[str, Any]) -> Dict[str, Any]: for key, value in new_settings.items(): if key == 'accent_color' and isinstance(value, str) and HEX_COLOR_RE.match(value): current[key] = value + elif key == 'theme' and value in SETTINGS_CHOICES['theme']: + current['theme'] = value + # Switching to a named preset also adopts its signature accent, + # unless this same request is also setting accent_color + # explicitly (in which case that wins). + if 'accent_color' not in new_settings: + current['accent_color'] = THEME_PALETTES[value]['accent'] elif key == 'library_path' and isinstance(value, str): value = value.strip() if value and not os.path.isdir(value): @@ -140,8 +237,17 @@ def save_settings(new_settings: Dict[str, Any]) -> Dict[str, Any]: def settings_css_vars(settings: Dict[str, Any]) -> Dict[str, str]: """Resolve a settings dict into the actual CSS custom-property values.""" + palette = THEME_PALETTES.get(settings['theme'], THEME_PALETTES['dark']) return { '--accent': settings['accent_color'], + '--accent-hover': palette['accent-hover'], + '--bg-primary': palette['bg-primary'], + '--bg-secondary': palette['bg-secondary'], + '--bg-tertiary': palette['bg-tertiary'], + '--bg-tertiary-hover': palette['bg-tertiary-hover'], + '--text-primary': palette['text-primary'], + '--text-muted': palette['text-muted'], + '--border-color': palette['border-color'], '--font-family': FONT_FAMILY_CSS[settings['font_family']], '--font-size-base': FONT_SIZE_CSS[settings['font_size']], '--container-max-width': LAYOUT_WIDTH_CSS[settings['layout_width']], @@ -737,12 +843,17 @@ def browse_library(): def settings_page(): """Render the display-settings page.""" current = load_settings() + named_presets = sorted( + (t for t in SETTINGS_CHOICES['theme'] if t not in ('dark', 'light')), + key=lambda t: THEME_DISPLAY_NAMES[t] + ) return render_template( 'settings.html', settings=current, default_library_path=LIBRARY_PATH, - theme_choices=sorted(SETTINGS_CHOICES['theme']), - font_family_choices=['system', 'sans', 'serif', 'monospace'], + theme_choices=['dark', 'light'] + named_presets, + theme_display_names=THEME_DISPLAY_NAMES, + font_family_choices=['system', 'sans', 'serif', 'monospace', 'monaspace'], font_size_choices=['small', 'medium', 'large', 'xlarge'], layout_width_choices=['normal', 'wide', 'full'], density_choices=['comfortable', 'compact'], diff --git a/templates/settings.html b/templates/settings.html index 66d5cb1..01ebcf2 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -233,7 +233,7 @@ @@ -276,7 +276,7 @@