mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 01:44:16 +08:00
497 lines
19 KiB
Plaintext
497 lines
19 KiB
Plaintext
{#
|
|
Fluent Theme for LuCI
|
|
Based on luci-theme-argon patterns, translated to ucode syntax
|
|
|
|
Licensed to the public under the Apache License 2.0
|
|
#}
|
|
|
|
{%
|
|
import { access } from 'fs';
|
|
import { cursor } from 'uci';
|
|
import { getuid, getspnam } from 'luci.core';
|
|
import { srand } from 'math';
|
|
|
|
//send as HTML5
|
|
http.prepare_content('text/html; charset=UTF-8');
|
|
|
|
srand(+substr(reverse(time() + ""), 0, 8));
|
|
|
|
// System info
|
|
let boardinfo = ubus.call('system', 'board');
|
|
let hostname = boardinfo && boardinfo.hostname ? boardinfo.hostname : '?';
|
|
let model = boardinfo && boardinfo.model ? boardinfo.model : (boardinfo && boardinfo.board_name ? boardinfo.board_name : '?');
|
|
|
|
let cfg = cursor();
|
|
let defaults = {};
|
|
include('themes/fluent/defaults', { defaults });
|
|
let mode = defaults.mode;
|
|
let primary = defaults.primary;
|
|
let dark_primary = defaults.dark_primary;
|
|
let blur_radius = defaults.blur;
|
|
let blur_radius_dark = defaults.blur_dark;
|
|
let blur_opacity = defaults.transparency;
|
|
let blur_opacity_dark = defaults.transparency_dark;
|
|
let font_weight = defaults.font_weight;
|
|
let font_size = defaults.font_size;
|
|
let progressbar_font = defaults.progressbar_font;
|
|
let dark_progressbar_font = defaults.dark_progressbar_font;
|
|
let main_left_font_color = '#69758c';
|
|
let bar_color = primary;
|
|
let sidebar_width = defaults.sidebar_width;
|
|
let header_height = defaults.header_height;
|
|
let border_radius = defaults.border_radius;
|
|
let card_shadow = defaults.card_shadow;
|
|
let control_height = defaults.control_height;
|
|
let transition_speed = defaults.transition_speed;
|
|
let custom_css = defaults.custom_css;
|
|
let custom_select = defaults.custom_select;
|
|
let view_transition = defaults.view_transition;
|
|
let tab_animation = defaults.tab_animation;
|
|
let prefers_reduced_motion = defaults.prefers_reduced_motion;
|
|
let loading_bar = defaults.loading_bar;
|
|
let sticky_action_bar = defaults.sticky_action_bar;
|
|
let page_bg = defaults.page_bg;
|
|
let card_bg = defaults.card_bg;
|
|
let sidebar_bg = defaults.sidebar_bg;
|
|
let dark_page_bg = defaults.dark_page_bg;
|
|
let dark_card_bg = defaults.dark_card_bg;
|
|
let dark_sidebar_bg = defaults.dark_sidebar_bg;
|
|
let progressbar_text_position = defaults.progressbar_text_position;
|
|
let menu_layout = defaults.menu_layout || null;
|
|
let direction = {};
|
|
include('themes/fluent/direction', { cfg, defaults, direction });
|
|
|
|
// Transition speed mapping
|
|
let transition_map = {
|
|
'fast': '100ms',
|
|
'normal': '150ms',
|
|
'slow': '250ms',
|
|
'none': '0ms'
|
|
};
|
|
|
|
// Shadow mapping (utilizing CSS variables for dynamic mode-aware soft shadows)
|
|
let shadow_map = {
|
|
'none': 'none',
|
|
'small': 'var(--fluent-shadow-sm)',
|
|
'medium': 'var(--fluent-shadow-md)',
|
|
'large': 'var(--fluent-shadow-lg)'
|
|
};
|
|
|
|
if (access('/etc/config/fluent')) {
|
|
mode = cfg.get_first('fluent', 'global', 'mode') || mode;
|
|
primary = cfg.get_first('fluent', 'global', 'primary') || primary;
|
|
dark_primary = cfg.get_first('fluent', 'global', 'dark_primary') || dark_primary;
|
|
font_weight = cfg.get_first('fluent', 'global', 'font_weight') || font_weight;
|
|
font_size = cfg.get_first('fluent', 'global', 'font_size') || font_size;
|
|
blur_radius = cfg.get_first('fluent', 'global', 'blur') || blur_radius;
|
|
blur_radius_dark = cfg.get_first('fluent', 'global', 'blur_dark') || blur_radius_dark;
|
|
blur_opacity = cfg.get_first('fluent', 'global', 'transparency') || blur_opacity;
|
|
blur_opacity_dark = cfg.get_first('fluent', 'global', 'transparency_dark') || blur_opacity_dark;
|
|
progressbar_font = cfg.get_first('fluent', 'global', 'progressbar_font') || progressbar_font;
|
|
dark_progressbar_font = cfg.get_first('fluent', 'global', 'dark_progressbar_font') || dark_progressbar_font;
|
|
sidebar_width = cfg.get_first('fluent', 'global', 'sidebar_width') || sidebar_width;
|
|
header_height = cfg.get_first('fluent', 'global', 'header_height') || header_height;
|
|
border_radius = cfg.get_first('fluent', 'global', 'border_radius') || border_radius;
|
|
card_shadow = cfg.get_first('fluent', 'global', 'card_shadow') || card_shadow;
|
|
control_height = cfg.get_first('fluent', 'global', 'control_height') || control_height;
|
|
transition_speed = cfg.get_first('fluent', 'global', 'transition_speed') || transition_speed;
|
|
custom_css = cfg.get_first('fluent', 'global', 'custom_css') || custom_css;
|
|
page_bg = cfg.get_first('fluent', 'global', 'page_bg') || page_bg;
|
|
card_bg = cfg.get_first('fluent', 'global', 'card_bg') || card_bg;
|
|
sidebar_bg = cfg.get_first('fluent', 'global', 'sidebar_bg') || sidebar_bg;
|
|
dark_page_bg = cfg.get_first('fluent', 'global', 'dark_page_bg') || dark_page_bg;
|
|
dark_card_bg = cfg.get_first('fluent', 'global', 'dark_card_bg') || dark_card_bg;
|
|
dark_sidebar_bg = cfg.get_first('fluent', 'global', 'dark_sidebar_bg') || dark_sidebar_bg;
|
|
custom_select = cfg.get_first('fluent', 'global', 'custom_select') || custom_select;
|
|
view_transition = cfg.get_first('fluent', 'global', 'view_transition') || view_transition;
|
|
tab_animation = cfg.get_first('fluent', 'global', 'tab_animation') || tab_animation;
|
|
prefers_reduced_motion = cfg.get_first('fluent', 'global', 'prefers_reduced_motion') || prefers_reduced_motion;
|
|
loading_bar = cfg.get_first('fluent', 'global', 'loading_bar') || loading_bar;
|
|
sticky_action_bar = cfg.get_first('fluent', 'global', 'sticky_action_bar') || sticky_action_bar;
|
|
progressbar_text_position = cfg.get_first('fluent', 'global', 'progressbar_text_position') || progressbar_text_position;
|
|
menu_layout = cfg.get_first('fluent', 'global', 'menu_layout') || null;
|
|
|
|
bar_color = mode === 'dark' ? dark_primary : primary;
|
|
main_left_font_color = font_weight === 'normal' ? '#5f6368' : '#69758c';
|
|
}
|
|
|
|
// Computed values
|
|
let transition_duration = transition_map[transition_speed] || '150ms';
|
|
let card_shadow_value = shadow_map[card_shadow] || shadow_map['small'];
|
|
let brand_name = hostname;
|
|
let page_title = striptags(hostname + (node && node.title ? ' - ' + _(node.title) : ''));
|
|
%} {% if (ctx.authsession) { %}
|
|
<!DOCTYPE html>
|
|
<html lang="{{ dispatcher.lang }}" dir="{{ direction.resolved }}">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>{{ page_title }} - LuCI</title>
|
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
|
|
<meta name="format-detection" content="telephone=no, email=no" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="x5-fullscreen" content="true">
|
|
<meta name="full-screen" content="yes">
|
|
<meta name="x5-page-mode" content="app">
|
|
<meta name="browsermode" content="application">
|
|
<meta name="msapplication-tap-highlight" content="no">
|
|
<meta name="msapplication-TileColor" content="{{ bar_color }}">
|
|
<meta name="application-name" content="{{ hostname }} - LuCI">
|
|
<meta name="apple-mobile-web-app-title" content="{{ hostname }} - LuCI">
|
|
<link rel="apple-touch-icon" href="{{ media }}/icon/icon-192.png">
|
|
<link rel="icon" type="image/png" sizes="192x192" href="{{ media }}/icon/icon-192.png">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ media }}/icon/favicon-32.png">
|
|
<link rel="icon" type="image/svg+xml" href="{{ media }}/img/fluent.svg">
|
|
<link rel="manifest" href="{{ media }}/icon/manifest.json" crossorigin="use-credentials">
|
|
<meta name="msapplication-TileColor" content="{{ bar_color }}">
|
|
<meta name="msapplication-TileImage" content="{{ media }}/icon/icon-192.png">
|
|
|
|
{% if (mode === 'auto') { %}
|
|
<meta name="theme-color" content="{{ bar_color }}">
|
|
<script type="text/javascript">
|
|
let prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
if (prefersDarkMode) {
|
|
viewport = document.querySelector("meta[name=theme-color]");
|
|
viewport.setAttribute('content', '#444950');
|
|
}
|
|
</script>
|
|
{% } else if (mode === 'dark') { %}
|
|
<meta name="theme-color" content="#444950">
|
|
{% } else if (mode === 'light') { %}
|
|
<meta name="theme-color" content="{{ bar_color }}">
|
|
{% } %}
|
|
|
|
{% if (access('/www/luci-static/fluent/css/fluent.css.gz')) { %}
|
|
<style id="fluent-compressed-css"></style>
|
|
<script>
|
|
(function () {
|
|
const style = document.getElementById('fluent-compressed-css');
|
|
if (!style || typeof DecompressionStream === 'undefined') return;
|
|
|
|
fetch('{{ media }}/css/fluent.css.gz?v={# PKG_VERSION #}').then((response) => {
|
|
if (!response.ok || !response.body) throw new Error('Unable to load Fluent CSS');
|
|
return new Response(response.body.pipeThrough(new DecompressionStream('gzip'))).text();
|
|
}).then((css) => {
|
|
style.textContent = css;
|
|
}).catch(() => {
|
|
style.remove();
|
|
});
|
|
})();
|
|
</script>
|
|
{% } else { %}
|
|
<link rel="stylesheet" href="{{ media }}/css/fluent.css">
|
|
{% } %}
|
|
<style title="text/css">
|
|
{# Default light values #}
|
|
:root {
|
|
--fluent-primary: {{ primary }};
|
|
--fluent-bg: {{ page_bg }};
|
|
--fluent-bg-card: {{ card_bg }};
|
|
--fluent-sidebar-bg: {{ sidebar_bg }};
|
|
--fluent-font-weight: {{ font_weight }};
|
|
--fluent-font-size: {{ font_size }}px;
|
|
--fluent-sidebar-width: {{ sidebar_width }}px;
|
|
--fluent-header-height: {{ header_height }}px;
|
|
--fluent-radius-sm: {{ border_radius / 2 }}px;
|
|
--fluent-control-height: {{ control_height }}px;
|
|
--fluent-radius-md: {{ border_radius }}px;
|
|
--fluent-card-shadow: {{ card_shadow_value }};
|
|
--fluent-transition-fast: {{ transition_duration }} ease;
|
|
--fluent-transition-normal: {{ transition_duration }} ease;
|
|
--fluent-progressbar-font-color: {{ progressbar_font }};
|
|
--fluent-blur-radius: {{ blur_radius }}px;
|
|
--fluent-blur-opacity: {{ blur_opacity }};
|
|
--fluent-blur-radius-dark: {{ blur_radius_dark }}px;
|
|
--fluent-blur-opacity-dark: {{ blur_opacity_dark }};
|
|
--fluent-main-left-font-color: {{ main_left_font_color }};
|
|
}
|
|
|
|
{# Dark mode overrides #}
|
|
:root[data-theme="dark"] {
|
|
--fluent-primary: {{ dark_primary }};
|
|
--fluent-bg: {{ dark_page_bg }};
|
|
--fluent-bg-card: {{ dark_card_bg }};
|
|
--fluent-sidebar-bg: {{ dark_sidebar_bg }};
|
|
--fluent-text: #f3f2f1;
|
|
--fluent-text-secondary: #a0a0a0;
|
|
--fluent-border: #404040;
|
|
--fluent-progressbar-font-color: {{ dark_progressbar_font }};
|
|
}
|
|
|
|
{# Font size on body #}
|
|
body {
|
|
font-size: {{ font_size }}px;
|
|
}
|
|
|
|
{# Custom CSS injection #}
|
|
{% if (custom_css) { %}
|
|
{{ custom_css }}
|
|
{% } %}
|
|
</style>
|
|
|
|
{% if (view_transition == '1') { %}
|
|
<style>
|
|
@view-transition {
|
|
navigation: auto;
|
|
}
|
|
|
|
/* Smooth fade-over page transition without dimming unchanged areas */
|
|
::view-transition-old(root) {
|
|
animation: none;
|
|
}
|
|
::view-transition-new(root) {
|
|
animation: 180ms cubic-bezier(0, 0, 0.2, 1) both fade-in;
|
|
}
|
|
|
|
@keyframes fade-in {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
::view-transition-old(root),
|
|
::view-transition-new(root) {
|
|
animation: none !important;
|
|
}
|
|
}
|
|
</style>
|
|
{% } %}
|
|
|
|
{% if (loading_bar == '1') { %}
|
|
<style>
|
|
.fluent-loading-bar {
|
|
position: fixed;
|
|
top: 0;
|
|
inset-inline-start: 0;
|
|
width: 100%;
|
|
height: 3px;
|
|
background-color: transparent;
|
|
z-index: 99999;
|
|
pointer-events: none;
|
|
overflow: hidden;
|
|
opacity: 1;
|
|
transition: opacity 150ms ease;
|
|
}
|
|
|
|
.fluent-loading-bar.loaded {
|
|
opacity: 0;
|
|
}
|
|
|
|
.fluent-loading-bar-indicator {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
inset-inline-start: 0;
|
|
width: 100%;
|
|
background-color: var(--fluent-primary, #0078d4);
|
|
transform-origin: left;
|
|
animation: fluent-indeterminate-bar 1.8s infinite cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
html[dir="rtl"] .fluent-loading-bar-indicator,
|
|
body[data-fluent-dir="rtl"] .fluent-loading-bar-indicator {
|
|
transform-origin: right;
|
|
}
|
|
|
|
@keyframes fluent-indeterminate-bar {
|
|
0% {
|
|
transform: translateX(-100%) scaleX(0.1);
|
|
}
|
|
50% {
|
|
transform: translateX(-30%) scaleX(0.6);
|
|
}
|
|
100% {
|
|
transform: translateX(100%) scaleX(0.1);
|
|
}
|
|
}
|
|
</style>
|
|
{% } %}
|
|
|
|
<link rel="shortcut icon" type="image/ico" href="{{ media }}/favicon.ico">
|
|
{% if (node && node.css) { %}
|
|
<link rel="stylesheet" href="{{ resource }}/{{ node.css }}">
|
|
{% } %}
|
|
{% if (css) { %}
|
|
<style title="text/css">
|
|
{{ css }}
|
|
</style>
|
|
{% } %}
|
|
<script src="{{ dispatcher.build_url('admin/translations', dispatcher.lang) }}?v={{ version.luciversion }}"></script>
|
|
<script src="{{ resource }}/cbi.js?v={{ version.luciversion }}"></script>
|
|
<script src="{{ resource }}/luci.js?v={{ version.luciversion }}"></script>
|
|
<script>
|
|
// Dark mode logic
|
|
(function() {
|
|
var mode = '{{ mode }}';
|
|
var isDark = (mode === 'dark');
|
|
|
|
if (mode === 'auto') {
|
|
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
}
|
|
|
|
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
|
})();
|
|
</script>
|
|
</head>
|
|
|
|
<body
|
|
class="lang_{{ dispatcher.lang }} {{ node ? entityencode(node.title, true) : '' }} {{ ctx.authsession ? 'logged-in' : '' }}"
|
|
data-page="{{ entityencode(join('-', ctx.request_path), true) }}"
|
|
data-theme-mode="{{ mode }}"
|
|
data-menu-layout="{{ entityencode(menu_layout ? sprintf('%J', menu_layout) : '', true) }}"
|
|
data-menu-cache-scope="{{ entityencode(ctx.authsession, true) }}"
|
|
data-tab-cache-path="{{ entityencode(sprintf('%J', ctx.request_path), true) }}"
|
|
data-fluent-dir="{{ direction.resolved }}"
|
|
data-theme-custom-select="{{ custom_select }}"
|
|
data-view-transition="{{ view_transition }}"
|
|
data-tab-animation="{{ tab_animation }}"
|
|
data-prefers-reduced-motion="{{ prefers_reduced_motion }}"
|
|
data-loading-bar="{{ loading_bar }}"
|
|
data-sticky-action-bar="{{ sticky_action_bar }}"
|
|
data-progressbar-text="{{ progressbar_text_position }}"
|
|
data-sidebar-state="expanded">
|
|
{% if (ctx.authsession) { %}
|
|
<script>
|
|
(function() {
|
|
if (window.innerWidth <= 768) return;
|
|
try {
|
|
var sidebarState = localStorage.getItem('fluent-sidebar-state');
|
|
if (sidebarState === 'collapsed' || sidebarState === 'expanded') {
|
|
document.body.setAttribute('data-sidebar-state', sidebarState);
|
|
}
|
|
} catch (_) {}
|
|
})();
|
|
</script>
|
|
{% } %}
|
|
|
|
{% if (loading_bar == '1') { %}
|
|
<div id="fluent-top-loading" class="fluent-loading-bar" aria-hidden="true">
|
|
<div class="fluent-loading-bar-indicator"></div>
|
|
</div>
|
|
{% } %}
|
|
|
|
<div class="main">
|
|
{% if (ctx.authsession) { %}
|
|
<div class="main-left" id="mainmenu" style="display:none">
|
|
<div class="sidebar__search-slot" aria-hidden="true"></div>
|
|
</div>
|
|
<script>
|
|
(function() {
|
|
try {
|
|
var cacheScope = document.body.getAttribute('data-menu-cache-scope');
|
|
if (!cacheScope) return;
|
|
var cached = sessionStorage.getItem('fluent_sidebar_html_' + cacheScope);
|
|
if (cached) {
|
|
var m = document.getElementById('mainmenu');
|
|
m.innerHTML = cached;
|
|
m.style.display = '';
|
|
|
|
var currentPath = location.pathname;
|
|
var links = m.querySelectorAll('a[href]');
|
|
var matched = null;
|
|
var maxLen = -1;
|
|
|
|
for (var i = 0; i < links.length; i++) {
|
|
var href = links[i].getAttribute('href');
|
|
if (href && href !== '#' && href.indexOf('javascript:') !== 0) {
|
|
if (currentPath === href || currentPath.indexOf(href) === 0) {
|
|
if (href.length > maxLen) {
|
|
matched = links[i];
|
|
maxLen = href.length;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (matched) {
|
|
var actives = m.querySelectorAll('.active');
|
|
for (var j = 0; j < actives.length; j++) {
|
|
actives[j].classList.remove('active');
|
|
}
|
|
|
|
matched.classList.add('active');
|
|
if (matched.parentElement) {
|
|
matched.parentElement.classList.add('active');
|
|
}
|
|
|
|
var parentSubmenu = matched.closest('ul.slide-menu');
|
|
if (parentSubmenu) {
|
|
parentSubmenu.classList.add('active');
|
|
var parentLi = parentSubmenu.parentElement;
|
|
if (parentLi) {
|
|
parentLi.classList.add('active');
|
|
var parentA = parentLi.querySelector(':scope > a');
|
|
if (parentA) parentA.classList.add('active');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
})();
|
|
</script>
|
|
{% } %}
|
|
<div class="main-right">
|
|
<header class="header">
|
|
<div class="header__brand">
|
|
<button type="button" class="sidebar-collapse-toggle desktop-toggle"></button>
|
|
<a class="header__toggle showSide mobile-toggle"></a>
|
|
<a class="header__title brand" href="/">
|
|
<img src="{{ media }}/img/fluent.svg" class="icon" alt="">
|
|
<span>{{ brand_name }}</span>
|
|
</a>
|
|
</div>
|
|
{% if (ctx.authsession) { %}
|
|
<div class="header__search-slot" aria-hidden="true"><div class="header__search-placeholder"></div></div>
|
|
<div class="header-actions">
|
|
<button class="theme-toggle" id="theme-toggle" title="{{ _('Switch theme mode') }}" aria-label="{{ _('Switch theme mode') }}"></button>
|
|
<div class="status" id="indicators"></div>
|
|
</div>
|
|
{% } %}
|
|
</header>
|
|
<div class="darkMask"></div>
|
|
<div id="maincontent">
|
|
<div class="container">
|
|
{% if (getuid() == 0 && getspnam('root') && getspnam('root').pwdp === '' && join('-', ctx.request_path) != 'admin-system-admin') { %}
|
|
<div class="alert-message error">
|
|
<h4>{{ _('No password set!') }}</h4>
|
|
<p>{{ _('There is no password set on this router. Please configure a root password to protect the web interface.') }}</p>
|
|
{% if (dispatcher.lookup('admin/system/admin')) { %}
|
|
<div class="right"><a class="btn"
|
|
href="{{ dispatcher.build_url('admin/system/admin') }}">{{ _('Go to password configuration...') }}</a></div>
|
|
{% } %}
|
|
</div>
|
|
{% } %}
|
|
|
|
<noscript>
|
|
<div class="alert-message error">
|
|
<h4>{{ _('JavaScript required!') }}</h4>
|
|
<p>{{ _('You must enable JavaScript in your browser or LuCI will not work properly.') }}</p>
|
|
</div>
|
|
</noscript>
|
|
|
|
<div id="tabmenu" style="display:none"></div>
|
|
<script>
|
|
(function() {
|
|
try {
|
|
var cacheScope = document.body.getAttribute('data-menu-cache-scope');
|
|
if (!cacheScope) return;
|
|
var encodedPath = document.body.getAttribute('data-tab-cache-path');
|
|
var pathParts = encodedPath ? JSON.parse(encodedPath) : [];
|
|
if (!Array.isArray(pathParts) || pathParts.length < 3) return;
|
|
var pathKey = JSON.stringify(pathParts);
|
|
var cachedTabs = sessionStorage.getItem('fluent_tabmenu_html_' + cacheScope + '_' + pathKey);
|
|
if (cachedTabs) {
|
|
var t = document.getElementById('tabmenu');
|
|
t.innerHTML = cachedTabs;
|
|
t.style.display = '';
|
|
}
|
|
} catch (_) {}
|
|
})();
|
|
</script>
|
|
{% } else { %}
|
|
{% include("themes/fluent/out_header_login"); %}
|
|
{% include("themes/fluent/sysauth"); %}
|
|
{% ctx._fluent_login_shown = true; %}
|
|
{% } %}
|