/** * 主題管理 JavaScript 模組 * 處理 localStorage 儲存、讀取和主題切換 */ class ThemeManager { constructor() { this.STORAGE_KEY = 'theme-preference'; this.LIGHT_THEME = 'light'; this.DARK_THEME = 'dark'; this.SYSTEM_PREFERENCE = 'system'; // 初始化主題 this.initTheme(); } /** * 初始化主題 - 在應用程式啟動時調用 */ initTheme() { const savedTheme = this.getSavedTheme(); const systemPreference = this.getSystemPreference(); // 決定要使用的主題 let themeToApply = this.LIGHT_THEME; // 預設亮色 if (savedTheme) { // 使用儲存的用戶偏好 themeToApply = savedTheme; } else if (systemPreference === this.DARK_THEME) { // 使用系統偏好(如果沒有儲存的選擇) themeToApply = this.DARK_THEME; } this.applyTheme(themeToApply); this.updateThemeToggleButton(themeToApply); } /** * 從 localStorage 讀取儲存的主題偏好 * @returns {string|null} 儲存的主題名稱或 null */ getSavedTheme() { try { if (typeof(Storage) !== "undefined" && localStorage) { const saved = localStorage.getItem(this.STORAGE_KEY); if (saved === this.LIGHT_THEME || saved === this.DARK_THEME) { return saved; } } } catch (e) { console.warn('無法存取 localStorage:', e); } return null; } /** * 儲存主題偏好至 localStorage * @param {string} theme - 要儲存的主題名稱 */ saveTheme(theme) { try { if (typeof(Storage) !== "undefined" && localStorage) { localStorage.setItem(this.STORAGE_KEY, theme); } } catch (e) { console.warn('無法儲存主題至 localStorage:', e); } } /** * 取得系統偏好設定(使用 CSS Media Query) * @returns {string} 'dark' 或 'light' */ getSystemPreference() { if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { return this.DARK_THEME; } return this.LIGHT_THEME; } /** * 套用主題至 DOM * @param {string} theme - 要套用的主題名稱 */ applyTheme(theme) { const validTheme = (theme === this.DARK_THEME) ? this.DARK_THEME : this.LIGHT_THEME; // 設定 data-theme 屬性至根元素 if (document.documentElement) { document.documentElement.setAttribute('data-theme', validTheme); } // 設定 body 屬性(備選方案) if (document.body) { document.body.setAttribute('data-theme', validTheme); } // 保存用戶選擇 this.saveTheme(validTheme); // 觸發主題改變事件 this.dispatchThemeChangeEvent(validTheme); } /** * 獲取當前套用的主題 * @returns {string} 當前主題名稱 */ getCurrentTheme() { const htmlTheme = document.documentElement?.getAttribute('data-theme'); if (htmlTheme === this.DARK_THEME) { return this.DARK_THEME; } return this.LIGHT_THEME; } /** * 切換主題 */ toggleTheme() { const currentTheme = this.getCurrentTheme(); const newTheme = currentTheme === this.DARK_THEME ? this.LIGHT_THEME : this.DARK_THEME; this.applyTheme(newTheme); this.updateThemeToggleButton(newTheme); // 觸發 Plotly 圖表更新事件(如果存在) this.notifyChartThemeChange(newTheme); } /** * 更新主題切換按鈕的文字和狀態 * @param {string} theme - 當前主題 */ updateThemeToggleButton(theme) { const buttons = document.querySelectorAll('.theme-toggle-btn, [data-theme-toggle]'); buttons.forEach(btn => { if (theme === this.DARK_THEME) { btn.textContent = '☀️ 亮色模式'; btn.setAttribute('aria-label', '切換至亮色模式'); } else { btn.textContent = '🌙 深色模式'; btn.setAttribute('aria-label', '切換至深色模式'); } }); } /** * 觸發主題改變事件 * @param {string} theme - 新主題名稱 */ dispatchThemeChangeEvent(theme) { const event = new CustomEvent('themechange', { detail: { theme: theme } }); window.dispatchEvent(event); } /** * 通知圖表元件主題已改變 * @param {string} theme - 新主題名稱 */ notifyChartThemeChange(theme) { // 觸發自定義事件以通知 Plotly 圖表 const event = new CustomEvent('chartthemechange', { detail: { theme: theme } }); window.dispatchEvent(event); } /** * 監聽系統主題偏好的改變 */ watchSystemPreference() { if (window.matchMedia) { const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); // 使用 addEventListener(新 API) if (darkModeQuery.addEventListener) { darkModeQuery.addEventListener('change', (e) => { // 僅在沒有用戶選擇的情況下應用系統偏好 if (!this.getSavedTheme()) { const newTheme = e.matches ? this.DARK_THEME : this.LIGHT_THEME; this.applyTheme(newTheme); } }); } } } /** * 設置主題切換按鈕的點擊事件 */ setupThemeButtons() { // 尋找所有帶有 theme-toggle-btn 類別的元素 const findAndSetupButtons = () => { const buttons = document.querySelectorAll('.theme-toggle-btn'); if (buttons.length === 0) { // 如果未找到,稍後重試(Gradio 可能還在載入) setTimeout(findAndSetupButtons, 100); return; } buttons.forEach((btn) => { // 檢查是否已經綁定(防止重複) if (btn._themeToggleSetup) { return; } btn._themeToggleSetup = true; // 綁定點擊事件 btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.toggleTheme(); }); // 鍵盤導航支援 (Enter 和 Space) btn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); this.toggleTheme(); } }); console.log('✅ 深色模式按鈕已設置:', btn); }); }; findAndSetupButtons(); } } // 在頁面加載時初始化主題管理器 function initializeThemeManager() { console.log('🌙 初始化主題管理器...'); window.themeManager = new ThemeManager(); window.themeManager.watchSystemPreference(); // 立即設置按鈕 window.themeManager.setupThemeButtons(); // 定期檢查按鈕(Gradio 可能動態添加元素) setInterval(() => { const buttons = document.querySelectorAll('.theme-toggle-btn'); buttons.forEach((btn) => { if (!btn._themeToggleSetup) { window.themeManager.setupThemeButtons(); } }); }, 500); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeThemeManager); } else { // 如果 DOM 已加載,直接初始化 initializeThemeManager(); } // 監聽 Gradio 的自定義加載事件(如果存在) if (window.gradio) { // Gradio 可能有自己的事件系統 console.log('🎵 Gradio 已檢測到'); } // 導出以便在其他模組中使用 if (typeof module !== 'undefined' && module.exports) { module.exports = ThemeManager; }