mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 17:53:56 +08:00
🎉 Sync 2026-10-01 14:45:51
This commit is contained in:
1 parent
f1518a803c
commit
620e59fee5
124 files changed
+21594
-1
No files matched your search
Whitespace-only changes.
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,339 @@
|
||||
/* ==========================================================================
|
||||
Dark Mode — luci-theme-glass
|
||||
Compiled separately and injected by header.ut when dark mode is active.
|
||||
========================================================================== */
|
||||
:root {
|
||||
/* ---- Colors — Dark mode ---- */
|
||||
--color-primary: #0A84FF;
|
||||
--color-primary-hover: #409cff;
|
||||
--color-primary-active: #0066d6;
|
||||
--color-primary-rgb: 10, 132, 255;
|
||||
--color-success: #30D158;
|
||||
--color-warning: #FFD60A;
|
||||
--color-danger: #FF453A;
|
||||
--color-info: #64D2FF;
|
||||
--color-text: #f5f5f7;
|
||||
--color-text-secondary: #98989d;
|
||||
--color-text-tertiary: #636366;
|
||||
--color-text-inverse: #1d1d1f;
|
||||
--color-bg: #1c1c1e;
|
||||
--color-bg-elevated: #2c2c2e;
|
||||
--color-bg-secondary: #3a3a3c;
|
||||
--color-border: rgba(255, 255, 255, 0.08);
|
||||
--color-border-strong: rgba(255, 255, 255, 0.16);
|
||||
--color-separator: rgba(84, 84, 88, 0.36);
|
||||
/* ---- Dark glassmorphism ---- */
|
||||
--glass-blur: 25px;
|
||||
--glass-saturate: 180%;
|
||||
--glass-bg: rgba(38, 38, 42, 0.55);
|
||||
--glass-bg-hover: rgba(52, 52, 56, 0.65);
|
||||
--glass-bg-strong: rgba(38, 38, 42, 0.72);
|
||||
--glass-border: rgba(255, 255, 255, 0.12);
|
||||
--glass-shadow: 0 2px 16px rgba(0, 0, 0, 0.2), 0 0 1px rgba(0, 0, 0, 0.25);
|
||||
--glass-shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.15);
|
||||
/* ---- Dark background ---- */
|
||||
--bg-gradient: linear-gradient(135deg, #2c2e3a 0%, #353848 50%, #2a2d38 100%);
|
||||
}
|
||||
/* ---- Dark-mode-specific overrides ---- */
|
||||
/* Inputs need darker backgrounds */
|
||||
.cbi-input-text,
|
||||
.cbi-input-password,
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="number"],
|
||||
textarea,
|
||||
select {
|
||||
background: rgba(44, 44, 46, 0.55);
|
||||
}
|
||||
/* Login card */
|
||||
.login-card {
|
||||
background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
|
||||
background-color: rgba(38, 38, 42, 0.55);
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25), 0 2px 8px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 16px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.login-form .login-field input {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 4px rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.login-btn {
|
||||
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.06) 25%, rgba(240, 248, 255, 0.07) 50%, rgba(255, 255, 255, 0.05) 75%, rgba(230, 240, 255, 0.06) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.15), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 0 12px rgba(255, 255, 255, 0.15);
|
||||
color: var(--color-text);
|
||||
}
|
||||
/* Buttons — glass-ui dark pattern */
|
||||
.btn,
|
||||
.cbi-button {
|
||||
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.06) 25%, rgba(240, 248, 255, 0.07) 50%, rgba(255, 255, 255, 0.05) 75%, rgba(230, 240, 255, 0.06) 100%);
|
||||
background-color: transparent;
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.15), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 0 12px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.btn:hover,
|
||||
.cbi-button:hover {
|
||||
opacity: 0.9;
|
||||
background-color: transparent;
|
||||
}
|
||||
.btn:active,
|
||||
.cbi-button:active {
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.cbi-button-negative,
|
||||
.cbi-button-remove,
|
||||
.cbi-button-reset {
|
||||
background-image: linear-gradient(135deg, rgba(255, 69, 58, 0.12) 0%, rgba(255, 69, 58, 0.08) 50%, rgba(255, 69, 58, 0.1) 100%);
|
||||
background-color: transparent;
|
||||
color: var(--color-danger);
|
||||
border-color: rgba(255, 69, 58, 0.25);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.15), inset 0 0 0.5px rgba(255, 255, 255, 0.08), inset 0 0 8px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.cbi-button-negative:hover,
|
||||
.cbi-button-remove:hover,
|
||||
.cbi-button-reset:hover {
|
||||
opacity: 0.9;
|
||||
background-color: transparent;
|
||||
}
|
||||
.cbi-button-negative:active,
|
||||
.cbi-button-remove:active,
|
||||
.cbi-button-reset:active {
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
/* Sidebar */
|
||||
#sidebar {
|
||||
background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 20%, rgba(240, 248, 255, 0.08) 45%, rgba(255, 255, 255, 0.06) 70%, rgba(230, 240, 255, 0.07) 100%);
|
||||
background-color: rgba(38, 38, 42, 0.55);
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25), 0 2px 8px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 12px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
/* Header bar */
|
||||
#header {
|
||||
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 8px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
/* Page actions bar */
|
||||
.cbi-page-actions {
|
||||
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 8px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
/* Nav hover & active */
|
||||
.nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.nav-item.active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: var(--color-text);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.12), inset 0 0 8px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
/* Sub-item active — slider handles highlight */
|
||||
.nav-sub .nav-item.active {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
/* Sliding selector — dark glass */
|
||||
.nav-sub .nav-slider {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 0.5px solid rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(12px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(150%);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2), inset 0 1px 1px rgba(255, 255, 255, 0.08), inset 0 0 4px rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
/* Header tabs */
|
||||
.header-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.header-tab.active {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
/* Tab slider — dark glass */
|
||||
.tab-slider {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 0.5px solid rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.1), inset 0 0 8px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
/* Sub-nav bar */
|
||||
#header-sub {
|
||||
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), 0 0 0 0.5px rgba(255, 255, 255, 0.12) inset, 0 0 12px rgba(255, 255, 255, 0.15) inset;
|
||||
}
|
||||
.sub-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.sub-tab.active {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
/* Header glass pills */
|
||||
.header-title,
|
||||
#status-indicators,
|
||||
.header-actions {
|
||||
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.02) 50%, rgba(255, 255, 255, 0.03) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.12), inset 0 0.5px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
/* Theme toggle */
|
||||
.theme-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.theme-toggle-float {
|
||||
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18), 0 1px 2px rgba(0, 0, 0, 0.08), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 1px 6px rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
/* Tabs */
|
||||
.cbi-tabmenu {
|
||||
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 0 12px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.cbi-tabmenu li a:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.cbi-tabmenu li.cbi-tab a {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.12), inset 0 0 8px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
/* Section fold button */
|
||||
.cbi-section.fade-in .cbi-title > div:last-child span:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
/* Table row hover */
|
||||
table tbody tr:hover td,
|
||||
.table .tr:hover .td {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
/* Pre / code */
|
||||
pre,
|
||||
#syslog {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
/* Alert colors adjusted for dark */
|
||||
.alert-message,
|
||||
.cbi-section-error {
|
||||
background: rgba(100, 210, 255, 0.08);
|
||||
border-color: rgba(100, 210, 255, 0.18);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 6px rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.alert-message.notice,
|
||||
.alert-message.info {
|
||||
background: rgba(100, 210, 255, 0.1);
|
||||
border-color: rgba(100, 210, 255, 0.25);
|
||||
}
|
||||
.alert-message.warning {
|
||||
background: rgba(255, 214, 10, 0.1);
|
||||
border-color: rgba(255, 214, 10, 0.25);
|
||||
}
|
||||
.alert-message.error,
|
||||
.cbi-section-error {
|
||||
background: rgba(255, 69, 58, 0.1);
|
||||
border-color: rgba(255, 69, 58, 0.25);
|
||||
}
|
||||
.alert-message.success {
|
||||
background: rgba(48, 209, 88, 0.1);
|
||||
border-color: rgba(48, 209, 88, 0.25);
|
||||
}
|
||||
/* Diagnostics & file browser in dark */
|
||||
#diag-rc-output,
|
||||
.cbi-filebrowser {
|
||||
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18), 0 1px 2px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 6px rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
/* Ifacebox zone header in dark — slightly brighter tint */
|
||||
.ifacebox-head[style*="background-color"] {
|
||||
background-color: rgba(var(--zone-color-rgb), 0.2) !important;
|
||||
border-bottom-color: rgba(var(--zone-color-rgb), 0.25) !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
/* Zone badges in dark — tint zone color instead of opaque background */
|
||||
.zonebadge[style*="background-color"] {
|
||||
background-color: rgba(var(--zone-color-rgb), 0.25) !important;
|
||||
border-color: rgba(var(--zone-color-rgb), 0.3);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 0 0 4px rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
.zonebadge {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 0 0 4px rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
/* Ifacebox in dark */
|
||||
.ifacebadge {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 0 0 4px rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
/* Port grid boxes in dark — glass-ui dark card */
|
||||
div[style*="display:grid;grid-template-columns:repeat"] > .ifacebox {
|
||||
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%) !important;
|
||||
border-color: rgba(255, 255, 255, 0.15) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), 0 0 0 0.5px rgba(255, 255, 255, 0.12) inset, 0 0 12px rgba(255, 255, 255, 0.15) inset !important;
|
||||
}
|
||||
/* Network status table badges in dark */
|
||||
.network-status-table .ifacebox-body .ifacebadge {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
/* Dropdown open items hover */
|
||||
.cbi-dropdown[open] > ul.dropdown > li:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
/* Progressbar */
|
||||
.cbi-progressbar {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.cbi-progressbar > div {
|
||||
background: linear-gradient(135deg, rgba(var(--color-primary-rgb), 0.65) 0%, rgba(var(--color-primary-rgb), 0.5) 50%, rgba(var(--color-primary-rgb), 0.6) 100%);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), inset 0 0 6px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.cbi-progressbar::after {
|
||||
color: var(--color-text);
|
||||
text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
/* Ifacebox-body itemlist labels */
|
||||
.ifacebox-body > span > .nowrap > strong {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
/* UCI Changes dialog in dark */
|
||||
.uci-change-legend-label > ins {
|
||||
background-color: rgba(48, 209, 88, 0.3);
|
||||
border-color: rgba(48, 209, 88, 0.5);
|
||||
}
|
||||
.uci-change-legend-label > del {
|
||||
background-color: rgba(255, 69, 58, 0.3);
|
||||
border-color: rgba(255, 69, 58, 0.5);
|
||||
}
|
||||
.uci-change-legend-label > var {
|
||||
background-color: rgba(100, 210, 255, 0.25);
|
||||
border-color: rgba(100, 210, 255, 0.4);
|
||||
}
|
||||
.uci-change-list ins {
|
||||
background-color: rgba(48, 209, 88, 0.12);
|
||||
border-color: rgba(48, 209, 88, 0.25);
|
||||
}
|
||||
.uci-change-list del {
|
||||
background-color: rgba(255, 69, 58, 0.12);
|
||||
border-color: rgba(255, 69, 58, 0.25);
|
||||
}
|
||||
.uci-change-list var {
|
||||
background-color: rgba(100, 210, 255, 0.1);
|
||||
border-color: rgba(100, 210, 255, 0.18);
|
||||
}
|
||||
/* Selection */
|
||||
::selection {
|
||||
background: rgba(var(--color-primary-rgb), 0.35);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#F5F5F7"/>
|
||||
<stop offset="100%" stop-color="#E8E8ED"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="glass" x1="4" y1="4" x2="60" y2="60" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="white" stop-opacity="0.9"/>
|
||||
<stop offset="40%" stop-color="white" stop-opacity="0.4"/>
|
||||
<stop offset="100%" stop-color="white" stop-opacity="0.6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="edge" x1="4" y1="4" x2="60" y2="60" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="white" stop-opacity="0.95"/>
|
||||
<stop offset="50%" stop-color="#D1D1D6" stop-opacity="0.4"/>
|
||||
<stop offset="100%" stop-color="white" stop-opacity="0.7"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="refract" cx="18" cy="16" r="36" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="white" stop-opacity="0.55"/>
|
||||
<stop offset="100%" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
<!-- 3D shadow filter for WiFi arcs -->
|
||||
<filter id="arc-shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="1.5" stdDeviation="1.5" flood-color="#002266" flood-opacity="0.3"/>
|
||||
</filter>
|
||||
|
||||
<!-- Arc stroke gradients — darker Apple blue with depth -->
|
||||
<linearGradient id="arc1" x1="14" y1="26" x2="50" y2="30" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#3A8EF6" stop-opacity="0.3"/>
|
||||
<stop offset="20%" stop-color="#0040DD" stop-opacity="0.9"/>
|
||||
<stop offset="50%" stop-color="#0033BB" stop-opacity="1"/>
|
||||
<stop offset="80%" stop-color="#0040DD" stop-opacity="0.9"/>
|
||||
<stop offset="100%" stop-color="#3A8EF6" stop-opacity="0.3"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="arc2" x1="20" y1="32" x2="44" y2="36" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#3A8EF6" stop-opacity="0.25"/>
|
||||
<stop offset="25%" stop-color="#0040DD" stop-opacity="0.8"/>
|
||||
<stop offset="50%" stop-color="#0033BB" stop-opacity="0.95"/>
|
||||
<stop offset="75%" stop-color="#0040DD" stop-opacity="0.8"/>
|
||||
<stop offset="100%" stop-color="#3A8EF6" stop-opacity="0.25"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="arc3" x1="26" y1="38" x2="38" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#4D9BF7" stop-opacity="0.2"/>
|
||||
<stop offset="30%" stop-color="#0040DD" stop-opacity="0.7"/>
|
||||
<stop offset="50%" stop-color="#0033BB" stop-opacity="0.9"/>
|
||||
<stop offset="70%" stop-color="#0040DD" stop-opacity="0.7"/>
|
||||
<stop offset="100%" stop-color="#4D9BF7" stop-opacity="0.2"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="dot-grad" cx="32" cy="46" r="3.5" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#0033BB"/>
|
||||
<stop offset="60%" stop-color="#0040DD"/>
|
||||
<stop offset="100%" stop-color="#3A8EF6" stop-opacity="0.4"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Outer shadow for 3D depth -->
|
||||
<rect width="64" height="64" rx="14" fill="none" filter="url(#arc-shadow)"/>
|
||||
|
||||
<!-- Light background -->
|
||||
<rect width="64" height="64" rx="14" fill="url(#bg)"/>
|
||||
|
||||
<!-- Clean glass pane -->
|
||||
<rect x="4" y="4" width="56" height="56" rx="12" fill="url(#glass)" stroke="url(#edge)" stroke-width="0.75"/>
|
||||
|
||||
<!-- Light refraction highlight -->
|
||||
<rect x="4" y="4" width="56" height="28" rx="12" fill="url(#refract)"/>
|
||||
|
||||
<!-- Inner glow edge -->
|
||||
<rect x="5" y="5" width="54" height="54" rx="11" fill="none" stroke="white" stroke-opacity="0.35" stroke-width="0.5"/>
|
||||
|
||||
<!-- WiFi shadow layer (dark offset below — 3D depth) -->
|
||||
<path d="M14 30a25.5 25.5 0 0 1 36 0" fill="none" stroke="#002266" stroke-opacity="0.18" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M20 36a16 16 0 0 1 24 0" fill="none" stroke="#002266" stroke-opacity="0.15" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M26 42a7.5 7.5 0 0 1 12 0" fill="none" stroke="#002266" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
|
||||
|
||||
<!-- WiFi highlight layer (white offset above — raised/embossed) -->
|
||||
<path d="M14 27a25.5 25.5 0 0 1 36 0" fill="none" stroke="white" stroke-opacity="0.65" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M20 33a16 16 0 0 1 24 0" fill="none" stroke="white" stroke-opacity="0.6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M26 39a7.5 7.5 0 0 1 12 0" fill="none" stroke="white" stroke-opacity="0.55" stroke-width="2" stroke-linecap="round"/>
|
||||
|
||||
<!-- WiFi main arcs — darker blue gradient -->
|
||||
<path d="M14 28a25.5 25.5 0 0 1 36 0" fill="none" stroke="url(#arc1)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M20 34a16 16 0 0 1 24 0" fill="none" stroke="url(#arc2)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M26 40a7.5 7.5 0 0 1 12 0" fill="none" stroke="url(#arc3)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
|
||||
<!-- Center dot with 3D -->
|
||||
<circle cx="32" cy="48" r="2.5" fill="#002266" fill-opacity="0.15"/>
|
||||
<circle cx="32" cy="46" r="2.5" fill="url(#dot-grad)"/>
|
||||
<circle cx="31.2" cy="45.2" r="0.9" fill="white" fill-opacity="0.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,327 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require ui';
|
||||
|
||||
return baseclass.extend({
|
||||
__init__: function() {
|
||||
ui.menu.load().then(L.bind(this.render, this));
|
||||
},
|
||||
|
||||
/* Icon SVG paths for top-level sidebar categories (stroke-based, 24x24 viewBox) */
|
||||
icons: {
|
||||
'status': '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
|
||||
'system': '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09a1.65 1.65 0 00-1-1.51 1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09a1.65 1.65 0 001.51-1 1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06a1.65 1.65 0 001.82.33h.09a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82v.09a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/>',
|
||||
'network': '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>',
|
||||
'services': '<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>',
|
||||
'nas': '<line x1="22" y1="12" x2="2" y2="12"/><path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="16" x2="10.01" y2="16"/>',
|
||||
'vpn': '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/>',
|
||||
'docker': '<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
|
||||
'logout': '<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>',
|
||||
'_default': '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>'
|
||||
},
|
||||
|
||||
getIcon: function(name) {
|
||||
var pathData = this.icons[name] || this.icons['_default'];
|
||||
var markup = '<svg xmlns="http://www.w3.org/2000/svg" class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">' + pathData + '</svg>';
|
||||
var doc = new DOMParser().parseFromString(markup, 'image/svg+xml');
|
||||
return document.importNode(doc.documentElement, true);
|
||||
},
|
||||
|
||||
render: function(tree) {
|
||||
var container = document.getElementById('sidebar-nav');
|
||||
if (!container) return;
|
||||
|
||||
/* Clear existing menu elements to prevent duplicates on re-render (e.g. Save & Apply) */
|
||||
container.innerHTML = '';
|
||||
|
||||
var headerNav = document.getElementById('header-nav');
|
||||
if (headerNav) {
|
||||
/* Remove only tabs and sliders, preserve header-title */
|
||||
var old = headerNav.querySelectorAll('.header-tab, .tab-slider');
|
||||
for (var k = 0; k < old.length; k++) old[k].remove();
|
||||
}
|
||||
|
||||
var headerTabs = document.getElementById('header-tabs');
|
||||
if (headerTabs) headerTabs.innerHTML = '';
|
||||
|
||||
var topChildren = ui.menu.getChildren(tree);
|
||||
|
||||
for (var i = 0; i < topChildren.length; i++) {
|
||||
var topNode = topChildren[i];
|
||||
var isTopActive = L.env.requestpath.length
|
||||
? topNode.name == L.env.requestpath[0]
|
||||
: i == 0;
|
||||
|
||||
if (isTopActive) {
|
||||
this.renderSidebarMenu(container, topNode, topNode.name);
|
||||
}
|
||||
}
|
||||
|
||||
/* Render mode menu (top-level tabs) if more than one category */
|
||||
if (topChildren.length > 1) {
|
||||
this.renderModeMenu(topChildren);
|
||||
}
|
||||
|
||||
/* Set header title from the deepest active menu node */
|
||||
var titleEl = document.getElementById('header-title');
|
||||
if (titleEl) {
|
||||
var node = tree;
|
||||
var title = '';
|
||||
for (var i = 0; i < L.env.dispatchpath.length && node; i++) {
|
||||
node = node.children[L.env.dispatchpath[i]];
|
||||
if (node && node.title)
|
||||
title = node.title;
|
||||
}
|
||||
titleEl.textContent = title ? _(title) : '';
|
||||
}
|
||||
|
||||
/* Render tab menu for sub-sub pages */
|
||||
if (L.env.dispatchpath.length >= 3) {
|
||||
var node = tree;
|
||||
var url = '';
|
||||
for (var i = 0; i < 3 && node; i++) {
|
||||
node = node.children[L.env.dispatchpath[i]];
|
||||
url = url + (url ? '/' : '') + L.env.dispatchpath[i];
|
||||
}
|
||||
if (node)
|
||||
this.renderTabMenu(node, url);
|
||||
}
|
||||
},
|
||||
|
||||
/* Shared: position a slider behind the active tab element */
|
||||
positionTabSlider: function(container, slider, activeEl, animate) {
|
||||
if (!activeEl) return;
|
||||
if (!animate) slider.style.transition = 'none';
|
||||
slider.style.left = activeEl.offsetLeft + 'px';
|
||||
slider.style.width = activeEl.offsetWidth + 'px';
|
||||
slider.style.opacity = '1';
|
||||
if (!animate) {
|
||||
requestAnimationFrame(function() {
|
||||
slider.style.transition = '';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/* Shared: add slider to a horizontal tab container */
|
||||
addTabSlider: function(container, navigateOnClick) {
|
||||
var slider = E('div', { 'class': 'tab-slider' });
|
||||
container.insertBefore(slider, container.firstChild);
|
||||
var self = this;
|
||||
|
||||
/* Attach click handlers to all tabs */
|
||||
var tabs = container.querySelectorAll('.header-tab, .sub-tab');
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
(function(tab, sl) {
|
||||
tab.addEventListener('click', function(ev) {
|
||||
if (navigateOnClick) {
|
||||
/* Slide first, then navigate */
|
||||
ev.preventDefault();
|
||||
var prev = container.querySelector('.header-tab.active, .sub-tab.active');
|
||||
if (prev) prev.classList.remove('active');
|
||||
tab.classList.add('active');
|
||||
self.positionTabSlider(container, sl, tab, true);
|
||||
document.body.classList.add('page-leaving');
|
||||
var href = tab.href;
|
||||
setTimeout(function() {
|
||||
window.location.href = href;
|
||||
}, 180);
|
||||
} else {
|
||||
/* In-page tab switch — just move slider */
|
||||
self.positionTabSlider(container, sl, tab, true);
|
||||
}
|
||||
});
|
||||
})(tabs[i], slider);
|
||||
}
|
||||
|
||||
/* Position on active tab */
|
||||
requestAnimationFrame(function() {
|
||||
var active = container.querySelector('.header-tab.active, .sub-tab.active');
|
||||
self.positionTabSlider(container, slider, active, false);
|
||||
});
|
||||
|
||||
return slider;
|
||||
},
|
||||
|
||||
renderModeMenu: function(topChildren) {
|
||||
var container = document.getElementById('header-nav');
|
||||
if (!container) return;
|
||||
|
||||
for (var i = 0; i < topChildren.length; i++) {
|
||||
var child = topChildren[i];
|
||||
var isActive = L.env.requestpath.length
|
||||
? child.name == L.env.requestpath[0]
|
||||
: i == 0;
|
||||
|
||||
var item = E('a', {
|
||||
'class': 'header-tab' + (isActive ? ' active' : ''),
|
||||
'href': L.url(child.name),
|
||||
'data-name': child.name
|
||||
}, [ _(child.title) ]);
|
||||
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
/* Add sliding selector */
|
||||
this.addTabSlider(container, true);
|
||||
},
|
||||
|
||||
renderSidebarMenu: function(container, tree, url) {
|
||||
var children = ui.menu.getChildren(tree);
|
||||
if (children.length == 0) return;
|
||||
|
||||
var group = E('div', { 'class': 'nav-group' });
|
||||
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var child = children[i];
|
||||
var subChildren = ui.menu.getChildren(child);
|
||||
var hasChildren = subChildren.length > 0;
|
||||
|
||||
var isActive = L.env.dispatchpath[1] == child.name;
|
||||
|
||||
/* For items with children, link to first child */
|
||||
var linkUrl = hasChildren
|
||||
? L.url(url, child.name, subChildren[0].name)
|
||||
: L.url(url, child.name);
|
||||
|
||||
var item = E('a', {
|
||||
'class': 'nav-item' + (isActive ? ' open' : ''),
|
||||
'href': linkUrl,
|
||||
'data-name': child.name
|
||||
}, [
|
||||
E('span', { 'class': 'nav-label' }, [ _(child.title) ])
|
||||
]);
|
||||
|
||||
/* Add icon before the label */
|
||||
item.insertBefore(this.getIcon(child.name), item.firstChild);
|
||||
|
||||
if (hasChildren) {
|
||||
item.appendChild(E('svg', {
|
||||
'class': 'nav-arrow',
|
||||
'viewBox': '0 0 24 24',
|
||||
'fill': 'none',
|
||||
'stroke': 'currentColor',
|
||||
'stroke-width': '2'
|
||||
}, [
|
||||
E('polyline', { 'points': '9 18 15 12 9 6' })
|
||||
]));
|
||||
|
||||
}
|
||||
|
||||
group.appendChild(item);
|
||||
|
||||
if (hasChildren) {
|
||||
var subMenu = E('div', {
|
||||
'class': 'nav-sub' + (isActive ? ' open' : '')
|
||||
});
|
||||
|
||||
/* Sliding highlight indicator */
|
||||
var slider = E('div', { 'class': 'nav-slider' });
|
||||
subMenu.appendChild(slider);
|
||||
|
||||
for (var j = 0; j < subChildren.length; j++) {
|
||||
var sub = subChildren[j];
|
||||
var isSubActive = isActive && L.env.dispatchpath[2] == sub.name;
|
||||
|
||||
var subItem = E('a', {
|
||||
'class': 'nav-item' + (isSubActive ? ' active' : ''),
|
||||
'href': L.url(url, child.name, sub.name)
|
||||
}, [
|
||||
E('span', { 'class': 'nav-label' }, [ _(sub.title) ])
|
||||
]);
|
||||
|
||||
/* Slide highlight to clicked item before navigating */
|
||||
(function(el, sl) {
|
||||
el.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
var prev = el.parentNode.querySelector('.nav-item.active');
|
||||
if (prev) prev.classList.remove('active');
|
||||
el.classList.add('active');
|
||||
sl.style.top = el.offsetTop + 'px';
|
||||
sl.style.height = el.offsetHeight + 'px';
|
||||
sl.style.opacity = '1';
|
||||
document.body.classList.add('page-leaving');
|
||||
setTimeout(function() {
|
||||
window.location.href = el.href;
|
||||
}, 180);
|
||||
});
|
||||
})(subItem, slider);
|
||||
|
||||
subMenu.appendChild(subItem);
|
||||
}
|
||||
|
||||
/* Position slider on the active item after render */
|
||||
(function(sm, sl) {
|
||||
requestAnimationFrame(function() {
|
||||
var active = sm.querySelector('.nav-item.active');
|
||||
if (active) {
|
||||
sl.style.transition = 'none';
|
||||
sl.style.top = active.offsetTop + 'px';
|
||||
sl.style.height = active.offsetHeight + 'px';
|
||||
sl.style.opacity = '1';
|
||||
/* Re-enable transition after initial position */
|
||||
requestAnimationFrame(function() {
|
||||
sl.style.transition = '';
|
||||
});
|
||||
}
|
||||
});
|
||||
})(subMenu, slider);
|
||||
|
||||
group.appendChild(subMenu);
|
||||
|
||||
/* Bind click handler to toggle submenu */
|
||||
(function(navItem, subEl) {
|
||||
navItem.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
var isOpen = subEl.classList.contains('open');
|
||||
if (isOpen) {
|
||||
subEl.classList.remove('open');
|
||||
navItem.classList.remove('open');
|
||||
} else {
|
||||
subEl.classList.add('open');
|
||||
navItem.classList.add('open');
|
||||
}
|
||||
});
|
||||
})(item, subMenu);
|
||||
}
|
||||
}
|
||||
|
||||
container.appendChild(group);
|
||||
},
|
||||
|
||||
renderTabMenu: function(tree, url, level) {
|
||||
var container = document.getElementById('header-sub');
|
||||
if (!container) return E([]);
|
||||
|
||||
var children = ui.menu.getChildren(tree);
|
||||
var activeNode = null;
|
||||
|
||||
if (children.length == 0)
|
||||
return E([]);
|
||||
|
||||
/* Clear any CBI tabs that footer.ut added before menu loaded */
|
||||
container.innerHTML = '';
|
||||
container.classList.remove('active');
|
||||
document.body.classList.remove('has-sub-nav');
|
||||
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var child = children[i];
|
||||
var isActive = L.env.dispatchpath[3 + (level || 0)] == child.name;
|
||||
|
||||
container.appendChild(E('a', {
|
||||
'class': 'sub-tab' + (isActive ? ' active' : ''),
|
||||
'href': L.url(url, child.name)
|
||||
}, [ _(child.title) ]));
|
||||
|
||||
if (isActive)
|
||||
activeNode = child;
|
||||
}
|
||||
|
||||
container.classList.add('active');
|
||||
document.body.classList.add('has-sub-nav');
|
||||
|
||||
/* Add sliding selector */
|
||||
this.addTabSlider(container, true);
|
||||
|
||||
/* Do NOT recurse — deeper menu-tree levels stay in-content as CBI tabmenus */
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
'require fs';
|
||||
|
||||
var callSystemInfo = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'info'
|
||||
});
|
||||
|
||||
var callInterfaceDump = rpc.declare({
|
||||
object: 'network.interface',
|
||||
method: 'dump',
|
||||
expect: { 'interface': [] }
|
||||
});
|
||||
|
||||
var callDeviceStatus = rpc.declare({
|
||||
object: 'network.device',
|
||||
method: 'status',
|
||||
params: ['name']
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
prevStats: null,
|
||||
prevTime: null,
|
||||
netDevice: null,
|
||||
netLabel: null,
|
||||
netChecked: false,
|
||||
linkSpeed: null,
|
||||
numCores: 1,
|
||||
|
||||
__init__: function() {
|
||||
var self = this;
|
||||
/* Detect core count once so load average can be converted to %. */
|
||||
L.resolveDefault(fs.read('/proc/cpuinfo'), '').then(function(text) {
|
||||
var m = text.match(/^processor\s*:/gm);
|
||||
self.numCores = (m && m.length) || 1;
|
||||
});
|
||||
this.setupIndicators();
|
||||
},
|
||||
|
||||
icons: {
|
||||
cpu: '<svg xmlns="http://www.w3.org/2000/svg" class="indicator-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 9 4 4 7 11 10 2 13 9"/></svg>',
|
||||
ram: '<svg xmlns="http://www.w3.org/2000/svg" class="indicator-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M1 3 H13 V10 H8 V9 H6 V10 H1 Z"/><rect x="2.2" y="4.5" width="2" height="3"/><rect x="6" y="4.5" width="2" height="3"/><rect x="9.8" y="4.5" width="2" height="3"/><line x1="2" y1="10" x2="2" y2="11.5"/><line x1="4" y1="10" x2="4" y2="11.5"/><line x1="10" y1="10" x2="10" y2="11.5"/><line x1="12" y1="10" x2="12" y2="11.5"/></svg>',
|
||||
net: '<svg xmlns="http://www.w3.org/2000/svg" class="indicator-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="1" x2="7" y2="13"/><polyline points="3 4 7 1 11 4"/><polyline points="3 10 7 13 11 10"/></svg>',
|
||||
uptime: '<svg xmlns="http://www.w3.org/2000/svg" class="indicator-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="7" cy="7" r="6"/><polyline points="7 3 7 7 10 9"/></svg>'
|
||||
},
|
||||
|
||||
makeIcon: function(name) {
|
||||
var markup = this.icons[name];
|
||||
if (!markup) return document.createTextNode('');
|
||||
var doc = new DOMParser().parseFromString(markup, 'image/svg+xml');
|
||||
return document.importNode(doc.documentElement, true);
|
||||
},
|
||||
|
||||
createIndicator: function(name, title) {
|
||||
var el = document.createElement('span');
|
||||
el.setAttribute('data-indicator', name);
|
||||
el.title = title;
|
||||
el.appendChild(this.makeIcon(name));
|
||||
var val = document.createElement('span');
|
||||
val.className = 'indicator-value';
|
||||
/* Restore last known value from session to avoid flash of "--" on page nav */
|
||||
var cached = null;
|
||||
try { cached = sessionStorage.getItem('glass-status-' + name); } catch(e) {}
|
||||
val.textContent = cached || '--';
|
||||
el.appendChild(val);
|
||||
return el;
|
||||
},
|
||||
|
||||
setLevel: function(el, level) {
|
||||
el.setAttribute('data-level', level);
|
||||
},
|
||||
|
||||
setupIndicators: function() {
|
||||
this.container = document.getElementById('status-indicators');
|
||||
if (!this.container) return;
|
||||
|
||||
this.cpuEl = this.createIndicator('cpu', 'CPU Load');
|
||||
this.ramEl = this.createIndicator('ram', 'Memory Usage');
|
||||
this.uptimeEl = this.createIndicator('uptime', 'Uptime');
|
||||
|
||||
this.container.appendChild(this.cpuEl);
|
||||
this.container.appendChild(this.ramEl);
|
||||
this.container.appendChild(this.uptimeEl);
|
||||
|
||||
this.fetchAndUpdate();
|
||||
setInterval(L.bind(this.fetchAndUpdate, this), 5000);
|
||||
},
|
||||
|
||||
fetchAndUpdate: function() {
|
||||
var self = this;
|
||||
|
||||
L.resolveDefault(callSystemInfo(), {}).then(function(info) {
|
||||
self.updateSystem(info);
|
||||
});
|
||||
|
||||
if (!this.netChecked) {
|
||||
this.netChecked = true;
|
||||
L.resolveDefault(callInterfaceDump(), []).then(function(ifaces) {
|
||||
/* Find WAN device — prefer 'wan' over 'wan6' */
|
||||
var wanDev = null;
|
||||
for (var i = 0; i < ifaces.length; i++) {
|
||||
var iface = ifaces[i];
|
||||
if (iface.interface === 'wan') {
|
||||
wanDev = iface.l3_device || iface.device || null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!wanDev) {
|
||||
for (var i = 0; i < ifaces.length; i++) {
|
||||
if (ifaces[i].interface === 'wan6') {
|
||||
wanDev = ifaces[i].l3_device || ifaces[i].device || null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.resolvePhysicalDevice(wanDev || 'wan');
|
||||
});
|
||||
} else if (this.netDevice) {
|
||||
this.pollNetwork();
|
||||
}
|
||||
},
|
||||
|
||||
resolvePhysicalDevice: function(devName) {
|
||||
var self = this;
|
||||
L.resolveDefault(callDeviceStatus(devName), {}).then(function(dev) {
|
||||
if (!dev) return;
|
||||
var members = dev['bridge-members'];
|
||||
if (dev.type === 'bridge' && members && members.length > 0) {
|
||||
/* Bridge stats only count management traffic;
|
||||
use first physical port for real forwarded stats */
|
||||
self.netDevice = members[0];
|
||||
} else if (dev.statistics) {
|
||||
self.netDevice = devName;
|
||||
}
|
||||
if (!self.netDevice) return;
|
||||
|
||||
/* Query physical port for link speed and DSA info */
|
||||
L.resolveDefault(callDeviceStatus(self.netDevice), {}).then(function(phys) {
|
||||
if (phys && phys.speed) {
|
||||
var m = String(phys.speed).match(/^(\d+)/);
|
||||
if (m) self.linkSpeed = parseInt(m[1], 10);
|
||||
}
|
||||
/* DSA port with hardware offload: per-port counters miss
|
||||
offloaded flows; use the conduit (switch master) instead */
|
||||
if (phys && phys.devtype === 'dsa' && phys['hw-tc-offload'] && phys.conduit) {
|
||||
self.netLabel = self.netDevice;
|
||||
self.netDevice = phys.conduit;
|
||||
}
|
||||
self.netEl = self.createIndicator('net', 'Throughput');
|
||||
self.container.insertBefore(self.netEl, self.uptimeEl);
|
||||
self.pollNetwork();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
pollNetwork: function() {
|
||||
var self = this;
|
||||
L.resolveDefault(callDeviceStatus(this.netDevice), {}).then(function(dev) {
|
||||
self.updateNetwork(dev);
|
||||
});
|
||||
},
|
||||
|
||||
updateSystem: function(info) {
|
||||
if (!info) return;
|
||||
|
||||
if (info.load) {
|
||||
var load1 = info.load[0] / 65536;
|
||||
var load5 = (info.load[1] / 65536).toFixed(2);
|
||||
var load15 = (info.load[2] / 65536).toFixed(2);
|
||||
var pct = Math.min(load1 / this.numCores, 1) * 100;
|
||||
var pctStr = pct.toFixed(0) + '%';
|
||||
|
||||
this.cpuEl.querySelector('.indicator-value').textContent = pctStr;
|
||||
try { sessionStorage.setItem('glass-status-cpu', pctStr); } catch(e) {}
|
||||
this.cpuEl.title = 'CPU: ' + pctStr + ' (load ' + load1.toFixed(2) +
|
||||
' / ' + load5 + ' / ' + load15 + ' on ' + this.numCores +
|
||||
(this.numCores === 1 ? ' core)' : ' cores)');
|
||||
|
||||
var level = pct < 60 ? 'ok' : pct < 85 ? 'warn' : 'crit';
|
||||
this.setLevel(this.cpuEl, level);
|
||||
}
|
||||
|
||||
if (info.memory) {
|
||||
var total = info.memory.total;
|
||||
var avail = info.memory.available || info.memory.free;
|
||||
var used = total - avail;
|
||||
var pct = (used / total * 100).toFixed(0);
|
||||
this.ramEl.querySelector('.indicator-value').textContent = pct + '%';
|
||||
try { sessionStorage.setItem('glass-status-ram', pct + '%'); } catch(e) {}
|
||||
this.ramEl.title = 'RAM: ' + this.formatBytes(used) + ' / ' + this.formatBytes(total) + ' (' + pct + '% used)';
|
||||
|
||||
var level = pct < 60 ? 'ok' : pct < 85 ? 'warn' : 'crit';
|
||||
this.setLevel(this.ramEl, level);
|
||||
}
|
||||
|
||||
if (info.uptime) {
|
||||
this.uptimeEl.querySelector('.indicator-value').textContent = this.formatUptime(info.uptime);
|
||||
try { sessionStorage.setItem('glass-status-uptime', this.formatUptime(info.uptime)); } catch(e) {}
|
||||
this.uptimeEl.title = 'Uptime: ' + this.formatUptimeFull(info.uptime);
|
||||
}
|
||||
},
|
||||
|
||||
updateNetwork: function(dev) {
|
||||
if (!dev || !dev.statistics || !this.netEl) return;
|
||||
|
||||
var now = Date.now() / 1000;
|
||||
var rx = dev.statistics.rx_bytes || 0;
|
||||
var tx = dev.statistics.tx_bytes || 0;
|
||||
|
||||
if (this.prevStats && this.prevTime) {
|
||||
var dt = now - this.prevTime;
|
||||
if (dt > 0) {
|
||||
var rxSpeed = (rx - this.prevStats.rx) / dt;
|
||||
var txSpeed = (tx - this.prevStats.tx) / dt;
|
||||
if (rxSpeed < 0) rxSpeed = 0;
|
||||
if (txSpeed < 0) txSpeed = 0;
|
||||
var netText = '\u2193' + this.formatSpeed(rxSpeed) + ' \u2191' + this.formatSpeed(txSpeed);
|
||||
this.netEl.querySelector('.indicator-value').textContent = netText;
|
||||
try { sessionStorage.setItem('glass-status-net', netText); } catch(e) {}
|
||||
var tip = (this.netLabel || this.netDevice) + ': \u2193 ' + this.formatSpeedFull(rxSpeed) + ' / \u2191 ' + this.formatSpeedFull(txSpeed);
|
||||
if (this.linkSpeed)
|
||||
tip += ' (Link: ' + (this.linkSpeed >= 1000 ? (this.linkSpeed / 1000) + ' Gbps' : this.linkSpeed + ' Mbps') + ')';
|
||||
this.netEl.title = tip;
|
||||
|
||||
var peak = Math.max(rxSpeed, txSpeed);
|
||||
var level = peak < 1048576 ? 'ok' : peak < 52428800 ? 'active' : 'busy';
|
||||
this.setLevel(this.netEl, level);
|
||||
}
|
||||
}
|
||||
|
||||
this.prevStats = { rx: rx, tx: tx };
|
||||
this.prevTime = now;
|
||||
},
|
||||
|
||||
formatUptime: function(s) {
|
||||
var d = Math.floor(s / 86400);
|
||||
var h = Math.floor((s % 86400) / 3600);
|
||||
var m = Math.floor((s % 3600) / 60);
|
||||
if (d > 0) return d + 'd ' + h + 'h';
|
||||
return h + 'h ' + m + 'm';
|
||||
},
|
||||
|
||||
formatUptimeFull: function(s) {
|
||||
var d = Math.floor(s / 86400);
|
||||
var h = Math.floor((s % 86400) / 3600);
|
||||
var m = Math.floor((s % 3600) / 60);
|
||||
var parts = [];
|
||||
if (d > 0) parts.push(d + (d === 1 ? ' day' : ' days'));
|
||||
if (h > 0) parts.push(h + (h === 1 ? ' hour' : ' hours'));
|
||||
if (m > 0) parts.push(m + (m === 1 ? ' min' : ' mins'));
|
||||
return parts.join(', ') || '< 1 min';
|
||||
},
|
||||
|
||||
formatBytes: function(bytes) {
|
||||
if (bytes >= 1073741824)
|
||||
return (bytes / 1073741824).toFixed(1) + ' GB';
|
||||
if (bytes >= 1048576)
|
||||
return (bytes / 1048576).toFixed(0) + ' MB';
|
||||
return (bytes / 1024).toFixed(0) + ' KB';
|
||||
},
|
||||
|
||||
formatSpeed: function(bytesPerSec) {
|
||||
var bits = bytesPerSec * 8;
|
||||
if (bits >= 1000000000)
|
||||
return (bits / 1000000000).toFixed(1) + 'G';
|
||||
if (bits >= 1000000)
|
||||
return (bits / 1000000).toFixed(1) + 'M';
|
||||
if (bits >= 1000)
|
||||
return (bits / 1000).toFixed(1) + 'K';
|
||||
return Math.round(bits) + 'b';
|
||||
},
|
||||
|
||||
formatSpeedFull: function(bytesPerSec) {
|
||||
var bits = bytesPerSec * 8;
|
||||
if (bits >= 1000000000)
|
||||
return (bits / 1000000000).toFixed(1) + ' Gbps';
|
||||
if (bits >= 1000000)
|
||||
return (bits / 1000000).toFixed(1) + ' Mbps';
|
||||
if (bits >= 1000)
|
||||
return (bits / 1000).toFixed(1) + ' kbps';
|
||||
return Math.round(bits) + ' bps';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require uci';
|
||||
'require fs';
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
uci.load('glass'),
|
||||
L.resolveDefault(fs.list('/www/luci-static/glass/background'), [])
|
||||
]);
|
||||
},
|
||||
|
||||
render: function (data) {
|
||||
var bgFiles = (data[1] || []).filter(function(f) {
|
||||
return f.name && !f.name.startsWith('.') && /\.(jpe?g|png|gif|webp|mp4|webm)$/i.test(f.name);
|
||||
});
|
||||
var hasBg = bgFiles.length > 0;
|
||||
var bgName = hasBg ? bgFiles[0].name : null;
|
||||
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('glass', _('Glass Theme'),
|
||||
_('Configure the appearance of the Glass theme. Changes take effect after saving and refreshing the page.'));
|
||||
|
||||
/* ── General ── */
|
||||
s = m.section(form.NamedSection, 'global', 'global', _('General'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.ListValue, 'mode', _('Theme mode'),
|
||||
_('Controls the overall appearance. "Normal" follows the system preference.'));
|
||||
o.value('normal', _('Auto (follow system)'));
|
||||
o.value('light', _('Light'));
|
||||
o.value('dark', _('Dark'));
|
||||
o.default = 'normal';
|
||||
|
||||
o = s.option(form.Flag, 'status_bar', _('Header status bar'),
|
||||
_('Show live system stats (CPU, RAM, network, uptime) in the header. Disable to reduce resource usage on low-end devices.'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'font_size', _('Base font size'),
|
||||
_('Scales all text in the theme. Larger values improve readability on high-DPI displays.'));
|
||||
o.value('13', _('Small (13px)'));
|
||||
o.value('14', _('Normal (14px)'));
|
||||
o.value('16', _('Large (16px)'));
|
||||
o.value('18', _('Extra Large (18px)'));
|
||||
o.default = '14';
|
||||
|
||||
/* ── Colors ── */
|
||||
s = m.section(form.NamedSection, 'global', 'global', _('Accent Colors'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Value, 'primary', _('Primary color (light mode)'),
|
||||
_('Accent color used for active elements, links, and buttons in light mode.'));
|
||||
o.default = '#007AFF';
|
||||
o.placeholder = '#007AFF';
|
||||
o.datatype = 'string';
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
var el = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
var input = el.querySelector('input');
|
||||
if (input) {
|
||||
input.type = 'color';
|
||||
input.style.height = '2.5rem';
|
||||
input.style.cursor = 'pointer';
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'dark_primary', _('Primary color (dark mode)'),
|
||||
_('Accent color used for active elements, links, and buttons in dark mode.'));
|
||||
o.default = '#0A84FF';
|
||||
o.placeholder = '#0A84FF';
|
||||
o.datatype = 'string';
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
var el = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
var input = el.querySelector('input');
|
||||
if (input) {
|
||||
input.type = 'color';
|
||||
input.style.height = '2.5rem';
|
||||
input.style.cursor = 'pointer';
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
/* ── Glass Effects (Light Mode) ── */
|
||||
s = m.section(form.NamedSection, 'global', 'global', _('Glass Effects — Light Mode'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Value, 'blur', _('Blur intensity (px)'),
|
||||
_('Backdrop blur radius for glass panels. Higher values create a more frosted appearance.'));
|
||||
o.default = '20';
|
||||
o.placeholder = '20';
|
||||
o.datatype = 'range(0,50)';
|
||||
|
||||
o = s.option(form.Value, 'transparency', _('Panel transparency'),
|
||||
_('Background opacity of glass panels (0 = fully transparent, 1 = fully opaque).'));
|
||||
o.default = '0.72';
|
||||
o.placeholder = '0.72';
|
||||
o.datatype = 'string';
|
||||
|
||||
/* ── Glass Effects (Dark Mode) ── */
|
||||
s = m.section(form.NamedSection, 'global', 'global', _('Glass Effects — Dark Mode'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Value, 'blur_dark', _('Blur intensity (px)'),
|
||||
_('Backdrop blur radius for glass panels in dark mode.'));
|
||||
o.default = '25';
|
||||
o.placeholder = '25';
|
||||
o.datatype = 'range(0,50)';
|
||||
|
||||
o = s.option(form.Value, 'transparency_dark', _('Panel transparency'),
|
||||
_('Background opacity of glass panels in dark mode (0 = fully transparent, 1 = fully opaque).'));
|
||||
o.default = '0.30';
|
||||
o.placeholder = '0.30';
|
||||
o.datatype = 'string';
|
||||
|
||||
/* ── Background ── */
|
||||
s = m.section(form.NamedSection, 'global', 'global', _('Background'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_bg_info', _('Current background'));
|
||||
o.rawhtml = true;
|
||||
o.cfgvalue = function () {
|
||||
if (hasBg) {
|
||||
return '<span style="color:var(--color-success)">' +
|
||||
_('Active') + ': ' + bgName + '</span>';
|
||||
}
|
||||
return '<span style="color:var(--color-text-secondary)">' +
|
||||
_('No background set. Upload an image or video named "bg" (e.g. bg.jpg, bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP.') +
|
||||
'</span>';
|
||||
};
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user