diff options
Diffstat (limited to 'modules/ext/theme.js')
| -rw-r--r-- | modules/ext/theme.js | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/modules/ext/theme.js b/modules/ext/theme.js new file mode 100644 index 0000000..9a1dab5 --- /dev/null +++ b/modules/ext/theme.js @@ -0,0 +1,172 @@ +class ThemeModule { + constructor() { + this.commands = {}; + this.terminal = null; + this.key = 'webx11_themes'; + this._previewTimer = null; + this.defaults = { + 'dark': { '--bg-color': '#000000', '--bg-secondary': '#0a0a0a', '--bg-tertiary': '#111111', '--text-color': '#00ff00', '--text-muted': '#888888', '--accent-color': '#00aa00', '--border-color': '#005500' }, + 'light': { '--bg-color': '#ffffff', '--bg-secondary': '#f5f5f5', '--bg-tertiary': '#eeeeee', '--text-color': '#222222', '--text-muted': '#888888', '--accent-color': '#0066cc', '--border-color': '#cccccc' }, + 'amber': { '--bg-color': '#1a0f00', '--bg-secondary': '#2a1a00', '--bg-tertiary': '#332200', '--text-color': '#ffaa00', '--text-muted': '#886600', '--accent-color': '#ff8800', '--border-color': '#663300' }, + 'cyber': { '--bg-color': '#000a14', '--bg-secondary': '#001a2e', '--bg-tertiary': '#002240', '--text-color': '#00ffff', '--text-muted': '#0088aa', '--accent-color': '#ff00ff', '--border-color': '#004466' }, + 'ocean': { '--bg-color': '#0a1628', '--bg-secondary': '#0f2240', '--bg-tertiary': '#142e54', '--text-color': '#88ddff', '--text-muted': '#5599bb', '--accent-color': '#00aaff', '--border-color': '#1a4470' }, + 'forest': { '--bg-color': '#0a1a0e', '--bg-secondary': '#0f2814', '--bg-tertiary': '#14361c', '--text-color': '#88dd88', '--text-muted': '#559955', '--accent-color': '#33cc33', '--border-color': '#1a4a22' }, + 'monochrome': { '--bg-color': '#0a0a0a', '--bg-secondary': '#181818', '--bg-tertiary': '#222222', '--text-color': '#cccccc', '--text-muted': '#666666', '--accent-color': '#999999', '--border-color': '#333333' }, + 'sunset': { '--bg-color': '#1a0a14', '--bg-secondary': '#2a1220', '--bg-tertiary': '#3a1a2c', '--text-color': '#ffbb99', '--text-muted': '#cc7755', '--accent-color': '#ff6644', '--border-color': '#552233' }, + 'midnight': { '--bg-color': '#050510', '--bg-secondary': '#0a0a20', '--bg-tertiary': '#101030', '--text-color': '#aaaaee', '--text-muted': '#555588', '--accent-color': '#6666ff', '--border-color': '#1a1a3a' } + }; + this.themes = this.load(); + } + requirements() { + return { met: true, missing: [] }; + } + getHelpInfo() { + return { 'theme': 'theme <set|list|new|delete|export|preview|keep> [...] — Manage and preview visual themes' }; + } + load() { + try { + const data = localStorage.getItem(this.key); + return data ? { ...this.defaults, ...JSON.parse(data) } : { ...this.defaults }; + } catch (e) { return { ...this.defaults }; } + } + save() { + const custom = {}; + Object.keys(this.themes).forEach(k => { + if (!this.defaults[k]) custom[k] = this.themes[k]; + }); + localStorage.setItem(this.key, JSON.stringify(custom)); + } + registerCommands() { + this.commands['theme'] = (args) => this.handle(args); + } + handle(args) { + if (!args[0]) return this.terminal.print('Usage: theme set <name>, theme list, theme new, theme delete <name>, theme export <name>, theme preview <name>, theme keep', 'warning'); + const sub = args[0].toLowerCase(); + if (sub === 'set') this.setTheme(args[1]); + else if (sub === 'list') this.listThemes(); + else if (sub === 'new') this.newTheme(args.slice(1)); + else if (sub === 'delete') this.deleteTheme(args[1]); + else if (sub === 'export') this.exportTheme(args[1]); + else if (sub === 'preview') this.previewTheme(args[1]); + else if (sub === 'keep') this.keepPreview(); + else this.terminal.print(`Unknown: ${sub}`, 'error'); + } + _applyVars(name) { + if (!this.themes[name]) return false; + const vars = this.themes[name]; + const root = document.documentElement; + Object.entries(vars).forEach(([key, val]) => root.style.setProperty(key, val)); + return true; + } + setTheme(name) { + if (!name) return this.terminal.print('Usage: theme set <name>', 'warning'); + if (!this.themes[name]) return this.terminal.print(`Theme '${name}' not found`, 'error'); + // Cancel any active preview + if (this._previewTimer) { + clearTimeout(this._previewTimer); + this._previewTimer = null; + } + this._applyVars(name); + localStorage.setItem('webx11_active_theme', name); + this.terminal.print(`Theme set to: ${name}`, 'success'); + } + previewTheme(name) { + if (!name) return this.terminal.print('Usage: theme preview <name>', 'warning'); + if (!this.themes[name]) return this.terminal.print(`Theme '${name}' not found`, 'error'); + // Cancel any existing preview + if (this._previewTimer) { + clearTimeout(this._previewTimer); + this._previewTimer = null; + } + const previous = localStorage.getItem('webx11_active_theme') || 'dark'; + this._applyVars(name); + this.terminal.print(`🎨 Previewing: ${name} — will revert in 5s`, 'info'); + this.terminal.print(' Type "theme keep" to keep this theme permanently', 'info'); + this._previewTimer = setTimeout(() => { + this._previewTimer = null; + this._applyVars(previous); + this.terminal.print(`⏱ Preview ended — reverted to: ${previous}`, 'info'); + }, 5000); + } + keepPreview() { + if (!this._previewTimer) return this.terminal.print('No active preview to keep', 'warning'); + clearTimeout(this._previewTimer); + this._previewTimer = null; + const root = document.documentElement; + const cssVars = ['--bg-color', '--bg-secondary', '--bg-tertiary', '--text-color', '--text-muted', '--accent-color', '--border-color']; + const currentVars = {}; + cssVars.forEach(v => { currentVars[v] = getComputedStyle(root).getPropertyValue(v).trim() || '#000'; }); + let matchName = null; + for (const [name, vars] of Object.entries(this.themes)) { + const match = cssVars.every(v => vars[v] === currentVars[v]); + if (match) { matchName = name; break; } + } + if (matchName) { + localStorage.setItem('webx11_active_theme', matchName); + this.terminal.print(`✅ Preview kept — theme saved as: ${matchName}`, 'success'); + } else { + // Custom vars — save current state as active + const active = localStorage.getItem('webx11_active_theme') || 'dark'; + this.terminal.print(`✅ Preview kept (${active} theme)`, 'success'); + } + } + listThemes() { + const active = localStorage.getItem('webx11_active_theme') || 'dark'; + this.terminal.print('=== Themes ===', 'info'); + Object.keys(this.themes).forEach(name => { + const mark = name === active ? ' ✅' : ''; + this.terminal.print(` ${name}${mark}`); + }); + this.terminal.print('Use: theme set <name> to apply', 'info'); + } + newTheme(args) { + if (args.length < 1) return this.terminal.print('Usage: theme new <name> --bg #000 --text #0f0', 'warning'); + const name = args[0].toLowerCase(); + if (this.themes[name]) return this.terminal.print(`Theme '${name}' already exists`, 'warning'); + const vars = {}; + const root = document.documentElement; + const cssVars = ['--bg-color', '--bg-secondary', '--bg-tertiary', '--text-color', '--text-muted', '--accent-color', '--border-color', '--error-color', '--warning-color', '--info-color', '--success-color']; + for (let i = 1; i < args.length; i++) { + if (args[i].startsWith('--')) { + const val = args[i + 1]; + if (val && !val.startsWith('--')) { + vars[args[i]] = val; + i++; + } + } + } + if (Object.keys(vars).length === 0) { + const preview = {}; + cssVars.forEach(v => { preview[v] = getComputedStyle(root).getPropertyValue(v).trim() || '#000'; }); + this.themes[name] = preview; + } else { + this.themes[name] = vars; + } + this.save(); + this.terminal.print(`Theme '${name}' created with ${Object.keys(this.themes[name]).length} variables`, 'success'); + } + deleteTheme(name) { + if (!name) return this.terminal.print('Usage: theme delete <name>', 'warning'); + if (this.defaults[name]) return this.terminal.print('Cannot delete built-in theme', 'warning'); + if (!this.themes[name]) return this.terminal.print(`Theme '${name}' not found`, 'error'); + delete this.themes[name]; + this.save(); + const active = localStorage.getItem('webx11_active_theme'); + if (active === name) { localStorage.removeItem('webx11_active_theme'); } + this.terminal.print(`Theme '${name}' deleted`, 'success'); + } + exportTheme(name) { + if (!name) return this.terminal.print('Usage: theme export <name>', 'warning'); + if (!this.themes[name]) return this.terminal.print(`Theme '${name}' not found`, 'error'); + const data = JSON.stringify(this.themes[name], null, 2); + const blob = new Blob([data], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = `theme-${name}.json`; a.click(); + URL.revokeObjectURL(url); + this.terminal.print(`Theme '${name}' exported`, 'success'); + } +} +if (typeof window.webx11 !== 'undefined' && window.webx11) { + webx11.moduleManager.registerModule(new ThemeModule(), 'https://webx11.duckdns.org/spaces/chatstudio/newos4/modules/theme.js'); +} |