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 <noreply@anthropic.com>
188 lines
5.2 KiB
JavaScript
188 lines
5.2 KiB
JavaScript
'use strict';
|
|
'require baseclass';
|
|
|
|
/**
|
|
* SecuBox CyberMood Theme Controller
|
|
* Provides Theme.init(), Theme.apply(), Theme.setLanguage(), Theme.t(), and UI helpers.
|
|
*/
|
|
|
|
return baseclass.extend({
|
|
currentTheme: 'dark',
|
|
currentLanguage: 'en',
|
|
translations: {},
|
|
availableThemes: ['dark', 'light', 'cyberpunk'],
|
|
|
|
init: function(options) {
|
|
var opts = options || {};
|
|
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';
|
|
|
|
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) {
|
|
var self = this;
|
|
this.currentLanguage = lang || 'en';
|
|
|
|
if (self.translations[self.currentLanguage]) {
|
|
return Promise.resolve(self.translations[self.currentLanguage]);
|
|
}
|
|
|
|
var url = L.resource('secubox-theme/i18n/' + this.currentLanguage + '.json');
|
|
return fetch(url).then(function(res) {
|
|
if (!res.ok)
|
|
throw new Error('Unable to load translations for ' + self.currentLanguage);
|
|
return res.json();
|
|
}).then(function(dict) {
|
|
self.translations[self.currentLanguage] = dict;
|
|
return dict;
|
|
}).catch(function(err) {
|
|
console.error('Translation error:', err);
|
|
self.translations[self.currentLanguage] = self.translations.en || {};
|
|
return self.translations[self.currentLanguage];
|
|
});
|
|
},
|
|
|
|
t: function(key, params) {
|
|
params = params || {};
|
|
var dict = this.translations[this.currentLanguage] ||
|
|
this.translations.en || {};
|
|
var str = dict[key] || key;
|
|
Object.keys(params).forEach(function(k) {
|
|
str = str.replace(new RegExp('\\{' + k + '\\}', 'g'), params[k]);
|
|
});
|
|
return str;
|
|
},
|
|
|
|
createCard: function(options) {
|
|
var opts = options || {};
|
|
return E('div', { 'class': 'cyber-card' }, [
|
|
opts.hideHeader ? null : E('div', { 'class': 'cyber-card-header' }, [
|
|
E('div', { 'class': 'cyber-card-title' }, [
|
|
opts.icon ? E('span', { 'style': 'margin-right: 0.35rem;' }, opts.icon) : null,
|
|
opts.title || ''
|
|
]),
|
|
opts.badge || null
|
|
]),
|
|
E('div', { 'class': 'cyber-card-body' }, opts.content || [])
|
|
]);
|
|
},
|
|
|
|
createButton: function(options) {
|
|
var opts = options || {};
|
|
var classes = ['cyber-btn'];
|
|
if (opts.variant === 'secondary') classes.push('cyber-btn--secondary');
|
|
if (opts.variant === 'danger') classes.push('cyber-btn--danger');
|
|
if (opts.variant === 'ghost') classes.push('cyber-btn--ghost');
|
|
|
|
return E('button', Object.assign({
|
|
'class': classes.join(' ')
|
|
}, opts.attrs || {}), [
|
|
opts.icon ? E('span', {}, opts.icon) : null,
|
|
opts.label || ''
|
|
]);
|
|
},
|
|
|
|
createBadge: function(text, variant) {
|
|
var classes = ['cyber-badge'];
|
|
if (variant) classes.push('cyber-badge--' + variant);
|
|
return E('span', { 'class': classes.join(' ') }, text);
|
|
},
|
|
|
|
createPage: function(options) {
|
|
var opts = options || {};
|
|
return E('div', { 'class': 'cyber-container' }, [
|
|
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;
|
|
}
|
|
});
|