mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 12:43:56 +08:00
💋 Sync 2026-10-16 23:55:30
This commit is contained in:
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
|
||||
});
|
||||
Reference in new issue
Block a user