/** * GoingsOn - Theme Management * Loads themes from shared TOML files via Tauri commands */ (function() { 'use strict'; const { invoke } = window.__TAURI__.core; // CSS variable mapping: TOML dot-path -> CSS custom property const COLOR_MAP = { 'background.primary': '--bg-primary', 'background.secondary': '--bg-secondary', 'background.tertiary': '--bg-tertiary', 'background.surface': '--bg-card', 'foreground.primary': '--text-primary', 'foreground.secondary': '--text-secondary', 'foreground.muted': '--text-muted', 'accent.red': '--accent-red', 'accent.green': '--accent-green', 'accent.blue': '--accent-blue', 'accent.yellow': '--accent-yellow', 'accent.purple': '--accent-purple', 'accent.cyan': '--accent-cyan', 'border.default': '--border-color', }; // Theme state lives in GoingsOn.state (centralized) GoingsOn.state.set('currentThemeId', 'neobrute'); GoingsOn.state.set('themeCache', {}); GoingsOn.state.set('themeList', []); // ============ Theme Functions ============ /** * Fetch and cache theme list from backend. * @returns {Promise>} Array of theme metadata objects */ async function fetchThemeList() { const cached = GoingsOn.state.themeList; if (cached.length > 0) return cached; try { const list = await invoke('list_themes'); GoingsOn.state.set('themeList', list); return list; } catch (e) { console.error('Failed to list themes:', e); GoingsOn.state.set('themeList', []); return []; } } /** * Fetch a single theme's colors, using cache if available. * @param {string} themeId - Theme ID to fetch * @returns {Promise} Theme object with colors, or null on error */ async function fetchTheme(themeId) { const cache = GoingsOn.state.themeCache; if (cache[themeId]) return cache[themeId]; try { const theme = await invoke('get_theme', { id: themeId }); cache[themeId] = theme; GoingsOn.state.set('themeCache', cache); return theme; } catch (e) { console.error(`Failed to load theme ${themeId}:`, e); return null; } } /** * Apply theme colors to CSS custom properties on the document root. * @param {Object} colors - Map of TOML dot-paths to color values */ function applyColors(colors) { const root = document.documentElement; for (const [tomlKey, cssVar] of Object.entries(COLOR_MAP)) { if (colors[tomlKey]) { root.style.setProperty(cssVar, colors[tomlKey]); } } // Phase 7 Tier 3 #13 — sync the iOS / browser chrome color with the // active theme's primary background so the status bar matches. const themeMeta = document.getElementById('meta-theme-color'); if (themeMeta && colors['background.primary']) { themeMeta.setAttribute('content', colors['background.primary']); } } /** * Load and apply a theme by ID, saving the selection to localStorage. * @param {string} themeId - Theme ID to load */ async function loadTheme(themeId) { const theme = await fetchTheme(themeId); if (!theme) { console.warn(`Theme not found: ${themeId}, falling back to neobrute`); if (themeId !== 'neobrute') { return loadTheme('neobrute'); } return; } applyColors(theme.colors); GoingsOn.state.set('currentThemeId', themeId); localStorage.setItem('goingson-theme', themeId); // Update the theme selector if it exists const selector = document.getElementById('theme-selector'); if (selector) { selector.value = themeId; } } /** * Get the user's system theme preference. * @returns {string} 'dark' or 'light' */ function getSystemThemePreference() { if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { return 'dark'; } return 'light'; } /** * Load theme from localStorage or use system preference */ async function loadThemeFromStorage() { // Pre-fetch the theme list so it's cached for the settings UI await fetchThemeList(); const savedTheme = localStorage.getItem('goingson-theme'); if (savedTheme === 'system') { await applySystemTheme(); } else if (savedTheme) { await loadTheme(savedTheme); } else { await applySystemTheme(); } } /** * Apply theme based on system preference */ async function applySystemTheme() { const preference = getSystemThemePreference(); if (preference === 'dark') { await loadTheme('catppuccin-mocha'); } else { await loadTheme('neobrute'); } localStorage.setItem('goingson-theme', 'system'); } /** * Handle theme selector change. * @param {string} value - Selected theme ID or 'system' */ async function onThemeChange(value) { if (value === 'system') { await applySystemTheme(); } else { await loadTheme(value); } } /** * Get themes grouped by type (for settings UI). * @returns {Promise<{light: Object[], dark: Object[], highContrast: Object[]}>} */ async function getThemesByType() { const list = await fetchThemeList(); const light = []; const dark = []; const highContrast = []; for (const t of list) { if (t.variant === 'high-contrast') { highContrast.push(t); } else if (t.variant === 'light') { light.push(t); } else { dark.push(t); } } return { light, dark, highContrast }; } /** * Get current theme ID. * @returns {string} Active theme ID */ function getCurrentThemeId() { return GoingsOn.state.currentThemeId; } // Listen for system theme changes if (window.matchMedia) { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (localStorage.getItem('goingson-theme') === 'system') { applySystemTheme(); } }); } // ============ Import / Export ============ /** * Import a custom theme TOML file via native file dialog. */ async function importTheme() { try { const { open } = window.__TAURI__.dialog; const path = await open({ filters: [{ name: 'Theme', extensions: ['toml'] }], multiple: false, }); if (!path) return; const meta = await invoke('import_theme', { path }); // Clear caches so new theme is discoverable GoingsOn.state.set('themeCache', {}); GoingsOn.state.set('themeList', []); GoingsOn.ui.showToast(`Imported "${meta.name}"`); await loadTheme(meta.id); } catch (e) { GoingsOn.ui.showToast('Import failed: ' + (e.message || e), 'error'); } } /** * Export the current theme TOML file via native save dialog. */ async function exportTheme() { const currentId = GoingsOn.state.currentThemeId; if (!currentId || currentId === 'system') return; try { const { save } = window.__TAURI__.dialog; const path = await save({ defaultPath: currentId + '.toml', filters: [{ name: 'Theme', extensions: ['toml'] }], }); if (!path) return; await invoke('export_theme', { id: currentId, path }); GoingsOn.ui.showToast('Theme exported'); } catch (e) { GoingsOn.ui.showToast('Export failed: ' + (e.message || e), 'error'); } } // ============ Populate GoingsOn.themes Namespace ============ GoingsOn.themes = { load: loadTheme, loadFromStorage: loadThemeFromStorage, applySystem: applySystemTheme, onChange: onThemeChange, getByType: getThemesByType, getCurrentId: getCurrentThemeId, getSystemPreference: getSystemThemePreference, importTheme, exportTheme, }; })();