💋 Sync 2026-10-16 23:55:30

This commit is contained in:
github-actions[bot] committed 2026-10-16 23:55:30 +08:00
1 parent 76f10c2a74
commit 1849cc6156
254 files changed
+25484 -8840

No files matched your search

@@ -0,0 +1,166 @@
'use strict';
'require baseclass';
'require rpc';
'require ui';
var callGet = rpc.declare({ object: 'luci.argon_appearance', method: 'get', expect: { '': {} }, raise: true });
var callSet = rpc.declare({ object: 'luci.argon_appearance', method: 'set', params: ['mode'], expect: { '': {} }, raise: true });
var callAccess = rpc.declare({ object: 'session', method: 'access', params: ['scope', 'object', 'function'], expect: { access: false }, raise: true });
return baseclass.extend({
__init__: function() {
this.root = document.getElementById('argon-appearance');
if (!this.root)
return;
this.mode = document.documentElement.dataset.argonMode;
this.busy = false;
this.preference = window.matchMedia('(prefers-color-scheme: dark)');
return Promise.all([ callGet(), callAccess('ubus', 'luci.argon_appearance', 'set') ]).then(function(data) {
if (data[0].result !== 0 || !this.validMode(data[0].mode))
return;
this.writable = data[1] === true;
this.render();
this.applyMode(data[0].mode);
this.root.hidden = false;
var onSystemChange = function() {
if (this.mode === 'normal')
this.applyMode('normal');
}.bind(this);
if (this.preference.addEventListener)
this.preference.addEventListener('change', onSystemChange);
else
this.preference.addListener(onSystemChange);
}.bind(this)).catch(function() {
// Leave the server-rendered theme intact when the service is unavailable.
});
},
validMode: function(mode) {
return [ 'normal', 'light', 'dark' ].includes(mode);
},
render: function() {
this.labels = { light: _('Light mode'), dark: _('Dark mode'), normal: _('Follow system') };
this.trigger = E('button', {
type: 'button', 'class': 'argon-appearance-trigger',
'aria-label': _('Theme mode'), 'aria-expanded': 'false', 'aria-controls': 'argon-appearance-options',
click: function() { this.open(this.panel.hidden); }.bind(this)
});
this.radios = Object.keys(this.labels).map(function(mode) {
return E('input', {
type: 'radio', name: 'argon-appearance-mode', value: mode,
'class': 'visually-hidden',
disabled: !this.writable || null,
change: function(event) {
if (event.target.checked)
this.saveMode(mode);
}.bind(this)
});
}.bind(this));
this.panel = E('fieldset', { id: 'argon-appearance-options', 'class': 'argon-appearance-panel', hidden: true }, [
E('legend', { 'class': 'visually-hidden' }, _('Theme mode')),
...this.radios.map(function(radio) {
return E('label', { 'class': 'argon-appearance-option', 'data-mode': radio.value }, [
radio, E('span', {}, this.labels[radio.value])
]);
}.bind(this)),
!this.writable ? E('p', {}, _('Read-only')) : ''
]);
this.message = E('span', { 'class': 'visually-hidden', role: 'status' });
this.root.appendChild(this.trigger);
this.root.appendChild(this.panel);
this.root.appendChild(this.message);
this.root.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && !this.panel.hidden) {
event.preventDefault();
this.open(false, true);
}
}.bind(this));
document.addEventListener('click', function(event) {
if (!this.root.contains(event.target))
this.open(false);
}.bind(this));
this.root.addEventListener('focusout', function(event) {
if (event.relatedTarget && !this.root.contains(event.relatedTarget))
this.open(false);
// A pointer press on a label can leave focus on the page until release.
// With no destination, wait for label activation or the outside click.
}.bind(this));
window.addEventListener('blur', function() { this.open(false); }.bind(this));
},
open: function(show, restoreFocus) {
this.panel.hidden = !show;
this.trigger.setAttribute('aria-expanded', String(show));
if (show) {
var checked = this.radios.find(function(radio) { return radio.checked && !radio.disabled; });
if (checked)
checked.focus();
} else if (restoreFocus) {
this.trigger.focus();
}
},
applyMode: function(mode) {
var html = document.documentElement;
html.classList.add('argon-theme-changing');
html.dataset.argonMode = mode;
this.mode = mode;
this.root.dataset.mode = mode;
this.radios.forEach(function(radio) { radio.checked = radio.value === mode; });
this.trigger.title = _('Theme mode') + ': ' + this.labels[mode];
this.trigger.setAttribute('aria-label', this.trigger.title);
// Update browser chrome as well as the CSS palette, including system mode.
var dark = mode === 'dark' || (mode === 'normal' && this.preference.matches);
var color = getComputedStyle(html).getPropertyValue(dark ? '--dark-primary' : '--light-primary').trim();
document.querySelectorAll('meta[name="theme-color"]').forEach(function(meta, index) {
meta.removeAttribute('media');
if (index === 0)
meta.content = color;
else
meta.remove();
});
void html.offsetHeight;
cancelAnimationFrame(this.frame);
this.frame = requestAnimationFrame(function() {
html.classList.remove('argon-theme-changing');
});
},
saveMode: function(mode) {
if (!this.writable || this.busy || !this.validMode(mode) || mode === this.mode)
return Promise.resolve();
var previous = this.mode;
this.busy = true;
this.radios.forEach(function(radio) { radio.disabled = true; });
this.panel.setAttribute('aria-busy', 'true');
this.message.textContent = _('Saving theme mode…');
this.applyMode(mode);
this.open(false, true);
return callSet(mode).then(function(result) {
if (result.result !== 0 || result.mode !== mode)
throw new Error('save failed');
this.saved(mode);
}.bind(this)).catch(function() {
// A lost reply may follow a successful commit; read back before reverting.
return callGet().catch(function() { return {}; }).then(function(state) {
this.applyMode(state.result === 0 && this.validMode(state.mode) ? state.mode : previous);
if (state.result === 0 && state.mode === mode) {
this.saved(mode);
return;
}
this.message.textContent = _('Unable to save theme mode. Please try again.');
ui.addNotification(null, E('p', { role: 'alert' }, this.message.textContent), 'error');
}.bind(this));
}.bind(this)).finally(function() {
this.busy = false;
this.panel.removeAttribute('aria-busy');
this.radios.forEach(function(radio) { radio.disabled = !this.writable; }.bind(this));
}.bind(this));
},
saved: function(mode) {
this.message.textContent = _('Theme mode saved.');
window.dispatchEvent(new CustomEvent('argon-appearance-saved', { detail: { mode: mode } }));
}
});
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" width="320" height="180">
<defs><linearGradient id="sky" x2="1" y2="1"><stop stop-color="#dbeafe"/><stop offset="1" stop-color="#818cf8"/></linearGradient></defs>
<rect width="320" height="180" fill="url(#sky)"/>
<circle cx="260" cy="38" r="15" fill="#fef3c7"/>
<path d="M0 130 72 76 148 140 237 66 320 124V180H0Z" fill="#6366a0"/>
<path d="M0 164 111 110 207 158 280 115 320 145V180H0Z" fill="#414679"/>
<rect x="104" y="27" width="112" height="126" rx="6" fill="#f4f5f7" fill-opacity=".94"/><g transform="translate(118 41)">
<rect x="13" width="16" height="16" rx="4" fill="#5e72e4"/>
<circle cx="21" cy="8" r="4" fill="none" stroke="#fff" stroke-width="2"/>
<rect x="37" y="5" width="34" height="5" rx="2" fill="#32325d"/>
<rect x="7" y="32" width="70" height="3" rx="1.5" fill="#8898aa"/><rect x="7" y="50" width="70" height="3" rx="1.5" fill="#8898aa"/><rect x="7" y="67" width="70" height="16" rx="3" fill="#5e72e4"/><rect x="26" y="93" width="32" height="2" rx="1" fill="#8898aa"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" width="320" height="180">
<defs><linearGradient id="sky" x2="1" y2="1"><stop stop-color="#dbeafe"/><stop offset="1" stop-color="#818cf8"/></linearGradient></defs>
<rect width="320" height="180" fill="url(#sky)"/>
<circle cx="260" cy="38" r="15" fill="#fef3c7"/>
<path d="M0 130 72 76 148 140 237 66 320 124V180H0Z" fill="#6366a0"/>
<path d="M0 164 111 110 207 158 280 115 320 145V180H0Z" fill="#414679"/>
<rect x="20" width="112" height="180" fill="#f4f5f7" fill-opacity=".94"/><g transform="translate(34 20)">
<rect x="13" width="16" height="16" rx="4" fill="#5e72e4"/>
<circle cx="21" cy="8" r="4" fill="none" stroke="#fff" stroke-width="2"/>
<rect x="37" y="5" width="34" height="5" rx="2" fill="#32325d"/>
<rect x="7" y="48" width="70" height="3" rx="1.5" fill="#8898aa"/><rect x="7" y="68" width="70" height="3" rx="1.5" fill="#8898aa"/><rect x="7" y="92" width="70" height="16" rx="3" fill="#5e72e4"/><rect x="26" y="139" width="32" height="2" rx="1" fill="#8898aa"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,77 @@
'use strict';
'require baseclass';
// Native realtime plots retain the coordinates calculated on first render.
// Scale that coordinate system on resize, keeping text and strokes readable.
return baseclass.extend({
__init__: function() {
var view = document.querySelector('#view');
if (!view || !window.ResizeObserver)
return;
var graphs = new Map();
var frame = null;
var update = function() {
frame = null;
view.querySelectorAll('svg').forEach(function(svg) {
if (graphs.has(svg) || svg.hasAttribute('viewBox'))
return;
// An inactive tab may first become measurable after the window
// has resized. Read the native plot extent, not today's viewport.
var width = 0;
svg.querySelectorAll('polyline').forEach(function(line) {
for (var i = 0; i < line.points.numberOfItems; i++)
width = Math.max(width, line.points.getItem(i).x);
});
var height = svg.clientHeight;
if (width <= 0 || !(height > 0))
return;
graphs.set(svg, width);
svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
svg.setAttribute('preserveAspectRatio', 'none');
});
graphs.forEach(function(width, svg) {
if (!svg.isConnected) {
graphs.delete(svg);
return;
}
var bounds = svg.getBoundingClientRect();
if (bounds.width <= 0)
return;
var ratio = bounds.width / width;
var inverse = 1 / ratio;
svg.querySelectorAll('text').forEach(function(text) {
var x = parseFloat(text.getAttribute('x')) || 0;
var offset = x * (1 - inverse);
var transform = function(shift) {
text.style.transform = 'translateX(' + shift + 'px) scaleX(' + inverse + ')';
};
text.style.transformOrigin = '0 0';
transform(offset);
var label = text.getBoundingClientRect();
var shift = Math.max(0, bounds.left + 4 - label.left) -
Math.max(0, label.right - bounds.right + 4);
if (shift)
transform(offset + shift / ratio);
});
});
};
var schedule = function() {
if (frame === null)
frame = requestAnimationFrame(update);
};
new ResizeObserver(schedule).observe(view);
new MutationObserver(schedule).observe(view, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [ 'data-tab-active', 'hidden', 'points' ]
});
schedule();
}
});
@@ -0,0 +1,60 @@
'use strict';
'require baseclass';
// Keep the native counters and tooltip, adding structure after each poll render.
return baseclass.extend({
__init__: function() {
var view = document.querySelector('#view');
if (!view)
return;
var frame = null;
var update = function() {
frame = null;
view.querySelectorAll('.ifacebox[style*="min-width:70px"] > .ifacebox-body:last-child > .cbi-tooltip-container').forEach(function(container) {
if (container.querySelector('.argon-port-traffic'))
return;
var values = {};
var nodes = Array.from(container.childNodes);
nodes.forEach(function(node) {
if (node.nodeType !== 3)
return;
var text = node.textContent.trim();
if (text.charAt(0) === '▲' || text.charAt(0) === '▼')
values[text.charAt(0)] = text.slice(1).trim();
});
if (!values['▲'] || !values['▼'])
return;
var list = document.createElement('dl');
list.className = 'argon-port-traffic';
[
[ 'tx', 'TX', _('Transmitted bytes'), values['▲'] ],
[ 'rx', 'RX', _('Received bytes'), values['▼'] ]
].forEach(function(entry) {
var row = document.createElement('div');
var label = document.createElement('dt');
var value = document.createElement('dd');
row.dataset.direction = entry[0];
label.textContent = entry[1];
label.title = entry[2];
value.textContent = entry[3];
row.append(label, value);
list.append(row);
});
nodes.forEach(function(node) {
if (node.nodeType === 3 || node.nodeName === 'BR')
node.remove();
});
container.prepend(list);
});
};
var schedule = function() {
if (frame === null)
frame = requestAnimationFrame(update);
};
new MutationObserver(schedule).observe(view, { childList: true, subtree: true, characterData: true });
schedule();
}
});
@@ -0,0 +1,63 @@
'use strict';
'require baseclass';
return baseclass.extend({
__init__: function() {
var view = document.querySelector('#view');
if (!view)
return;
var frame = null;
var update = function() {
frame = null;
view.querySelectorAll('.cbi-dynlist > .item[data-key]').forEach(function(item) {
if (item.querySelector('.argon-key-details'))
return;
var name = item.querySelector(':scope > strong');
var metadata = item.querySelector(':scope > small');
if (!name || !metadata)
return;
// The native preview is abbreviated base64, not a key fingerprint.
var preview = metadata.lastElementChild;
if (preview && preview.tagName === 'CODE') {
if (preview.previousSibling && preview.previousSibling.nodeName === 'BR')
preview.previousSibling.remove();
preview.remove();
}
var remove = document.createElement('button');
remove.type = 'button';
remove.className = 'cbi-button cbi-button-remove argon-key-delete';
remove.textContent = _('Delete');
remove.setAttribute('aria-label', _('Delete key') + ': ' + name.textContent);
remove.disabled = !L.hasViewPermission();
remove.addEventListener('click', function(ev) {
ev.stopPropagation();
// Dispatch on the original item so its native confirmation keeps
// the exact data-key and the original authorization checks.
item.click();
});
var details = document.createElement('details');
var summary = document.createElement('summary');
var key = document.createElement('pre');
details.className = 'argon-key-details';
summary.textContent = _('Public key');
key.textContent = item.getAttribute('data-key');
details.append(summary, key);
// Expanding or selecting the key must not open Delete confirmation.
details.addEventListener('click', function(ev) { ev.stopPropagation(); });
item.append(remove, details);
item.classList.add('argon-key-item');
});
};
var schedule = function() {
if (frame === null)
frame = requestAnimationFrame(update);
};
new MutationObserver(schedule).observe(view, { childList: true, subtree: true });
schedule();
}
});
@@ -0,0 +1,116 @@
'use strict';
'require baseclass';
'require fs';
'require rpc';
'require ui';
var callGet = rpc.declare({ object: 'luci.argon_branding', method: 'get', expect: { '': {} }, raise: true });
var callSet = rpc.declare({ object: 'luci.argon_branding', method: 'set', params: ['target', 'token'], expect: { '': {} }, raise: true });
var callReset = rpc.declare({ object: 'luci.argon_branding', method: 'reset', params: ['target'], expect: { '': {} }, raise: true });
return baseclass.extend({
load: function() {
return callGet().catch(function() { return null; });
},
render: function(map, state) {
var busy = false;
var buttons = [];
var previews = {};
var defaults = {
favicon: L.resource('../argon/icon/favicon-32x32.png'),
logo: L.resource('../argon/img/argon.svg')
};
var labels = { favicon: _('Browser icon'), logo: _('Login logo') };
var available = state && state.result === 0;
function assetUrl(kind) {
return state && /^image-[0-9a-f]{64}\.png$/.test(state[kind] || '') ?
L.resource('../argon/branding/' + state[kind]) : defaults[kind];
}
function refresh() {
Object.keys(previews).forEach(function(kind) {
previews[kind].src = assetUrl(kind);
});
buttons.forEach(function(button) {
button.node.disabled = !!(map.readonly || busy || !available ||
(button.reset && !state[button.target]));
});
}
function update(target, upload) {
if (map.readonly || busy || !available)
return Promise.resolve();
busy = true;
refresh();
var file;
return Promise.resolve().then(function() {
if (!upload)
return callReset(target);
var token = Array.from(crypto.getRandomValues(new Uint32Array(4)), function(n) {
return ('00000000' + n.toString(16)).slice(-8);
}).join('');
file = '/tmp/argon_branding_' + token + '.png';
return ui.uploadFile(file).then(function() {
return callSet(target, token);
});
}).then(function(result) {
if (result.result === 2)
throw new Error(_('Invalid PNG image or dimensions.'));
if (result.result === 3)
throw new Error(_('Another update is running. Please try again.'));
if (result.result !== 0)
throw new Error(_('Unable to save custom branding.'));
state = result;
}).catch(function(error) {
ui.addNotification(null, E('p', {}, error.message), 'error');
}).finally(function() {
return (file ? L.resolveDefault(fs.remove(file)) : Promise.resolve()).finally(function() {
busy = false;
refresh();
});
});
}
function action(target, upload, label) {
var node = E('button', {
'type': 'button',
'class': 'btn cbi-button ' + (upload ? 'cbi-button-action' : 'cbi-button-reset'),
'click': function() { return update(target, upload); }
}, label);
buttons.push({ node: node, target: target, reset: !upload });
return node;
}
var cards = Object.keys(labels).map(function(kind) {
var preview = E('img', {
'src': assetUrl(kind), 'alt': labels[kind], 'width': 64, 'height': 64,
'error': function() { if (this.getAttribute('src') !== defaults[kind]) this.src = defaults[kind]; }
});
previews[kind] = preview;
return E('div', { 'class': 'argon-branding-card' }, [
E('h4', {}, labels[kind]),
E('div', { 'class': 'argon-branding-row' }, [
preview,
E('div', { 'class': 'argon-branding-actions' }, [
action(kind, true, _('Upload PNG...')),
action(kind, false, _('Restore default'))
])
])
]);
});
var content = E('div', { 'class': 'argon-branding-content' }, [
E('p', {}, _('PNG images, 16–1024 pixels per side, up to 1 MiB. Browser icons must be square.')),
E('div', { 'class': 'argon-branding-cards' }, cards),
action('both', true, _('Upload for both...')),
E('p', {}, _('Changes apply immediately. Reload open pages to update their icons.'))
]);
if (!available)
content.appendChild(E('p', { 'class': 'error' }, _('Unable to load custom branding. Reload this page or reinstall the theme.')));
refresh();
return E('div', { 'class': 'cbi-section', 'id': 'argon-branding' }, [
E('h3', {}, _('Custom branding')), content
]);
}
});
@@ -201,9 +201,27 @@ return baseclass.extend({
* Load menu data and trigger rendering
*/
__init__: function () {
this.patchDynamicListFocus();
ui.menu.load().then(L.bind(this.render, this));
},
patchDynamicListFocus: function () {
var prototype = ui.Dropdown && ui.Dropdown.prototype;
var setFocus = prototype && prototype.setFocus;
if (typeof setFocus !== 'function' || setFocus.argonDynamicListGuard)
return;
var guardedFocus = function (node, item) {
// A DynamicList change handler can consume a newly created option
// before Dropdown.createItems() tries to scroll and focus it.
if (item && !item.parentNode && node && node.closest('.cbi-dynlist'))
return;
return setFocus.apply(this, arguments);
};
guardedFocus.argonDynamicListGuard = true;
prototype.setFocus = guardedFocus;
},
/**
* Main render function for the menu system
* @param {Object} tree - Menu tree structure from LuCI
@@ -250,7 +268,8 @@ return baseclass.extend({
* @param {Event} ev - Click event from menu item
*/
handleMenuExpand: function (ev) {
var target = ev.target;
// The click may originate on the SVG icon inside the navigation link.
var target = ev.currentTarget;
var slide = target.parentNode;
var slideMenu = target.nextElementSibling;
var shouldCollapse = false;
@@ -302,6 +321,20 @@ return baseclass.extend({
* @param {number} level - Current nesting level (0-based)
* @returns {Element} - Generated menu element
*/
renderMenuIcon: function (name) {
var catalog = document.querySelector('meta[name="argon-menu-icons"]');
var icons = catalog ? (catalog.getAttribute('content') || '').split(/\s+/) : [];
var filename = /^[a-zA-Z0-9_-]+$/.test(name) && icons.indexOf(name + '.svg') !== -1
? name + '.svg' : 'default.svg';
return E('span', {
'class': 'menu-icon',
'aria-hidden': 'true',
'data-icon': filename.slice(0, -4),
'style': '--menu-icon: url("' + L.media('icon', filename) + '")'
});
},
renderMainMenu: function (tree, url, level) {
var currentLevel = (level || 0) + 1;
var menuContainer = E('ul', { 'class': level ? 'slide-menu' : 'nav' });
@@ -341,7 +374,7 @@ return baseclass.extend({
'click': (currentLevel === 1) ? ui.createHandlerFn(this, 'handleMenuExpand') : null,
'class': menuClass,
'data-title': child.title.replace(/ /g, "_"), // More robust space replacement
}, [_(child.title)]),
}, currentLevel === 1 ? [this.renderMenuIcon(child.name), _(child.title)] : [_(child.title)]),
submenu
]);
@@ -0,0 +1,354 @@
'use strict';
'require form';
'require argon.branding as branding';
'require fs';
'require rpc';
'require uci';
'require ui';
'require view';
var callAvailSpace = rpc.declare({
object: 'luci.argon',
method: 'avail',
raise: true,
expect: { '': { avail: 0 } }
});
var callRemoveArgon = rpc.declare({
object: 'luci.argon',
method: 'remove',
params: ['filename'],
raise: true,
expect: { '': {} }
});
var callRenameArgon = rpc.declare({
object: 'luci.argon',
method: 'rename',
params: ['newname'],
raise: true,
expect: { '': {} }
});
var bg_path = '/www/luci-static/argon/background/';
var trans_set = [0, 0.1, 0.2, 0.3, 0.4,
0.5, 0.6, 0.7, 0.8, 0.9, 1 ];
function expandHex(value) {
if (/^#[0-9a-f]{3}$/i.test(value))
return value.replace(/[0-9a-f]/gi, function(digit) { return digit + digit; }).toLowerCase();
return /^#[0-9a-f]{6}$/i.test(value) ? value.toLowerCase() : null;
}
var ColorValue = form.Value.extend({
validate: function(section_id, value) {
return expandHex(value) !== null ||
_('Expecting: %s').format(_('valid HEX color value'));
},
renderWidget: function(section_id, option_index, cfgvalue) {
// Keep the native Textfield and its binding as the saved form value.
var node = this.super('renderWidget', [section_id, option_index, cfgvalue]);
var input = node.querySelector('input');
var picker = E('input', {
'type': 'color',
'id': this.cbid(section_id) + '-picker',
'class': 'argon-color-swatch',
'value': expandHex(input.value) || this.default,
'disabled': input.disabled || null,
'aria-label': this.title,
'title': this.title
});
node.classList.add('argon-color-picker');
input.setAttribute('spellcheck', 'false');
input.setAttribute('autocapitalize', 'off');
input.setAttribute('autocomplete', 'off');
node.appendChild(picker);
var updatePicker = function() {
var color = expandHex(input.value);
if (color !== null)
picker.value = color;
};
var updateText = L.bind(function() {
if (input.disabled || input.readOnly)
return;
input.value = picker.value;
input.dispatchEvent(new Event('change', { bubbles: true }));
this.triggerValidation(section_id);
}, this);
input.addEventListener('input', updatePicker);
input.addEventListener('change', updatePicker);
picker.addEventListener('input', updateText);
picker.addEventListener('change', updateText);
return node;
}
});
return view.extend({
load: function() {
return Promise.all([
uci.load('argon'),
callAvailSpace().catch(function(e) {
ui.addNotification(null, E('p', _('Failed to read available space: %s.').format(e.message)));
return { avail: 0 };
}),
fs.list(bg_path).then(function(files) {
return files.filter(function(file) {
return file.type === 'file' && /\.(jpe?g|png|gif|webp|mp4|webm)$/i.test(file.name);
});
}).catch(function(e) {
ui.addNotification(null, E('p', _('Failed to list background files: %s.').format(e.message)));
return [];
}),
branding.load()
]);
},
render: function(data) {
var m, s, o;
m = new form.Map('argon', _('Argon theme configuration'),
_('Here you can set the blur and transparency of the login page of argon theme, and manage the background pictures and videos. Chrome is recommended.'));
s = m.section(form.TypedSection, 'global', _('Theme configuration'));
s.addremove = false;
s.anonymous = true;
var loginStyles = {
classic: _('Classic sidebar'),
centered: _('Centered card')
};
o = s.option(form.ListValue, 'login_style', _('Login page style'),
_('Applies the next time you open the login page.'));
o.value('classic', loginStyles.classic);
o.value('centered', loginStyles.centered);
o.default = 'classic';
o.rmempty = false;
o.renderWidget = function(section_id, option_index, cfgvalue) {
var style = cfgvalue === 'centered' ? 'centered' : 'classic';
return E('div', {}, [
form.ListValue.prototype.renderWidget.call(this, section_id, option_index, style),
E('img', {
'id': this.cbid(section_id) + '-preview',
'src': L.resource('argon-config/login-' + style + '.svg'),
'alt': loginStyles[style],
'width': 320,
'height': 180,
'class': 'argon-login-preview'
})
]);
};
o.onchange = function(ev, section_id, value) {
var preview = document.getElementById(this.cbid(section_id) + '-preview');
var style = value === 'centered' ? 'centered' : 'classic';
if (preview) {
preview.src = L.resource('argon-config/login-' + style + '.svg');
preview.alt = loginStyles[style];
}
};
o = s.option(form.ListValue, 'online_wallpaper', _('Wallpaper source'));
o.value('none', _('Built-in'));
o.value('bing', _('Bing'));
o.value('unsplash', _('Unsplash'));
o.value('wallhaven', _('Wallhaven'));
o.default = 'none';
o.rmempty = false;
o = s.option(form.Value, 'use_api_key', _('Unsplash API access key'),
_('Required when using Unsplash wallpapers.'));
o.password = true;
o.depends('online_wallpaper', 'unsplash');
o.rmempty = true;
o = s.option(form.ListValue, 'mode', _('Theme mode'));
o.value('normal', _('Follow system'));
o.value('light', _('Light mode'));
o.value('dark', _('Dark mode'));
o.default = 'normal';
o.rmempty = false;
// A header shortcut saves independently of this form. Keep the widget and
// its comparison value current, while leaving other pending fields alone.
o.renderWidget = function(section_id, option_index, cfgvalue) {
var option = this;
var node = form.ListValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
var syncMode = function(event) {
if (!node.isConnected) {
window.removeEventListener('argon-appearance-saved', syncMode);
return;
}
option.cfgvalue(section_id, event.detail.mode);
L.dom.callClassMethod(node, 'setValue', event.detail.mode);
};
window.addEventListener('argon-appearance-saved', syncMode);
return node;
};
o = s.option(ColorValue, 'primary', _('Primary color'), _('A HEX color (default: #5e72e4).'));
o.default = '#5e72e4';
o.rmempty = false;
o = s.option(form.ListValue, 'transparency', _('Login panel opacity'),
_('0 transparent - 1 opaque (suggest: transparent: 0 or translucent preset: 0.5).'));
for (var i of trans_set)
o.value(i);
o.default = '0.5';
o.rmempty = false;
o = s.option(form.Value, 'blur', _('Login panel blur'),
_('Larger value will more blurred (suggest: clear: 1 or blur preset: 10).'));
o.datatype = 'and(ufloat,range(0,100))';
o.default = '10';
o.rmempty = false;
o = s.option(form.ListValue, 'dark_primary_mode', _('Dark mode colors'),
_('Automatically generate dark colors from the primary color, or choose a custom dark color.'));
o.value('auto', _('Automatic'));
o.value('custom', _('Custom'));
o.default = 'auto';
o.rmempty = false;
o.cfgvalue = function(section_id) {
var mode = uci.get('argon', section_id, 'dark_primary_mode');
return mode === 'auto' || mode === 'custom' ? mode :
(expandHex(uci.get('argon', section_id, 'dark_primary')) ? 'custom' : 'auto');
};
o = s.option(ColorValue, 'dark_primary', _('[Dark mode] Primary Color'),
_('A HEX Color (default: #483d8b).'));
o.default = '#483d8b';
o.rmempty = false;
o.depends('dark_primary_mode', 'custom');
o.retain = true;
o = s.option(form.ListValue, 'dark_glass_mode', _('Dark mode login panel'),
_('Use the same opacity and blur in both modes, or customize the dark login panel.'));
o.value('inherit', _('Follow light mode'));
o.value('custom', _('Custom'));
o.default = 'inherit';
o.rmempty = false;
o.cfgvalue = function(section_id) {
var mode = uci.get('argon', section_id, 'dark_glass_mode');
if (mode === 'inherit' || mode === 'custom')
return mode;
var blur = uci.get('argon', section_id, 'blur') || '10';
var opacity = uci.get('argon', section_id, 'transparency') || '0.5';
var darkBlur = uci.get('argon', section_id, 'blur_dark');
var darkOpacity = uci.get('argon', section_id, 'transparency_dark');
return (darkBlur != null && +darkBlur !== +blur) ||
(darkOpacity != null && +darkOpacity !== +opacity) ? 'custom' : 'inherit';
};
o = s.option(form.ListValue, 'transparency_dark', _('[Dark mode] Transparency'),
_('0 transparent - 1 opaque (suggest: black translucent preset: 0.5).'));
for (var i of trans_set)
o.value(i);
o.default = '0.5';
o.rmempty = false;
o.depends('dark_glass_mode', 'custom');
o.retain = true;
o = s.option(form.Value, 'blur_dark', _('[Dark mode] Frosted Glass Radius'),
_('Larger value will more blurred (suggest: clear: 1 or blur preset: 10).'));
o.datatype = 'and(ufloat,range(0,100))';
o.default = '10';
o.rmempty = false;
o.depends('dark_glass_mode', 'custom');
o.retain = true;
o = s.option(form.Button, '_save', _('Save settings'));
o.inputstyle = 'apply';
o.inputtitle = _('Save current settings');
o.onclick = function() {
if (this.map.readonly)
return Promise.resolve();
return this.map.save(null, true).then(function() {
return ui.changes.apply(true);
}).catch(function(e) {
ui.addNotification(null, E('p', e.message));
});
};
s = m.section(form.TypedSection);
s.render = function() { return branding.render(this.map, data[3]); };
s = m.section(form.TypedSection, null, _('Upload background (available space: %1024.2mB)')
.format(data[1].avail * 1024),
_('You can upload files such as gif/jpg/mp4/png/webm/webp files, to change the login page background.'));
s.addremove = false;
s.anonymous = true;
o = s.option(form.Button, '_upload_bg', _('Upload background'),
_('Files will be uploaded to <code>%s</code>.').format(bg_path));
o.inputstyle = 'action';
o.inputtitle = _('Upload...');
o.onclick = function(ev, section_id) {
if (this.map.readonly)
return Promise.resolve();
var file = '/tmp/argon_background.tmp';
var uploaded = false;
return ui.uploadFile(file, ev.target).then(function(res) {
uploaded = true;
return callRenameArgon(res.name).then(function(ret) {
if (ret.result === 0)
return location.reload();
throw new Error(_('Failed to upload file: %s.').format(res.name));
});
})
.catch(function(e) {
ui.addNotification(null, E('p', e.message));
if (uploaded)
return L.resolveDefault(fs.remove(file));
});
};
o.modalonly = true;
s = m.section(form.TableSection);
s.render = function() {
var tbl = E('table', { 'class': 'table cbi-section-table' },
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, [ _('Filename') ]),
E('th', { 'class': 'th' }, [ _('Modified date') ]),
E('th', { 'class': 'th' }, [ _('Size') ]),
E('th', { 'class': 'th' }, [ _('Action') ])
])
);
cbi_update_table(tbl, data[2].map(L.bind(function(file) {
return [
file.name,
new Date(file.mtime * 1000).toLocaleString(),
String.format('%1024.2mB', file.size),
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'disabled': this.map.readonly || null,
'click': ui.createHandlerFn(this, function() {
if (this.map.readonly)
return Promise.resolve();
return callRemoveArgon(file.name).then(function(ret) {
if (ret.result === 0)
return location.reload();
throw new Error(_('Failed to delete file: %s.').format(file.name));
}).catch(function(e) {
ui.addNotification(null, E('p', e.message));
});
})
}, [ _('Delete') ])
];
}, this)), E('em', _('No files found.')));
return E('div', { 'class': 'cbi-section', 'id': 'cbi-filelist' }, [
E('h3', _('Background file list')),
tbl
]);
};
return m.render();
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});