Spaces:
Running
Running
| /** | |
| * 主題管理 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; | |
| } | |