update 2025-10-25 20:33:15

This commit is contained in:
actions-user
2025-10-25 20:33:15 +08:00
parent 5c4121f06f
commit 4b28b70e14
12 changed files with 3555 additions and 3162 deletions

View File

@ -28,6 +28,5 @@
},
"uci": ["ddns-go"]
}
},
{ "user": "ddns-go", "access": { "network.interface.*": { "methods": [ "status" ] } } }
}
}

View File

@ -7,9 +7,9 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Material3 Theme
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=alpha-0.0.5
PKG_RELEASE:=20250102
LUCI_DEPENDS:=
PKG_VERSION:=1.0
PKG_RELEASE:=20250617
PKG_LICENSE:=Apache-2.0
@ -20,12 +20,14 @@ define Package/luci-theme-material3/postrm
uci -q delete luci.themes.Material3Blue
uci -q delete luci.themes.Material3Green
uci -q delete luci.themes.Material3Red
uci set luci.main.mediaurlbase='/luci-static/bootstrap'
# uci -q delete luci.themes.Material3Dark
# uci -q delete luci.themes.Material3Light
uci commit luci
}
endef
LUCI_MINIFY_CSS:=0
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
# call BuildPackage - OpenWrt buildroot signature

View File

@ -27,7 +27,7 @@ This is a personal project based on the LuCI Bootstrap theme, imitating the Mate
</table>
## 📝 ToDo
- [ ] Dark themes
- [X] Dark themes
- [ ] Fix some style issues
- [ ] Add more color schemes
- [ ] Improve to be closer to MD3 principles

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1760837915938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6738" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M817.834667 627.643733h-272.725334v-80.554666a93.661867 93.661867 0 0 0-33.109333-181.248 93.661867 93.661867 0 0 0-33.109333 181.248v80.554666H206.165333c-49.493333 0-89.770667 40.277333-89.770666 89.770667v114.688c0 49.493333 40.277333 89.429333 89.770666 89.429333h611.669334c49.493333 0 89.770667-39.936 89.770666-89.429333v-114.688c0-49.493333-40.277333-89.770667-89.770666-89.770667z m-598.698667 181.248a34.0992 34.0992 0 0 1-34.474667-34.133333c0-18.773333 15.36-34.133333 34.474667-34.133333 18.773333 0 33.792 15.36 33.792 34.133333 0 19.114667-15.018667 34.133333-33.792 34.133333z m136.533333 0a34.0992 34.0992 0 0 1-34.474666-34.133333c0-18.773333 15.36-34.133333 34.474666-34.133333 18.773333 0 33.792 15.36 33.792 34.133333 0 19.114667-15.018667 34.133333-33.792 34.133333z m136.533334 0a34.0992 34.0992 0 0 1-34.474667-34.133333c0-18.773333 15.36-34.133333 34.474667-34.133333 18.773333 0 33.792 15.36 33.792 34.133333 0 19.114667-15.018667 34.133333-33.792 34.133333z" p-id="6739"></path><path d="M190.293333 508.791467a32.9728 32.9728 0 0 0 31.300267-34.542934A292.181333 292.181333 0 0 1 306.176 253.508267c113.595733-113.5616 298.325333-113.5616 411.8528 0a291.7376 291.7376 0 0 1 84.411733 220.706133 32.9728 32.9728 0 0 0 65.8432 3.310933c5.085867-101.034667-32.699733-199.68-103.6288-270.6432-139.264-139.195733-365.841067-139.264-505.105066 0a358.126933 358.126933 0 0 0-103.799467 270.609067 33.041067 33.041067 0 0 0 34.542933 31.300267z" p-id="6740"></path><path d="M352.768 300.1344c-46.523733 46.557867-69.973333 111.616-64.375467 178.517333 1.501867 18.090667 17.066667 31.880533 35.601067 30.037334a32.938667 32.938667 0 0 0 30.1056-35.601067c-3.9936-47.5136 12.4928-93.559467 45.294933-126.327467a159.573333 159.573333 0 0 1 225.416534-0.034133c32.768 32.733867 49.186133 78.813867 45.124266 126.327467a32.9728 32.9728 0 0 0 65.706667 5.597866c5.700267-66.935467-17.7152-132.027733-64.238933-178.517333a225.553067 225.553067 0 0 0-318.634667 0z" p-id="6741"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761042005281" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8444" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M817.834667 627.643733h-272.725334v-80.554666a93.661867 93.661867 0 0 0-33.109333-181.248 93.661867 93.661867 0 0 0-33.109333 181.248v80.554666H206.165333c-49.493333 0-89.770667 40.277333-89.770666 89.770667v114.688c0 49.493333 40.277333 89.429333 89.770666 89.429333h611.669334c49.493333 0 89.770667-39.936 89.770666-89.429333v-114.688c0-49.493333-40.277333-89.770667-89.770666-89.770667z m-598.698667 181.248a34.0992 34.0992 0 0 1-34.474667-34.133333c0-18.773333 15.36-34.133333 34.474667-34.133333 18.773333 0 33.792 15.36 33.792 34.133333 0 19.114667-15.018667 34.133333-33.792 34.133333z m136.533333 0a34.0992 34.0992 0 0 1-34.474666-34.133333c0-18.773333 15.36-34.133333 34.474666-34.133333 18.773333 0 33.792 15.36 33.792 34.133333 0 19.114667-15.018667 34.133333-33.792 34.133333z m136.533334 0a34.0992 34.0992 0 0 1-34.474667-34.133333c0-18.773333 15.36-34.133333 34.474667-34.133333 18.773333 0 33.792 15.36 33.792 34.133333 0 19.114667-15.018667 34.133333-33.792 34.133333z" p-id="8445"></path><path d="M190.293333 508.791467a32.9728 32.9728 0 0 0 31.300267-34.542934A292.181333 292.181333 0 0 1 306.176 253.508267c113.595733-113.5616 298.325333-113.5616 411.8528 0a291.7376 291.7376 0 0 1 84.411733 220.706133 32.9728 32.9728 0 0 0 65.8432 3.310933c5.085867-101.034667-32.699733-199.68-103.6288-270.6432-139.264-139.195733-365.841067-139.264-505.105066 0a358.126933 358.126933 0 0 0-103.799467 270.609067 33.041067 33.041067 0 0 0 34.542933 31.300267z" p-id="8446"></path><path d="M352.768 300.1344c-46.523733 46.557867-69.973333 111.616-64.375467 178.517333 1.501867 18.090667 17.066667 31.880533 35.601067 30.037334a32.938667 32.938667 0 0 0 30.1056-35.601067c-3.9936-47.5136 12.4928-93.559467 45.294933-126.327467a159.573333 159.573333 0 0 1 225.416534-0.034133c32.768 32.733867 49.186133 78.813867 45.124266 126.327467a32.9728 32.9728 0 0 0 65.706667 5.597866c5.700267-66.935467-17.7152-132.027733-64.238933-178.517333a225.553067 225.553067 0 0 0-318.634667 0z" p-id="8447"></path></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,4 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="#5f6368"
d="M20 6h-8l-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2zm0 12H4V8h16v10z" />
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761041450663" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5826" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M888 128H170.4c-39.6 0-71.9 32.2-72 71.7-0.3 2.7-0.4 5.5-0.4 8.2v607.6c0 2.8 0.1 5.5 0.4 8.3 0.2 39.5 32.4 71.7 72 71.7h4.6c1 0 2 0.1 3 0.1h29c1 0 2 0 3-0.1h678c39.7 0 72-32.3 72-72V200c0-39.7-32.3-72-72-72z m-220 72v623.6H515.5V200H668zM192.5 447.1c-19.9 0-36-16.1-36-36s16.1-36 36-36 36 16.1 36 36-16.1 36-36 36z m0-118.3c-19.9 0-36-16.1-36-36s16.1-36 36-36 36 16.1 36 36-16.1 36-36 36zM286.6 200h157v623.6h-157c0.3-2.6 0.4-5.3 0.4-7.9V208c0-2.7-0.2-5.4-0.4-8zM888 823.6H740V200h148v623.6z" p-id="5827"></path><path d="M329.8 256.3h72v120h-72zM555.8 256.3h72v120h-72zM777.2 256.3h72v120h-72z" p-id="5828"></path></svg>

Before

Width:  |  Height:  |  Size: 198 B

After

Width:  |  Height:  |  Size: 951 B

View File

@ -1,4 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="#5f6368" stroke="null"
d="M16.85 17.33a.44.44 0 0 1 .26.77 7.95 7.95 0 0 1-11.18-1.08 7.95 7.95 0 0 1-1.18-1.93.44.44 0 0 1 .6-.56 33.9 33.9 0 0 0 5.44 1.92 30.2 30.2 0 0 0 6.06.88zm-5.23-4.3a19.08 19.08 0 0 0 7.95.6.44.44 0 0 0 .33-.35A7.95 7.95 0 0 0 4.5 9.53a.43.43 0 0 0 .14.47 19.2 19.2 0 0 0 6.98 3.05v-.02zm10.92 1.33c.06-.22.08-.45.06-.67a.89.89 0 0 0-1.77.22c-.18.66-3.6 1.45-9.43 0-4.41-1.11-7.43-2.88-8.12-4.05a.58.58 0 0 1-.12-.37.88.88 0 0 0-1.7-.46 2.19 2.19 0 0 0 .29 1.72c1.07 1.8 4.68 3.72 9.2 4.86a27.58 27.58 0 0 0 6.48.89c2.65.01 4.7-.59 5.11-2.11v-.03z" />
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761042033823" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9763" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M911.6 645.7h-72.8V499.2H524.1V378.3h72.8c26.7 0 48.4-21.7 48.4-48.4v-97.2c0-26.7-21.7-48.4-48.4-48.4H427.1c-26.7 0-48.4 21.7-48.4 48.4v97.2c0 26.7 21.7 48.4 48.4 48.4h72.8v120.8H185.2v146.5h-72.8C85.7 645.6 64 667.3 64 694v97.2c0 26.7 21.7 48.4 48.4 48.4h169.8c26.7 0 48.4-21.7 48.4-48.4V694c0-26.7-21.7-48.4-48.4-48.4h-72.8V523.4h290.5v122.3h-72.8c-26.7 0-48.4 21.7-48.4 48.4v97.2c0 26.7 21.7 48.4 48.4 48.4h169.8c26.7 0 48.4-21.7 48.4-48.4v-97.2c0-26.7-21.7-48.4-48.4-48.4h-72.8V523.4h290.5v122.3h-72.8c-26.7 0-48.4 21.7-48.4 48.4v97.2c0 26.7 21.7 48.4 48.4 48.4h169.8c26.7 0 48.4-21.7 48.4-48.4v-97.2c0-26.8-21.7-48.4-48.4-48.4z" p-id="9764"></path></svg>

Before

Width:  |  Height:  |  Size: 669 B

After

Width:  |  Height:  |  Size: 990 B

View File

@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1760837779866" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4564" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M938.666667 170.666667v-21.333334C938.666667 89.6 891.733333 42.666667 832 42.666667S725.333333 89.6 725.333333 149.333333V170.666667c-25.6 0-42.666667 17.066667-42.666666 42.666666v170.666667c0 25.6 17.066667 42.666667 42.666666 42.666667h213.333334c25.6 0 42.666667-17.066667 42.666666-42.666667V213.333333c0-25.6-17.066667-42.666667-42.666666-42.666666z m-34.133334 0h-145.066666v-21.333334c0-38.4 34.133333-72.533333 72.533333-72.533333 38.4 0 72.533333 34.133333 72.533333 72.533333V170.666667z m-98.133333 341.333333c0 12.8 4.266667 29.866667 4.266667 42.666667 0 89.6-34.133333 170.666667-89.6 230.4-12.8-34.133333-42.666667-59.733333-81.066667-59.733334h-42.666667v-128c0-25.6-17.066667-42.666667-42.666666-42.666666H298.666667v-85.333334h85.333333c25.6 0 42.666667-17.066667 42.666667-42.666666V341.333333h85.333333c46.933333 0 85.333333-38.4 85.333333-85.333333V149.333333c-38.4-12.8-85.333333-21.333333-128-21.333333C234.666667 128 42.666667 320 42.666667 554.666667s192 426.666667 426.666666 426.666666 426.666667-192 426.666667-426.666666c0-12.8 0-29.866667-4.266667-42.666667h-85.333333zM426.666667 891.733333C260.266667 870.4 128 725.333333 128 554.666667c0-25.6 4.266667-51.2 8.533333-76.8L341.333333 682.666667v42.666666c0 46.933333 38.4 85.333333 85.333334 85.333334v81.066666z" p-id="4565"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761041980620" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7231" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M938.666667 170.666667 938.666667 149.333333C938.666667 89.6 891.733333 42.666667 832 42.666667 772.266667 42.666667 725.333333 89.6 725.333333 149.333333L725.333333 170.666667c-25.6 0-42.666667 17.066667-42.666667 42.666667l0 170.666667c0 25.6 17.066667 42.666667 42.666667 42.666667l213.333333 0c25.6 0 42.666667-17.066667 42.666667-42.666667L981.333333 213.333333C981.333333 187.733333 964.266667 170.666667 938.666667 170.666667zM904.533333 170.666667l-145.066667 0L759.466667 149.333333c0-38.4 34.133333-72.533333 72.533333-72.533333 38.4 0 72.533333 34.133333 72.533333 72.533333L904.533333 170.666667zM806.4 512c0 12.8 4.266667 29.866667 4.266667 42.666667 0 89.6-34.133333 170.666667-89.6 230.4-12.8-34.133333-42.666667-59.733333-81.066667-59.733333l-42.666667 0 0-128c0-25.6-17.066667-42.666667-42.666667-42.666667L298.666667 554.666667l0-85.333333 85.333333 0c25.6 0 42.666667-17.066667 42.666667-42.666667L426.666667 341.333333l85.333333 0c46.933333 0 85.333333-38.4 85.333333-85.333333L597.333333 149.333333C558.933333 136.533333 512 128 469.333333 128 234.666667 128 42.666667 320 42.666667 554.666667c0 234.666667 192 426.666667 426.666667 426.666667 234.666667 0 426.666667-192 426.666667-426.666667 0-12.8 0-29.866667-4.266667-42.666667L806.4 512zM426.666667 891.733333c-166.4-21.333333-298.666667-166.4-298.666667-337.066667 0-25.6 4.266667-51.2 8.533333-76.8L341.333333 682.666667l0 42.666667c0 46.933333 38.4 85.333333 85.333333 85.333333L426.666667 891.733333z" p-id="7232"></path></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -474,7 +474,7 @@ header h3 a {
}
header.with-shadow {
background: var(--md-sys-color-card-translucent);
background: var(--md-sys-color-card);
box-shadow: 0 .5px 1.5px 0 rgba(0, 0, 0, 19%), 0 0 1px 0 rgba(0, 0, 0, 3.9%)
}

View File

@ -108,7 +108,7 @@ return baseclass.extend({
className = 'tabmenu-item-%s %s'.format(children[i].name, activeClass);
ul.appendChild(E('li', { 'class': className }, [
E('a', { 'href': L.url(url, children[i].name) }, [_(children[i].title)])]));
E('a', { 'href': L.url(url, children[i].name) }, [children[i].name === 'nas' ? 'NAS' : _(children[i].title)])]));
if (isActive)
activeNode = children[i];
@ -196,7 +196,7 @@ return baseclass.extend({
location.href = targetUrl;
}
}).bind(null, submenu, !!submenu.firstElementChild, linkurl)
}, [_(children[i].title)]),
}, [children[i].name === 'nas' ? 'NAS' : _(children[i].title)]),
submenu
]);
@ -216,7 +216,7 @@ return baseclass.extend({
var isActive = (L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0);
ul.appendChild(E('li', { 'class': isActive ? 'active' : null }, [
E('a', { 'href': L.url(children[i].name) }, [_(children[i].title)])
E('a', { 'href': L.url(children[i].name) }, [children[i].name === 'nas' ? 'NAS' : _(children[i].title)])
]));
if (isActive)

View File

@ -1,31 +1,15 @@
#!/bin/sh
changed=0
set_opt() {
local key=$1
local val=$2
if ! uci -q get "luci.$key" 2>/dev/null; then
uci set "luci.$key=$val"
changed=1
fi
}
set_opt themes.Material3 /luci-static/material3
if [ "$PKG_UPGRADE" != 1 ] && [ $changed = 1 ]; then
set_opt main.mediaurlbase /luci-static/material3
if [ "$PKG_UPGRADE" != 1 ]; then
uci get luci.themes.Material3 >/dev/null 2>&1 || \
uci batch <<-EOF
set luci.themes.Material3=/luci-static/material3
set luci.main.mediaurlbase=/luci-static/material3
set luci.themes.Material3Blue=/luci-static/material3-blue
set luci.themes.Material3Green=/luci-static/material3-green
set luci.themes.Material3Red=/luci-static/material3-red
commit luci
EOF
fi
# set_opt themes.Material3Dark /luci-static/material3-dark
# set_opt themes.Material3Light /luci-static/material3-light
set_opt themes.Material3Blue /luci-static/material3-blue
set_opt themes.Material3Green /luci-static/material3-green
set_opt themes.Material3Red /luci-static/material3-red
if [ $changed = 1 ]; then
uci commit luci
fi
exit 0
exit 0

View File

@ -9,7 +9,7 @@
import { getuid, getspnam } from 'luci.core';
const boardinfo = ubus.call('system', 'board');
const darkpref = 'false';
const darkpref = uci.get('luci', 'main', 'darkmode') || 'auto';
let themepref = null;
switch (theme) {
case 'material3-blue': themepref = 'blue'; break;
@ -21,20 +21,10 @@
-%}
<!DOCTYPE html>
<html lang="{{ dispatcher.lang }}" {{ darkpref ? `data-darkmode="${darkpref}"` : '' }} {{ themepref ? `data-theme="${themepref}"` : '' }}>
<html lang="{{ dispatcher.lang }}" data-darkmode="{{ darkpref }}" {{ themepref ? `data-theme="${themepref}"` : '' }}>
<head>
<meta charset="utf-8">
<title>{{ striptags(`${boardinfo.hostname ?? '?'}${node ? ` - ${node.title}` : ''}`) }} - LuCI</title>
{% if (!darkpref): %}
<script>
var mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'),
rootElement = document.querySelector(':root'),
setDarkMode = function(match) { rootElement.setAttribute('data-darkmode', match.matches) };
mediaQuery.addEventListener('change', setDarkMode);
setDarkMode(mediaQuery);
</script>
{% endif %}
<meta name="viewport" content="initial-scale=1.0">
<link rel="stylesheet" href="{{ media }}/cascade.css">
<link rel="stylesheet" media="screen and (max-width: 854px)" href="{{ media }}/mobile.css" />
@ -47,6 +37,159 @@
{% endif %}
<script src="{{ dispatcher.build_url('admin/translations', dispatcher.lang) }}"></script>
<script src="{{ resource }}/cbi.js"></script>
<style title="darkmode-support">
/* Dark mode toggle styles */
:root {
--transition-duration: 0.3s;
--theme-switch-size: 42px;
--theme-switch-height: 24px;
--theme-switch-handle-size: 16px;
}
.theme-switch-container {
display: flex;
align-items: center;
position: relative;
}
.header-controls {
display: flex;
align-items: center;
gap: 16px;
margin-left: auto;
padding: 0 8px;
}
@media (max-width: 768px) {
.header-controls {
gap: 12px;
}
}
.theme-switch {
position: relative;
display: inline-block;
width: var(--theme-switch-size);
height: var(--theme-switch-height);
cursor: pointer;
}
.theme-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
inset: 0;
background-color: var(--switch-bg, #ccc);
transition: all var(--transition-duration) cubic-bezier(0.4, 0, 0.2, 1);
border-radius: var(--theme-switch-height);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.slider:before {
position: absolute;
content: "";
height: var(--theme-switch-handle-size);
width: var(--theme-switch-handle-size);
left: 4px;
bottom: 4px;
background-color: var(--switch-handle, white);
transition: all var(--transition-duration) cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
input:checked + .slider {
background-color: var(--primary);
}
input:checked + .slider:before {
transform: translateX(18px);
}
/* Theme transition */
html {
transition: background-color var(--transition-duration) ease,
color var(--transition-duration) ease;
}
/* Theme toggle icon styles */
.theme-toggle {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
transition: all var(--transition-duration) cubic-bezier(0.4, 0, 0.2, 1);
background: transparent;
}
.theme-toggle:hover {
background-color: var(--md-sys-color-surface-variant);
}
.theme-toggle .icon {
position: absolute;
transition: all var(--transition-duration) cubic-bezier(0.4, 0, 0.2, 1);
fill: var(--md-sys-color-refresh-button);
width: 20px;
height: 20px;
}
.theme-toggle .moon-icon {
opacity: 1;
transform: scale(1) rotate(0);
}
.theme-toggle .sun-icon {
opacity: 0;
transform: scale(0.5) rotate(90deg);
}
html[data-darkmode="true"] .theme-toggle .moon-icon {
opacity: 0;
transform: scale(0.5) rotate(-90deg);
}
html[data-darkmode="true"] .theme-toggle .sun-icon {
opacity: 1;
transform: scale(1) rotate(0);
}
.theme-toggle:hover .icon {
transform: rotate(15deg) scale(1.1);
}
html[data-darkmode="true"] .theme-toggle:hover .icon {
transform: rotate(-15deg) scale(1.1);
}
/* 添加点击效果 */
.theme-toggle:active .icon {
transform: scale(0.95);
}
/* System preference detection */
@media (prefers-color-scheme: dark) {
html[data-darkmode="auto"] {
--primary: #bb86fc;
--surface-1: #121212;
--surface-2: #1e1e1e;
--on-surface: #ffffff;
--on-surface-variant: rgba(255, 255, 255, 0.7);
}
}
</style>
</head>
<body class="lang_{{ dispatcher.lang }} {{ entityencode(striptags(node?.title ?? ''), true) }}" data-page="{{ entityencode(join('-', ctx.request_path), true) }}">
@ -55,11 +198,113 @@
<div class="menu-btn">
<span aria-label="{{ _('Toggle navigation menu') }}"></span>
</div>
<a class="brand" href="/">{{ striptags(boardinfo.hostname ?? '?') }}</a>
<div id="indicators" class="pull-right"></div>
</header>
<a class="brand" href="/">{{ striptags(boardinfo.hostname ?? '?') }}</a>
<div class="header-controls">
<a href="#" class="header-icon theme-toggle" id="theme-toggle" title="{{ _('Toggle dark mode') }}">
<svg class="icon moon-icon" width="20" height="25" viewBox="0 0 24 24">
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1-8.313-12.454z"/>
</svg>
<svg class="icon sun-icon" width="20" height="25" viewBox="0 0 24 24">
<path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/>
</svg>
</a>
<div id="indicators" class="pull-right"></div>
</div>
</header>
<script>
document.addEventListener('DOMContentLoaded', function() {
const toggle = document.getElementById('theme-toggle');
const html = document.documentElement;
const transitionDuration = 300; // ms
// 初始化主题(自动检测+记忆手动设置)
function initTheme() {
const manualMode = localStorage.getItem('luci-theme-manual');
const autoMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'true' : 'false';
const initMode = manualMode || autoMode;
// 添加过渡类
html.classList.add('theme-transition');
html.setAttribute('data-darkmode', initMode);
updateToggleState(initMode);
// 移除过渡类
setTimeout(() => {
html.classList.remove('theme-transition');
}, transitionDuration);
}
// 更新开关状态
function updateToggleState(isDark) {
if (toggle) {
toggle.checked = isDark === 'true';
// 添加切换动画
toggle.classList.add('theme-switching');
setTimeout(() => {
toggle.classList.remove('theme-switching');
}, transitionDuration);
}
}
// 切换主题(单次点击)
function toggleTheme() {
const current = html.getAttribute('data-darkmode');
const newMode = current === 'true' ? 'false' : 'true';
// 添加过渡类
html.classList.add('theme-transition');
// 更新状态
html.setAttribute('data-darkmode', newMode);
localStorage.setItem('luci-theme-manual', newMode);
updateToggleState(newMode);
// 移除过渡类
setTimeout(() => {
html.classList.remove('theme-transition');
}, transitionDuration);
// 同步到服务端
fetch('<%=url("admin/system/set-darkmode")%>', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode: newMode,
source: 'manual'
})
}).catch(error => {
console.error('Failed to sync theme preference:', error);
});
}
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('luci-theme-manual')) {
const newMode = e.matches ? 'true' : 'false';
html.classList.add('theme-transition');
html.setAttribute('data-darkmode', newMode);
updateToggleState(newMode);
setTimeout(() => {
html.classList.remove('theme-transition');
}, transitionDuration);
}
});
// 初始化
initTheme();
// 点击事件
if (toggle) {
toggle.addEventListener('click', function(e) {
e.preventDefault();
toggleTheme();
});
}
});
</script>
<aside class="sidebar">
<ul class="nav" id="topmenu" style="display:none"></ul>
</aside>