/** * GoingsOn - Theme Management * Loads themes from shared TOML files via Tauri commands */ (function() { 'use strict'; const { invoke } = window.__TAURI__.core; // Theme state lives in GoingsOn.state (centralized) GoingsOn.state.set('currentThemeId', 'goingson'); 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 resolved intent tokens, using cache if available. * @param {string} themeId - Theme ID to fetch * @returns {Promise} Theme object with `intents`, 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 a theme's resolved intent tokens as CSS custom properties on the * document root. Each intent key becomes `--{key}` (e.g. `surface-page` -> * `--surface-page`); styles.css aliases the app's brand vocabulary over them. * @param {Object} intents - Map of intent token name -> color value */ function applyColors(intents) { const root = document.documentElement; for (const [token, value] of Object.entries(intents)) { root.style.setProperty(`--${token}`, value); } // Sync the iOS / browser chrome color with the active theme's page // background so the status bar matches. const themeMeta = document.getElementById('meta-theme-color'); if (themeMeta && intents['surface-page']) { themeMeta.setAttribute('content', intents['surface-page']); } } /** * 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 goingson`); if (themeId !== 'goingson') { return loadTheme('goingson'); } return; } applyColors(theme.intents); 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('goingson'); } 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, }; })();