From de40c8e533fab926cf792ee41e0daf6206699245 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 28 Dec 2025 19:24:40 +0100 Subject: [PATCH] feat: Release v0.4.3 - Dual menu access and enhanced permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This release adds dual menu access for Network Modes (both SecuBox and LuCI Network menus) and significantly expands RPCD permissions for all mode configuration operations. ## Network Modes - Dual Menu Access (2 files) - Added Network Modes to standard LuCI Network menu (admin/network/modes) - Maintains existing SecuBox menu location (admin/secubox/network/modes) - Users can now access Network Modes from both locations - Menu order: 60 in Network menu, 10 in SecuBox Network category ## Network Modes - Enhanced Permissions (1 file) Added 13+ new RPCD methods to ACL for complete mode management: Read permissions: - preview_changes - sniffer_config, ap_config, relay_config, router_config - travel_config, doublenat_config, multiwan_config, vpnrelay_config - travel_scan_networks Write permissions: - apply_mode, confirm_mode, rollback - update_settings - generate_wireguard_keys, apply_wireguard_config - apply_mtu_clamping, enable_tcp_bbr - add_vhost, generate_config ## Network Modes - View Updates (11 files) Updated all mode views for consistency: - helpers.js: 28 lines refactored - overview.js: Enhanced view structure - All mode views: wizard, router, multiwan, doublenat, accesspoint, relay, vpnrelay, travel, sniffer ## Theme Enhancements (1 file) - theme.js: 89 lines added - Enhanced theme initialization and configuration - Improved component styling support ## SecuBox Dashboard (2 files) - Updated dashboard.js and modules.js - Improved view rendering and integration ## System Hub (3 files) - Enhanced logs.js, overview.js, services.js - Better view consistency and functionality Summary: - 19 files changed (+282, -36) - Dual menu access for Network Modes - 13+ new RPCD permission methods - All network mode views updated - Theme significantly enhanced 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../resources/network-modes/helpers.js | 28 +++--- .../view/network-modes/accesspoint.js | 5 +- .../resources/view/network-modes/doublenat.js | 5 +- .../resources/view/network-modes/multiwan.js | 5 +- .../resources/view/network-modes/overview.js | 7 +- .../resources/view/network-modes/relay.js | 5 +- .../resources/view/network-modes/router.js | 5 +- .../resources/view/network-modes/sniffer.js | 5 +- .../resources/view/network-modes/travel.js | 5 +- .../resources/view/network-modes/vpnrelay.js | 5 +- .../resources/view/network-modes/wizard.js | 5 +- .../luci/menu.d/luci-app-network-modes.json | 98 +++++++++++++++++++ .../rpcd/acl.d/luci-app-network-modes.json | 24 ++++- .../resources/view/secubox/dashboard.js | 5 +- .../resources/view/secubox/modules.js | 7 +- .../resources/view/system-hub/logs.js | 5 +- .../resources/view/system-hub/overview.js | 5 +- .../resources/view/system-hub/services.js | 5 +- .../resources/secubox-theme/theme.js | 89 ++++++++++++++++- 19 files changed, 282 insertions(+), 36 deletions(-) diff --git a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js index 9cf2d5c5..15c3fb3b 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js @@ -2,18 +2,18 @@ 'require ui'; 'require network-modes.api as api'; -var NAV_ITEMS = [ - { id: 'overview', icon: '📊', label: _('Overview') }, - { id: 'wizard', icon: '🧭', label: _('Wizard') }, - { id: 'router', icon: '🌐', label: _('Router') }, - { id: 'multiwan', icon: '🔀', label: _('Multi-WAN') }, - { id: 'doublenat', icon: '🧱', label: _('Double NAT') }, - { id: 'accesspoint', icon: '📡', label: _('Access Point') }, - { id: 'relay', icon: '📶', label: _('Relay') }, - { id: 'vpnrelay', icon: '🛡️', label: _('VPN Relay') }, - { id: 'travel', icon: '🧳', label: _('Travel') }, - { id: 'sniffer', icon: '🕵️', label: _('Sniffer') }, - { id: 'settings', icon: '⚙️', label: _('Settings') } +var NAV_BLUEPRINT = [ + { id: 'overview', icon: '📊', labelKey: 'Overview' }, + { id: 'wizard', icon: '🧭', labelKey: 'Wizard' }, + { id: 'router', icon: '🌐', labelKey: 'Router' }, + { id: 'multiwan', icon: '🔀', labelKey: 'Multi-WAN' }, + { id: 'doublenat', icon: '🧱', labelKey: 'Double NAT' }, + { id: 'accesspoint', icon: '📡', labelKey: 'Access Point' }, + { id: 'relay', icon: '📶', labelKey: 'Relay' }, + { id: 'vpnrelay', icon: '🛡️', labelKey: 'VPN Relay' }, + { id: 'travel', icon: '🧳', labelKey: 'Travel' }, + { id: 'sniffer', icon: '🕵️', labelKey: 'Sniffer' }, + { id: 'settings', icon: '⚙️', labelKey: 'Settings' } ]; function isToggleActive(node) { @@ -138,7 +138,7 @@ function createNavigationTabs(activeId) { var base = 'admin/secubox/network/modes/'; return E('nav', { 'class': 'nm-nav-tabs' }, [ E('div', { 'class': 'cyber-tablist' }, - NAV_ITEMS.map(function(item) { + NAV_BLUEPRINT.map(function(item) { var cls = 'cyber-tab'; if (activeId === item.id) cls += ' is-active'; @@ -149,7 +149,7 @@ function createNavigationTabs(activeId) { 'aria-current': activeId === item.id ? 'page' : null }, [ E('span', { 'class': 'cyber-tab-icon' }, item.icon), - E('span', { 'class': 'cyber-tab-label' }, item.label) + E('span', { 'class': 'cyber-tab-label' }, _(item.labelKey)) ]); }) ) diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/accesspoint.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/accesspoint.js index 9a296a71..b7301474 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/accesspoint.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/accesspoint.js @@ -6,7 +6,10 @@ 'require network-modes.helpers as helpers'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: nmLang }); function buildWifiToggle(flag, label, desc, active) { return E('div', { 'class': 'nm-toggle' }, [ diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/doublenat.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/doublenat.js index 200bd396..7b8ef4ba 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/doublenat.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/doublenat.js @@ -5,7 +5,10 @@ 'require network-modes.helpers as helpers'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: nmLang }); return view.extend({ title: _('Double NAT Mode'), diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/multiwan.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/multiwan.js index 0d2db2ae..10ab6d23 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/multiwan.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/multiwan.js @@ -5,7 +5,10 @@ 'require network-modes.helpers as helpers'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: nmLang }); return view.extend({ title: _('Multi-WAN Mode'), diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js index a153a2a7..8c63ec0c 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js @@ -7,8 +7,11 @@ 'require secubox/help as Help'; 'require secubox-theme/theme as Theme'; -// Initialize global theme -Theme.init({ theme: 'dark', language: 'en' }); +// Initialize global theme respecting LuCI preferences +var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: nmLang }); return view.extend({ title: _('Network Modes'), diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/relay.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/relay.js index a45ca9cd..e6d5963d 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/relay.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/relay.js @@ -6,7 +6,10 @@ 'require network-modes.helpers as helpers'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: nmLang }); function buildOptToggle(key, icon, label, desc, enabled) { return E('div', { 'class': 'nm-toggle' }, [ diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js index 12bd242a..960ed952 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js @@ -6,7 +6,10 @@ 'require network-modes.helpers as helpers'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: nmLang }); function buildToggle(id, icon, title, desc, enabled) { return E('div', { 'class': 'nm-toggle' }, [ diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/sniffer.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/sniffer.js index 32b81276..b068d1b2 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/sniffer.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/sniffer.js @@ -6,7 +6,10 @@ 'require network-modes.helpers as helpers'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: nmLang }); function buildToggle(id, icon, title, desc, active) { return E('div', { 'class': 'nm-toggle' }, [ diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/travel.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/travel.js index 7024ac8c..7e0ec999 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/travel.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/travel.js @@ -7,7 +7,10 @@ 'require secubox/help as Help'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: nmLang }); return view.extend({ title: _('Travel Router Mode'), diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/vpnrelay.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/vpnrelay.js index 0ac4e5c3..7fd2c4de 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/vpnrelay.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/vpnrelay.js @@ -5,7 +5,10 @@ 'require network-modes.helpers as helpers'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: nmLang }); return view.extend({ title: _('VPN Relay Mode'), diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js index 3904f4c4..fceb3749 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js @@ -50,7 +50,10 @@ var callRollback = rpc.declare({ expect: { } }); -Theme.init({ theme: 'dark', language: 'en' }); +var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: nmLang }); return view.extend({ title: _('Network Mode Wizard'), diff --git a/luci-app-network-modes/root/usr/share/luci/menu.d/luci-app-network-modes.json b/luci-app-network-modes/root/usr/share/luci/menu.d/luci-app-network-modes.json index 2a786e64..9fabdf8f 100644 --- a/luci-app-network-modes/root/usr/share/luci/menu.d/luci-app-network-modes.json +++ b/luci-app-network-modes/root/usr/share/luci/menu.d/luci-app-network-modes.json @@ -96,5 +96,103 @@ "type": "view", "path": "network-modes/settings" } + }, + "admin/network/modes": { + "title": "Network Modes", + "order": 60, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": ["luci-app-network-modes"] + } + }, + "admin/network/modes/overview": { + "title": "Overview", + "order": 10, + "action": { + "type": "view", + "path": "network-modes/overview" + } + }, + "admin/network/modes/wizard": { + "title": "Mode Wizard", + "order": 20, + "action": { + "type": "view", + "path": "network-modes/wizard" + } + }, + "admin/network/modes/router": { + "title": "Router Mode", + "order": 30, + "action": { + "type": "view", + "path": "network-modes/router" + } + }, + "admin/network/modes/multiwan": { + "title": "Multi-WAN Mode", + "order": 35, + "action": { + "type": "view", + "path": "network-modes/multiwan" + } + }, + "admin/network/modes/doublenat": { + "title": "Double NAT Mode", + "order": 37, + "action": { + "type": "view", + "path": "network-modes/doublenat" + } + }, + "admin/network/modes/accesspoint": { + "title": "Access Point Mode", + "order": 40, + "action": { + "type": "view", + "path": "network-modes/accesspoint" + } + }, + "admin/network/modes/relay": { + "title": "Relay Mode", + "order": 50, + "action": { + "type": "view", + "path": "network-modes/relay" + } + }, + "admin/network/modes/vpnrelay": { + "title": "VPN Relay Mode", + "order": 52, + "action": { + "type": "view", + "path": "network-modes/vpnrelay" + } + }, + "admin/network/modes/travel": { + "title": "Travel Mode", + "order": 55, + "action": { + "type": "view", + "path": "network-modes/travel" + } + }, + "admin/network/modes/sniffer": { + "title": "Sniffer Mode", + "order": 60, + "action": { + "type": "view", + "path": "network-modes/sniffer" + } + }, + "admin/network/modes/settings": { + "title": "Settings", + "order": 90, + "action": { + "type": "view", + "path": "network-modes/settings" + } } } diff --git a/luci-app-network-modes/root/usr/share/rpcd/acl.d/luci-app-network-modes.json b/luci-app-network-modes/root/usr/share/rpcd/acl.d/luci-app-network-modes.json index 111ebbb7..cf8a9ef7 100644 --- a/luci-app-network-modes/root/usr/share/rpcd/acl.d/luci-app-network-modes.json +++ b/luci-app-network-modes/root/usr/share/rpcd/acl.d/luci-app-network-modes.json @@ -8,7 +8,17 @@ "get_current_mode", "get_available_modes", "get_interfaces", - "validate_config" + "validate_config", + "preview_changes", + "sniffer_config", + "ap_config", + "relay_config", + "router_config", + "travel_config", + "doublenat_config", + "multiwan_config", + "vpnrelay_config", + "travel_scan_networks" ], "system": [ "info", "board" ], "network.interface": [ "status", "dump" ], @@ -24,7 +34,17 @@ "write": { "ubus": { "luci.network-modes": [ - "set_mode" + "set_mode", + "apply_mode", + "confirm_mode", + "rollback", + "update_settings", + "generate_wireguard_keys", + "apply_wireguard_config", + "apply_mtu_clamping", + "enable_tcp_bbr", + "add_vhost", + "generate_config" ] }, "uci": [ "network", "wireless", "firewall", "dhcp", "network-modes" ] diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js index ba1facc9..4ef3b38b 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js @@ -13,7 +13,10 @@ document.head.appendChild(E('link', { 'href': L.resource('secubox-theme/secubox-theme.css') })); -Theme.init({ theme: 'dark', language: 'en' }); +var secuLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: secuLang }); return view.extend({ dashboardData: null, diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js index 27bac2e1..588373d4 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js @@ -18,8 +18,11 @@ document.head.appendChild(E('link', { 'href': L.resource('secubox/modules.css') })); -// Initialize global theme -Theme.init({ theme: 'dark', language: 'en' }); +// Initialize global theme respecting LuCI selection +var secuLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: secuLang }); return view.extend({ modulesData: [], diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js index d433e6e5..cf452150 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js @@ -6,7 +6,10 @@ 'require system-hub/api as API'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: shLang }); return view.extend({ logs: [], diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js index 3e800382..fca3780d 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js @@ -6,7 +6,10 @@ 'require system-hub/api as API'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: shLang }); return view.extend({ sysInfo: null, diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js index 977cf386..ba63e6f4 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js @@ -6,7 +6,10 @@ 'require system-hub/api as API'; 'require secubox-theme/theme as Theme'; -Theme.init({ theme: 'dark', language: 'en' }); +var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: shLang }); return view.extend({ services: [], diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/theme.js b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/theme.js index fbca1cda..da1d7db8 100644 --- a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/theme.js +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/theme.js @@ -10,19 +10,38 @@ return baseclass.extend({ currentTheme: 'dark', currentLanguage: 'en', translations: {}, + availableThemes: ['dark', 'light', 'cyberpunk'], init: function(options) { var opts = options || {}; - var theme = opts.theme || this.currentTheme; - var lang = opts.language || this.currentLanguage; + var lang = opts.language || this._detectLanguage(); + var theme = this._isValidTheme(opts.theme) ? opts.theme : this._detectPreferredTheme(); this.apply(theme); return this.setLanguage(lang); }, apply: function(theme) { + if (!this._isValidTheme(theme)) + theme = this._detectPreferredTheme(); + this.currentTheme = theme || 'dark'; - document.body.setAttribute('data-secubox-theme', this.currentTheme); + + if (document.documentElement) + document.documentElement.setAttribute('data-secubox-theme', this.currentTheme); + if (document.body) + document.body.setAttribute('data-secubox-theme', this.currentTheme); + }, + + setPreferredTheme: function(theme) { + if (!this._isValidTheme(theme)) + return; + + try { + window.localStorage.setItem('secubox.theme', theme); + } catch (err) { /* ignore private mode */ } + + this.apply(theme); }, setLanguage: function(lang) { @@ -100,5 +119,69 @@ return baseclass.extend({ opts.header || null, E('div', { 'class': 'cyber-stack' }, opts.cards || []) ]); + }, + + _detectLanguage: function() { + if (typeof L !== 'undefined' && L.env && L.env.lang) + return L.env.lang; + if (document.documentElement && document.documentElement.getAttribute('lang')) + return document.documentElement.getAttribute('lang'); + if (navigator.language) + return navigator.language.split('-')[0]; + return this.currentLanguage; + }, + + _detectPreferredTheme: function() { + var stored; + + try { + stored = window.localStorage.getItem('secubox.theme'); + } catch (err) { + stored = null; + } + + if (this._isValidTheme(stored)) + return stored; + + if (typeof L !== 'undefined' && L.env && L.env.media_url_base) { + var media = L.env.media_url_base || ''; + if (/(openwrt|dark|argon|opentwenty|opentop)/i.test(media)) + return 'dark'; + if (/bootstrap|material|simple|freifunk/i.test(media)) + return 'light'; + } + + var attr = (document.documentElement && document.documentElement.getAttribute('data-theme')) || + (document.body && document.body.getAttribute('data-theme')); + if (attr) { + if (/cyber/i.test(attr)) + return 'cyberpunk'; + if (/light/i.test(attr)) + return 'light'; + if (/dark|secubox/i.test(attr)) + return 'dark'; + } + + if (document.body && document.body.className) { + if (/\bluci-theme-[a-z0-9]+/i.test(document.body.className)) { + if (/\b(light|bootstrap|material)\b/i.test(document.body.className)) + return 'light'; + if (/\b(openwrt2020|argon|dark)\b/i.test(document.body.className)) + return 'dark'; + } + } + + if (window.matchMedia) { + try { + if (window.matchMedia('(prefers-color-scheme: light)').matches) + return 'light'; + } catch (err) { /* ignore */ } + } + + return this.currentTheme; + }, + + _isValidTheme: function(theme) { + return this.availableThemes.indexOf(theme) !== -1; } });