diff --git a/src/autometabuilder/web/server.py b/src/autometabuilder/web/server.py index 301e132..1ea3cbe 100644 --- a/src/autometabuilder/web/server.py +++ b/src/autometabuilder/web/server.py @@ -369,6 +369,11 @@ async def update_settings(request: Request, username: str = Depends(get_current_ return RedirectResponse(url="/", status_code=303) +@app.get("/api/ui-context", response_class=JSONResponse) +async def get_ui_context(username: str = Depends(get_current_user)): + ui_messages, ui_lang = get_ui_messages() + return {"lang": ui_lang, "messages": ui_messages} + @app.get("/api/status") async def get_status(username: str = Depends(get_current_user)): return { diff --git a/src/autometabuilder/web/static/js/app_context.js b/src/autometabuilder/web/static/js/app_context.js index 6908fdd..f7b3acc 100644 --- a/src/autometabuilder/web/static/js/app_context.js +++ b/src/autometabuilder/web/static/js/app_context.js @@ -2,7 +2,7 @@ * AutoMetabuilder - Shared Context */ (() => { - const translations = window.AMB_I18N || {}; + let translations = {}; const t = (key, fallback = '') => translations[key] || fallback || key; const format = (text, values = {}) => text.replace(/\{(\w+)\}/g, (_, name) => values[name] ?? ''); const authHeaders = (() => { @@ -13,9 +13,31 @@ return { Authorization: `Basic ${token}` }; })(); - window.AMBContext = { + const context = { t, format, - authHeaders + authHeaders, + lang: 'en', + ready: null }; + + const loadContext = async () => { + try { + const response = await fetch('/api/ui-context', { + credentials: 'include', + headers: authHeaders || {} + }); + if (!response.ok) { + throw new Error(`UI context fetch failed: ${response.status}`); + } + const data = await response.json(); + translations = data.messages || {}; + context.lang = data.lang || 'en'; + } catch (error) { + console.error('Failed to load UI context', error); + } + }; + + context.ready = loadContext(); + window.AMBContext = context; })(); diff --git a/src/autometabuilder/web/static/js/plugin_registry.js b/src/autometabuilder/web/static/js/plugin_registry.js index 459e984..cfa250b 100644 --- a/src/autometabuilder/web/static/js/plugin_registry.js +++ b/src/autometabuilder/web/static/js/plugin_registry.js @@ -10,6 +10,9 @@ }; const initAll = async () => { + if (window.AMBContext?.ready) { + await window.AMBContext.ready; + } for (const plugin of plugins) { try { await plugin.init(); diff --git a/src/autometabuilder/web/static/js/plugins/navigation_manager.js b/src/autometabuilder/web/static/js/plugins/navigation_manager.js new file mode 100644 index 0000000..93b2f08 --- /dev/null +++ b/src/autometabuilder/web/static/js/plugins/navigation_manager.js @@ -0,0 +1,73 @@ +/** + * AutoMetabuilder - Navigation Manager + */ +(() => { + const NavigationManager = { + _popstateBound: false, + + init() { + this.bindLinks(); + this.activateFromHash(false); + if (!this._popstateBound) { + window.addEventListener('popstate', () => { + this.activateFromHash(false); + }); + this._popstateBound = true; + } + }, + + bindLinks() { + document.querySelectorAll('[data-section]').forEach(link => { + if (link.dataset.navBound === 'true') return; + link.dataset.navBound = 'true'; + link.addEventListener('click', event => { + event.preventDefault(); + this.showSection(link.dataset.section); + }); + }); + }, + + refresh() { + this.bindLinks(); + if (!this.activateFromHash(false)) { + const firstLink = document.querySelector('[data-section]'); + if (firstLink) { + this.showSection(firstLink.dataset.section, false); + } + } + }, + + activateFromHash(updateHistory) { + const hash = window.location.hash.slice(1); + if (!hash || !document.querySelector(`#${hash}`)) return false; + this.showSection(hash, updateHistory); + return true; + }, + + showSection(sectionId, updateHistory = true) { + document.querySelectorAll('.amb-section').forEach(section => { + section.classList.remove('active'); + }); + + const targetSection = document.querySelector(`#${sectionId}`); + if (targetSection) { + targetSection.classList.add('active'); + } + + document.querySelectorAll('.amb-nav-link').forEach(link => { + link.classList.remove('active'); + }); + const activeLink = document.querySelector(`[data-section="${sectionId}"]`); + if (activeLink) { + activeLink.classList.add('active'); + } + + if (updateHistory) { + history.pushState(null, '', `#${sectionId}`); + } + } + }; + + window.NavigationManager = NavigationManager; + window.AMBPlugins?.register('navigation_manager', async () => NavigationManager.init()); +})(); diff --git a/src/autometabuilder/web/static/js/plugins/theme_manager.js b/src/autometabuilder/web/static/js/plugins/theme_manager.js new file mode 100644 index 0000000..b7c6985 --- /dev/null +++ b/src/autometabuilder/web/static/js/plugins/theme_manager.js @@ -0,0 +1,45 @@ +/** + * AutoMetabuilder - Theme Manager + */ +(() => { + const ThemeManager = { + STORAGE_KEY: 'amb-theme', + + init() { + const saved = localStorage.getItem(this.STORAGE_KEY); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const theme = saved || (prefersDark ? 'dark' : 'light'); + this.setTheme(theme); + + document.querySelectorAll('[data-theme-toggle]').forEach(btn => { + btn.addEventListener('click', () => this.toggle()); + }); + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { + if (!localStorage.getItem(this.STORAGE_KEY)) { + this.setTheme(e.matches ? 'dark' : 'light'); + } + }); + }, + + setTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem(this.STORAGE_KEY, theme); + this.updateToggleIcon(theme); + }, + + toggle() { + const current = document.documentElement.getAttribute('data-theme'); + this.setTheme(current === 'dark' ? 'light' : 'dark'); + }, + + updateToggleIcon(theme) { + document.querySelectorAll('[data-theme-toggle] i').forEach(icon => { + icon.className = theme === 'dark' ? 'bi bi-moon-fill' : 'bi bi-sun-fill'; + }); + } + }; + + window.ThemeManager = ThemeManager; + window.AMBPlugins?.register('theme_manager', async () => ThemeManager.init()); +})();