💋 Sync 2026-10-04 04:16:09

This commit is contained in:
github-actions[bot] committed 2026-10-04 04:16:09 +08:00
1 parent f6b2df73ec
commit f62992fd80
745 files changed
+212416 -1173

No files matched your search

@@ -0,0 +1,36 @@
{%
defaults.mode = "auto";
defaults.primary = "#0078d4";
defaults.dark_primary = "#4da6ff";
defaults.progressbar_font = "#2e2b60";
defaults.dark_progressbar_font = "#d6d9e5";
defaults.page_bg = "#fafafa";
defaults.card_bg = "#ffffff";
defaults.sidebar_bg = "#f3f3f3";
defaults.dark_page_bg = "#1b1b1b";
defaults.dark_card_bg = "#2d2d2d";
defaults.dark_sidebar_bg = "#1f1f1f";
defaults.font_weight = "normal";
defaults.font_size = "14";
defaults.border_radius = "4";
defaults.control_height = "32";
defaults.sidebar_width = "260";
defaults.header_height = "48";
defaults.login_bg = "microsoft";
defaults.blur = "40";
defaults.transparency = "0.8";
defaults.blur_dark = "20";
defaults.transparency_dark = "0.8";
defaults.card_shadow = "small";
defaults.transition_speed = "normal";
defaults.custom_css = "";
defaults.custom_select = "1";
defaults.view_transition = "1";
defaults.tab_animation = "1";
defaults.prefers_reduced_motion = "1";
defaults.loading_bar = "1";
defaults.sticky_action_bar = "1";
defaults.uci_changes_preview = "1";
defaults.direction_mode = "auto";
defaults.progressbar_text_position = "top-start";
%}
@@ -0,0 +1,25 @@
{%
let configured_direction_mode = cfg.get_first('fluent', 'global', 'direction_mode') || defaults.direction_mode;
let direction_mode = configured_direction_mode;
if (direction_mode !== 'rtl' && direction_mode !== 'ltr' && direction_mode !== 'auto') {
direction_mode = defaults.direction_mode;
}
let locale = dispatcher.lang || '';
let resolved_direction = 'ltr';
if (direction_mode === 'rtl') {
resolved_direction = 'rtl';
} else if (direction_mode === 'auto') {
if (locale === 'ar' || locale === 'fa' ||
index(locale, 'ar_') === 0 || index(locale, 'ar-') === 0 ||
index(locale, 'fa_') === 0 || index(locale, 'fa-') === 0) {
resolved_direction = 'rtl';
}
}
direction.mode = direction_mode;
direction.locale = locale;
direction.resolved = resolved_direction;
%}
@@ -0,0 +1,39 @@
{#
Fluent Theme for LuCI - Footer
Licensed to the public under the Apache License 2.0
#}
{% if (!ctx._fluent_login_shown) { %}
</div>
{% if (!ctx.authsession) { %}
<footer>
<div class="mobile-hide">
<a class="luci-link" href="https://github.com/openwrt/luci">Powered by {{ version.luciname }} ({{ version.luciversion }})</a> /
{{ version.distversion }}
<ul class="breadcrumb pull-right" id="modemenu" style="display:none"></ul>
</div>
</footer>
{% } %}
</div>
</div>
<script>
// thanks for Jo-Philipp Wich <jow@openwrt.org>
var luciLocation = {{ ctx.path }};
var winHeight = window.innerHeight;
window.addEventListener('resize', function () {
var winWidth = window.innerWidth;
if(winWidth < 600){
var newHeight = window.innerHeight;
var keyboradHeight = newHeight - winHeight;
var ftcElement = document.querySelector(".ftc");
if (ftcElement) {
ftcElement.style.bottom = (keyboradHeight + 30) + "px";
}
}
});
</script>
<script type="text/javascript">L.require('menu-fluent')</script>
</body>
</html>
{% } %}
@@ -0,0 +1,17 @@
{#
Fluent Theme for LuCI - Login Footer
Licensed to the public under the Apache License 2.0
#}
</div>
<footer class="mobile-hide">
<div>
<a class="luci-link" href="https://github.com/openwrt/luci">Powered by {{ version.luciname }} ({{ version.luciversion }})</a> /
{{ version.distversion }}
<ul class="breadcrumb pull-right" id="modemenu" style="display:none"></ul>
</div>
</footer>
</div>
</div>
</body>
</html>
@@ -0,0 +1,496 @@
{#
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; %}
{% } %}
@@ -0,0 +1,145 @@
{#
Fluent Theme for LuCI - Login Page Header
Licensed to the public under the Apache License 2.0
#}
{%
import { access } from 'fs';
import { cursor } from 'uci';
import { srand } from 'math';
// System info
let boardinfo = ubus.call('system', 'board');
let hostname = boardinfo && boardinfo.hostname ? boardinfo.hostname : '?';
let cfg = cursor();
let defaults = {};
include('themes/fluent/defaults', { defaults });
http.prepare_content('text/html; charset=UTF-8');
srand(+substr(reverse(time() + ""), 0, 8));
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 bar_color = primary;
let direction = {};
include('themes/fluent/direction', { cfg, defaults, direction });
if (access('/etc/config/fluent')) {
primary = cfg.get_first('fluent', 'global', 'primary') || primary;
dark_primary = cfg.get_first('fluent', 'global', 'dark_primary') || dark_primary;
mode = cfg.get_first('fluent', 'global', 'mode') || mode;
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;
bar_color = (mode === 'dark') ? dark_primary : primary;
}
%}
<!DOCTYPE html>
<html lang="{{ dispatcher.lang }}" dir="{{ direction.resolved }}">
<head>
<meta charset="utf-8">
<title>{{ hostname }} - LuCI</title>
<!-- SEO and Description -->
<meta name="robots" content="noindex, nofollow">
<!-- Security -->
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
<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">
(function() {
let isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
document.querySelector("meta[name=theme-color]")?.setAttribute('content', isDark ? '#444950' : '{{ bar_color }}');
})();
</script>
{% } else if (mode === 'dark') { %}
<meta name="theme-color" content="#444950">
<script type="text/javascript">document.documentElement.setAttribute('data-theme', 'dark');</script>
{% } else if (mode === 'light') { %}
<meta name="theme-color" content="{{ bar_color }}">
<script type="text/javascript">document.documentElement.setAttribute('data-theme', 'light');</script>
{% } %}
{% 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 - must come FIRST so dark overrides can take precedence #}
:root {
--fluent-primary: {{ primary }};
--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 }};
}
{# Dark mode overrides - must come AFTER default values #}
{% if (mode === 'auto') { %}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--fluent-primary: {{ dark_primary }};
}
}
{% } else if (mode === 'dark') { %}
:root {
--fluent-primary: {{ dark_primary }};
}
{% } %}
</style>
<link rel="shortcut icon" type="image/ico" href="{{ media }}/favicon.ico">
<script src="{{ resource }}/luci.js?v={{ version.luciversion }}"></script>
<script src="{{ resource }}/cbi.js?v={{ version.luciversion }}"></script>
</head>
<body class="lang_{{ dispatcher.lang }} login-page" data-theme-mode="{{ mode }}" data-fluent-dir="{{ direction.resolved }}">
@@ -0,0 +1,11 @@
{#
Fluent Theme for LuCI - Login Header Wrapper
Licensed to the public under the Apache License 2.0
#}
{%
if (!ctx.template_header_sent) {
include("themes/fluent/header_login");
ctx.template_header_sent = true;
}
%}
@@ -0,0 +1,565 @@
{% include('themes/fluent/out_header_login'); %}
{#
Fluent Theme for LuCI - Login Page
Licensed to the public under the Apache License 2.0
#}
{%
import { access, glob, popen } from 'fs';
import { cursor } from 'uci';
// System info
let boardinfo = ubus.call('system', 'board');
let hostname = boardinfo && boardinfo.hostname ? boardinfo.hostname : '?';
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 bar_color = primary;
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;
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;
bar_color = mode === 'dark' ? dark_primary : primary;
}
// Background image handling
let imageTypes = {
jpg: true, png: true, gif: true, webp: true
};
let videoTypes = {
mp4: true, webm: true
};
function fetchMedia(path, themeDir) {
let backgroundTable = [];
let backgroundCount = 0;
try {
let files = glob(path);
for (let f in files) {
let m = match(f, regexp('.+\\.([A-Za-z0-9]+)$'));
let ext = m ? m[1] : null;
if (ext in imageTypes || ext in videoTypes) {
let parts = split(f, '/');
let filename = parts[length(parts) - 1];
push(backgroundTable, {
type: ext,
is_video: ext in videoTypes,
url: themeDir + filename
});
backgroundCount++;
}
}
} catch (e) {}
return [backgroundTable, backgroundCount];
}
function selectBackground(themeDir) {
let bgUrl = '';
let backgroundType = 'None';
let mimeType = '';
let login_bg = defaults.login_bg;
if (access('/etc/config/fluent')) {
login_bg = cfg.get_first('fluent', 'global', 'login_bg') || login_bg;
}
if (login_bg === 'microsoft') {
return ['', 'MicrosoftDynamic', ''];
}
if (login_bg === 'bing') {
try {
let proc = popen('/usr/libexec/fluent/online_wallpaper');
let picurl = proc ? trim(proc.read('all')) : '';
if (proc)
proc.close();
if (picurl) {
return [picurl, 'Image', ''];
}
} catch (e) {}
}
if (login_bg !== 'custom') {
return ['', 'None', ''];
}
// Custom mode: rotate uploaded media from the background directory
let mediaFiles = fetchMedia('/www' + themeDir + '*', themeDir);
let backgroundTable = mediaFiles[0];
let backgroundCount = mediaFiles[1];
if (backgroundCount > 0) {
let currentBg = backgroundTable[time() % backgroundCount];
bgUrl = currentBg.url;
if (currentBg.is_video) {
backgroundType = 'Video';
mimeType = 'video/' + currentBg.type;
}
}
return [bgUrl, backgroundType, mimeType];
}
let themeDir = media + '/background/';
let selectedBg = selectBackground(themeDir);
let bgUrl = selectedBg[0];
let backgroundType = selectedBg[1];
let mimeType = selectedBg[2];
%}
<!-- Login Page Start -->
<div class="login-page">
{% if (backgroundType === 'Video') { %}
<!-- Video Player Start -->
<div class="video">
<video autoplay loop muted id="video">
<source src="{{ bgUrl }}" type="{{ mimeType }}">
</video>
</div>
<div class="volume-control mute"></div>
<script>
document.querySelector(".volume-control").addEventListener("click", function(){
var video = document.getElementById("video");
if(this.classList.contains("mute")){
this.classList.remove("mute");
video.muted = false;
} else {
this.classList.add("mute");
video.muted = true;
}
});
</script>
<!-- Video Player End -->
{% } else if (backgroundType === 'MicrosoftDynamic') { %}
<!-- Microsoft-style Dynamic Canvas Background -->
<canvas id="login-canvas" class="main-bg"></canvas>
<script type="text/javascript">//<![CDATA[
(function() {
const canvas = document.getElementById('login-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let w = canvas.width = window.innerWidth;
let h = canvas.height = window.innerHeight;
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
// Dynamic color options based on theme
const colors = isDark ? {
bgStart: '#090a10',
bgEnd: '#030406',
ribbons: [
{
y1: 0.6, y2: 0.1, thicknessStart: 0.35, thicknessEnd: 0.3, thicknessMid: 0.5,
cp1x: 0.3, cp1y: 0.4, cp2x: 0.7, cp2y: 0.2, speed: 0.0004, phase: 0,
c1: 'rgba(10, 24, 72, 0.45)', c2: 'rgba(3, 8, 28, 0)'
},
{
y1: 0.75, y2: 0.2, thicknessStart: 0.25, thicknessEnd: 0.2, thicknessMid: 0.4,
cp1x: 0.4, cp1y: 0.55, cp2x: 0.8, cp2y: 0.35, speed: 0.0006, phase: 1.8,
c1: 'rgba(0, 72, 160, 0.35)', c2: 'rgba(0, 10, 40, 0)'
},
{
y1: 0.5, y2: 0.35, thicknessStart: 0.15, thicknessEnd: 0.15, thicknessMid: 0.3,
cp1x: 0.2, cp1y: 0.65, cp2x: 0.6, cp2y: 0.45, speed: 0.0005, phase: 3.5,
c1: 'rgba(84, 32, 140, 0.25)', c2: 'rgba(12, 4, 28, 0)'
},
{
y1: 0.5, y2: 0.35, thicknessStart: 0.02, thicknessEnd: 0.02, thicknessMid: 0.04,
cp1x: 0.2, cp1y: 0.65, cp2x: 0.6, cp2y: 0.45, speed: 0.0005, phase: 3.5,
c1: 'rgba(0, 180, 255, 0.25)', c2: 'rgba(0, 0, 0, 0)'
}
]
} : {
bgStart: '#eef3fa',
bgEnd: '#f4f6f9',
ribbons: [
{
y1: 0.6, y2: 0.1, thicknessStart: 0.35, thicknessEnd: 0.3, thicknessMid: 0.5,
cp1x: 0.3, cp1y: 0.4, cp2x: 0.7, cp2y: 0.2, speed: 0.0004, phase: 0,
c1: 'rgba(0, 120, 215, 0.08)', c2: 'rgba(0, 120, 215, 0)'
},
{
y1: 0.75, y2: 0.2, thicknessStart: 0.25, thicknessEnd: 0.2, thicknessMid: 0.4,
cp1x: 0.4, cp1y: 0.55, cp2x: 0.8, cp2y: 0.35, speed: 0.0006, phase: 1.8,
c1: 'rgba(112, 48, 160, 0.06)', c2: 'rgba(112, 48, 160, 0)'
},
{
y1: 0.5, y2: 0.35, thicknessStart: 0.15, thicknessEnd: 0.15, thicknessMid: 0.3,
cp1x: 0.2, cp1y: 0.65, cp2x: 0.6, cp2y: 0.45, speed: 0.0005, phase: 3.5,
c1: 'rgba(0, 130, 114, 0.07)', c2: 'rgba(0, 130, 114, 0)'
},
{
y1: 0.5, y2: 0.35, thicknessStart: 0.02, thicknessEnd: 0.02, thicknessMid: 0.04,
cp1x: 0.2, cp1y: 0.65, cp2x: 0.6, cp2y: 0.45, speed: 0.0005, phase: 3.5,
c1: 'rgba(255, 255, 255, 0.65)', c2: 'rgba(255, 255, 255, 0)'
}
]
};
// Random particles (stardust / micro-particles)
let particles = [];
const particleCount = isDark ? 65 : 30;
function initParticles() {
particles = [];
for (let i = 0; i < particleCount; i++) {
particles.push({
x: Math.random() * w,
y: Math.random() * h,
size: Math.random() * 1.5 + 0.4,
alpha: Math.random() * 0.7 + 0.1,
targetAlpha: Math.random() * 0.7 + 0.1,
dx: (Math.random() - 0.5) * 0.06,
dy: (Math.random() - 0.5) * 0.06,
flicker: Math.random() * 0.008 + 0.002
});
}
}
initParticles();
window.addEventListener('resize', () => {
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
initParticles();
});
function drawRibbon(time, opt) {
ctx.beginPath();
// Calculate dynamic start/end points using sine/cosine waves for gentle floating motion
const startX1 = -w * 0.2;
const startY1 = h * (opt.y1 + Math.sin(time * opt.speed + opt.phase) * 0.04);
const cp1x = w * (opt.cp1x + Math.cos(time * opt.speed * 0.8 + opt.phase) * 0.04);
const cp1y = h * (opt.cp1y + Math.sin(time * opt.speed * 1.1 + opt.phase) * 0.05);
const cp2x = w * (opt.cp2x + Math.sin(time * opt.speed * 0.9 + opt.phase * 1.5) * 0.04);
const cp2y = h * (opt.cp2y + Math.cos(time * opt.speed * 1.2 + opt.phase * 1.5) * 0.04);
const endX1 = w * 1.2;
const endY1 = h * (opt.y2 + Math.cos(time * opt.speed * 0.7 + opt.phase * 2) * 0.04);
ctx.moveTo(startX1, startY1);
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX1, endY1);
// Line down to start of bottom edge of the ribbon
const endX2 = w * 1.2;
const endY2 = endY1 + h * opt.thicknessEnd;
ctx.lineTo(endX2, endY2);
// Bezier curves back to left side
const cp3x = cp2x;
const cp3y = cp2y + h * opt.thicknessMid;
const cp4x = cp1x;
const cp4y = cp1y + h * opt.thicknessMid;
const startX2 = startX1;
const startY2 = startY1 + h * opt.thicknessStart;
ctx.bezierCurveTo(cp3x, cp3y, cp4x, cp4y, startX2, startY2);
ctx.closePath();
const grad = ctx.createLinearGradient(0, h * opt.y1, w, h * opt.y2);
grad.addColorStop(0, opt.c1);
grad.addColorStop(1, opt.c2);
ctx.fillStyle = grad;
ctx.fill();
}
function animate(time) {
// 1. Draw base gradient background
const bgGrad = ctx.createLinearGradient(0, 0, w, h);
bgGrad.addColorStop(0, colors.bgStart);
bgGrad.addColorStop(1, colors.bgEnd);
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, w, h);
// 2. Draw dynamic ribbons/waves
colors.ribbons.forEach(opt => {
drawRibbon(time, opt);
});
// 3. Draw particles
particles.forEach(p => {
// Update position
p.x += p.dx;
p.y += p.dy;
// Loop screen edges
if (p.x < 0) p.x = w;
if (p.x > w) p.x = 0;
if (p.y < 0) p.y = h;
if (p.y > h) p.y = 0;
// Twinkle/Flicker alpha
if (Math.abs(p.alpha - p.targetAlpha) < 0.02) {
p.targetAlpha = Math.random() * (isDark ? 0.7 : 0.4) + 0.1;
}
p.alpha += (p.targetAlpha - p.alpha) * p.flicker;
// Draw
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = isDark ? `rgba(255, 255, 255, ${p.alpha})` : `rgba(0, 120, 215, ${p.alpha * 0.7})`;
ctx.fill();
});
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
})();
//]]></script>
{% } else if (bgUrl) { %}
<!-- Image Background Start -->
<div class="main-bg" id="main-bg" style="background-image:url({{ bgUrl }})"></div>
<!-- Image Background End -->
{% } %}
<!-- Login Container Start -->
<div class="login-container">
<div class="login-form">
<!-- Card Header Start -->
<div class="ms-card-header">
<button type="button" class="ms-back-button" id="ms-back-btn" style="display: none;" aria-label="Back">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="17" y1="12" x2="7" y2="12"></line>
<polyline points="11 17 6 12 11 7"></polyline>
</svg>
</button>
<a class="brand" href="/">
<img src="{{ media }}/img/fluent.svg" class="icon" />
<span class="brand-text">{{ hostname }}</span>
</a>
</div>
<!-- Card Header End -->
<!-- Login Form Start -->
<form class="form-login" method="post" action="">
<!-- Step 1: Username -->
<div id="step-username" class="step-container">
<h2 class="login-title">{{ _('Authorization Required') }}</h2>
<p class="login-subtitle">{{ _('Please enter your username.') }}</p>
<div class="input-container">
<div class="ms-input-wrapper">
<input class="ms-input{% if (fuser) { %} cbi-input-invalid{% } %}" id="cbi-input-user" type="text" name="luci_username" value="{{ entityencode(duser, true) }}" placeholder=" " />
<label class="ms-label" for="cbi-input-user">{{ _('Username') }}</label>
<div class="ms-input-underline"></div>
</div>
</div>
<div class="button-container">
<button type="button" class="ms-button" id="btn-next">{{ _('Next') }}</button>
</div>
</div>
<!-- Step 2: Password -->
<div id="step-password" class="step-container">
<div class="login-account-pill-wrapper">
<div class="login-account-pill" id="login-account-pill">
<span class="login-account-pill__text" id="login-account-pill-text"></span>
</div>
</div>
<h2 class="login-title">{{ _('Authorization Required') }}</h2>
<p class="login-subtitle">{{ _('Please enter your password.') }}</p>
<div class="input-container">
<div class="ms-input-wrapper password-wrapper">
<input class="ms-input{% if (fuser) { %} cbi-input-invalid{% } %}" id="cbi-input-password" type="password" name="luci_password" placeholder=" " />
<label class="ms-label" for="cbi-input-password">{{ _('Password') }}</label>
<button type="button" class="ms-password-toggle" id="ms-password-toggle" tabindex="-1" aria-label="Toggle password visibility">
<svg class="eye-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
<div class="ms-input-underline"></div>
</div>
{%- if (fuser) { %}
<div class="errorbox">{{ _('Invalid username and/or password! Please try again.') }}</div>
{%- } %}
</div>
<div class="button-container">
<input type="submit" value="{{ _('Login') }}" class="ms-button" id="btn-login" />
</div>
</div>
</form>
<!-- Login Form End -->
<script type="text/javascript">//<![CDATA[
(function() {
var userInput = document.getElementById('cbi-input-user');
var passInput = document.getElementById('cbi-input-password');
var stepUsername = document.getElementById('step-username');
var stepPassword = document.getElementById('step-password');
var backBtn = document.getElementById('ms-back-btn');
var btnNext = document.getElementById('btn-next');
var accountPillText = document.getElementById('login-account-pill-text');
var passwordToggle = document.getElementById('ms-password-toggle');
var errorBox = document.querySelector('.errorbox');
var loginFormEl = document.querySelector('.form-login');
function getCurrentHost() {
if (window.location && window.location.host) {
return window.location.host;
}
return '{{ entityencode(hostname, true) }}';
}
function formatAccountPill(username) {
var normalizedUsername = username ? username.trim() : '';
if (!normalizedUsername) {
return '';
}
var currentHost = getCurrentHost();
return currentHost ? normalizedUsername + '@' + currentHost : normalizedUsername;
}
function syncAccountPill() {
if (accountPillText && userInput) {
accountPillText.textContent = formatAccountPill(userInput.value);
}
}
function clearError() {
if (errorBox) errorBox.style.display = 'none';
if (userInput) userInput.classList.remove('cbi-input-invalid');
if (passInput) passInput.classList.remove('cbi-input-invalid');
}
if (userInput) {
userInput.addEventListener('input', clearError);
}
if (passInput) {
passInput.addEventListener('input', clearError);
}
function showStep2() {
if (loginFormEl) loginFormEl.classList.add('show-step2');
if (backBtn) backBtn.style.display = 'flex';
if (passInput) {
setTimeout(function() {
passInput.focus();
}, 50);
}
}
function showStep1() {
if (loginFormEl) loginFormEl.classList.remove('show-step2');
if (backBtn) backBtn.style.display = 'none';
if (userInput) {
setTimeout(function() {
userInput.focus();
}, 50);
}
}
if (btnNext && userInput) {
btnNext.addEventListener('click', function(e) {
e.preventDefault();
if (userInput.value.trim() !== '') {
syncAccountPill();
showStep2();
} else {
if (typeof userInput.reportValidity === 'function') {
userInput.reportValidity();
} else {
alert('{{ _('Username is required.') }}');
}
}
});
userInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
btnNext.click();
}
});
}
if (backBtn) {
backBtn.addEventListener('click', function(e) {
e.preventDefault();
showStep1();
});
}
if (passwordToggle && passInput) {
passwordToggle.addEventListener('click', function(e) {
e.preventDefault();
if (passInput.type === 'password') {
passInput.type = 'text';
passwordToggle.innerHTML = '<svg class="eye-off-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>';
} else {
passInput.type = 'password';
passwordToggle.innerHTML = '<svg class="eye-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>';
}
});
}
// Initial router: If username is already populated, go straight to Step 2.
if (userInput && userInput.value.trim() !== '') {
syncAccountPill();
if (loginFormEl) {
loginFormEl.classList.add('no-transition');
showStep2();
// Force reflow to flush styles before removing no-transition
var reflow = loginFormEl.offsetHeight;
loginFormEl.classList.remove('no-transition');
} else {
showStep2();
}
} else {
showStep1();
}
})();
//]]></script>
{%
let https_key = cfg.get('uhttpd', 'main', 'key');
let https_port = cfg.get('uhttpd', 'main', 'listen_https');
if (type(https_port) === 'object') {
https_port = https_port[0];
}
if (https_port && access(https_key)) {
let m = match(https_port, regexp('(\\d+)$'));
https_port = m ? m[1] : null;
%}
<script type="text/javascript">//<![CDATA[
if (document.location.protocol != 'https:') {
var url = 'https://' + window.location.hostname + ':' + '{{ https_port }}' + window.location.pathname;
var img = new Image;
img.onload = function () { window.location = url };
img.src = 'https://' + window.location.hostname + ':' + '{{ https_port }}' + '{{ resource }}/cbi/up.gif?' + Math.random();
setTimeout(function () {
img.src = ''
}, 5000);
}
//]]></script>
{% } %}
{% include('themes/fluent/footer_login'); %}
<!-- Login Page End -->