🌴 Sync 2026-03-05 23:51:42

This commit is contained in:
github-actions[bot] committed 2026-03-05 23:51:42 +08:00
1 parent 97c049ed0a
commit 0acff03804
14253 files changed
+3893239

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
#
# Copyright (C) 2008-2014 The LuCI Team <luci@lists.subsignal.org>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Material3 Theme
LUCI_DEPENDS:=
PKG_VERSION:=1.0
PKG_RELEASE:=1
PKG_LICENSE:=Apache-2.0
define Package/luci-theme-material3/postrm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
uci -q delete luci.themes.Material3
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
+41
View File
@@ -0,0 +1,41 @@
# luci-theme-material3
A Material Design 3 theme for OpenWrt Luci.
This is a personal project based on the LuCI Bootstrap theme, imitating the Material Design 3 style. Since I'm not very familiar with web theme design, the CSS code might be a bit messy. Some of the project code was completed with the assistance of Cursor AI.
**Nice to initiate an issue or PR!**
## ✨ Preview
<table>
<tr>
<th>🖥️ Desktop</th>
<th>📱 Mobile</th>
</tr>
<tr>
<td><img src="imgs/md3-desktop-overview.jpg" style="max-height: 600px;" /></td>
<td><img src="imgs/md3-mobile-overview.jpg" style="max-height: 600px;" /></td>
</tr>
</table>
### 🎨 All color schemes
<table>
<tr>
<td><img src="imgs/md3-mobile-default.jpg" /></td>
<td><img src="imgs/md3-mobile-blue.jpg" /></td>
<td><img src="imgs/md3-mobile-green.jpg" /></td>
<td><img src="imgs/md3-mobile-red.jpg" /></td>
</tr>
</table>
## 📝 ToDo
- [X] Dark themes
- [ ] Fix some style issues
- [ ] Add more color schemes
- [ ] Improve to be closer to MD3 principles
- [ ] Optimize CSS...
## 🙏 Credits
- [OpenWrt Luci](https://github.com/openwrt/luci)
- [Cursor](https://www.cursor.com/)
- [MDUI](https://www.mdui.org/zh-cn/)
- [Material Design 3](https://m3.material.io/)
- [Reddit - LuCI theme bootstrap: Some modifications using userstyle](https://www.reddit.com/r/openwrt/comments/qmtmpf/lucithemebootstrap_some_modifications_using/)
@@ -0,0 +1 @@
material3
@@ -0,0 +1 @@
material3
@@ -0,0 +1 @@
material3
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M9 3h2v9H9V3zm0 11h2v2H9v-2z" />
</svg>

After

Width:  |  Height:  |  Size: 136 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="#5f6368"
d="M12.996 11.34h-2.1V9.45h2.1v1.9-.01zm0-6.53h-2.1v1.95h2.1V4.81zm2.5 4.64h-2.11v1.9h2.11v-1.9zm-5-2.3h-2.1v1.92h2.11V7.15h-.01zm2.5 0h-2.1v1.92h2.1V7.15zm8.88 3.2c-.45-.32-1.54-.42-2.34-.26-.1-.76-.55-1.44-1.32-2.05l-.44-.28-.3.44c-.57.9-.73 2.37-.12 3.34-.29.16-.83.35-1.54.35H1.846a6.7 6.7 0 0 0 1.41 5.19c1.19 1.4 2.98 2.11 5.29 2.11 5.03 0 8.78-2.3 10.5-6.53.68 0 2.19 0 2.92-1.44.03-.07.23-.42.26-.55l-.35-.32zm-16.38-.9h-2.11v1.9h2.12v-1.9h-.01zm2.5 0h-2.12v1.9h2.12v-1.9zm2.5 0h-2.12v1.9h2.11v-1.9h.01zm-2.5-2.3h-2.12v1.92h2.12V7.15z" />
</svg>

After

Width:  |  Height:  |  Size: 647 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="#5f6368"
d="m10 15.6 1.5 1.4 5-5-5-5-1.4 1.4 2.6 2.6H3v2h9.7L10 15.6zM19 3H5a2 2 0 0 0-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" />
</svg>

After

Width:  |  Height:  |  Size: 255 B

@@ -0,0 +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="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>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +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="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>

After

Width:  |  Height:  |  Size: 951 B

@@ -0,0 +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="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>

After

Width:  |  Height:  |  Size: 990 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path fill="#5f6368"
d="M4.74 2.25h3.82q2.48 0 2.48 2.49v3.82q0 2.48-2.48 2.48H4.74q-2.49 0-2.49-2.48V4.74q0-2.49 2.49-2.49zm10.13 0h3.82q2.48 0 2.48 2.49v3.82q0 2.48-2.48 2.48h-3.82q-2.49 0-2.49-2.48V4.74q0-2.49 2.49-2.49zM4.74 12.38h3.82q2.48 0 2.48 2.49v3.82q0 2.48-2.48 2.48H4.74q-2.49 0-2.49-2.48v-3.82q0-2.49 2.49-2.49z" />
<path fill="#5f6368"
d="m12.67 14.96 2.3-2.3q1.8-1.8 3.53 0l2.39 2.3q1.72 1.82 0 3.54l-2.4 2.39q-1.71 1.72-3.53 0l-2.3-2.4q-1.8-1.71 0-3.53z" />
</svg>

After

Width:  |  Height:  |  Size: 569 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d='M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z' />
</svg>

After

Width:  |  Height:  |  Size: 295 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="#5f6368"
d="M20 13H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h16c.55 0 1-.45 1-1v-6c0-.55-.45-1-1-1zM7 19c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM20 3H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h16c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zM7 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" />
</svg>

After

Width:  |  Height:  |  Size: 359 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="#5f6368" d="M18.8 8.12a2.91 2.91 0 1 1 2.9-2.91 2.91 2.91 0 0 1-2.9 2.9z" />
<path fill="#5f6368"
d="M15.38 21.7H7.65a5.28 5.28 0 0 1-.74-.05l-.34-.05a4.62 4.62 0 0 1-4.17-4.17l-.05-.34c0-.12-.03-.23-.04-.34l-.02-.4V8.62a5.28 5.28 0 0 1 .06-.74l.05-.34a4.62 4.62 0 0 1 4.17-4.17l.34-.05c.12 0 .23-.03.34-.04l.4-.02h5.9a.96.96 0 0 1 .75.35.98.98 0 0 1 .2.82.13.13 0 0 1 0 .03.12.12 0 0 0 0 .03 2.09 2.09 0 0 0-.05.3 4.52 4.52 0 0 0 .07 1.29l.02.07a2.06 2.06 0 0 0 .07.25 3.34 3.34 0 0 0 .36.9 3.17 3.17 0 0 0 .17.28l.04.06a4.6 4.6 0 0 0 1.18 1.19l.06.03.28.17a3.35 3.35 0 0 0 .9.37 2.06 2.06 0 0 0 .25.07l.07.02a4.63 4.63 0 0 0 .9.08c.13 0 .26 0 .4-.02l.13-.02.17-.02a.13.13 0 0 0 .03 0 .13.13 0 0 1 .03 0 1.01 1.01 0 0 1 .19-.03.97.97 0 0 1 .97.97v5.9a5.23 5.23 0 0 1-.06.74l-.05.33a4.62 4.62 0 0 1-4.18 4.18l-.33.05c-.11 0-.23.03-.34.04l-.4.02zM6.9 16.38a.74.74 0 0 0-.72.73.73.73 0 0 0 .72.72h8.74a.73.73 0 0 0 0-1.45H6.9zm0-3.88a.73.73 0 0 0-.72.72.74.74 0 0 0 .72.73h4.86a.74.74 0 0 0 .73-.73.73.73 0 0 0-.73-.72H6.9z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="#5f6368"
d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z" />
</svg>

After

Width:  |  Height:  |  Size: 774 B

@@ -0,0 +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="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>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" class="icon">
<circle cx="50" cy="50" r="50" fill="#8391AE" stroke="null" />
<path fill="#fff" stroke="null"
d="M75.26 51.66H24.74a9.18 9.18 0 0 0-9.18 9.18v4.6a9.18 9.18 0 0 0 9.18 9.18h50.52a9.18 9.18 0 0 0 9.18-9.18v-4.6a9.18 9.18 0 0 0-9.18-9.18zM59.18 68.88a5.17 5.17 0 1 1 5.17-5.17 5.17 5.17 0 0 1-5.17 5.17zm13.2 0a5.17 5.17 0 1 1 5.17-5.17 5.17 5.17 0 0 1-5.16 5.17zM29.34 30.72c6.9-6.62 14.06-9.83 21.59-9.83s15.04 3.44 21.9 10.24a3.44 3.44 0 1 0 4.73-4.9C69.6 18.26 60.33 14 50.92 14s-18.33 4.06-26.18 11.78a3.44 3.44 0 0 0 4.6 4.94zm3.29 8.1a3.44 3.44 0 0 0 4.59 5.03c8.77-8.17 18.78-7.97 27.4.37a3.44 3.44 0 1 0 4.8-4.96 26.96 26.96 0 0 0-18.5-8.27 26.75 26.75 0 0 0-18.2 7.83h-.1z" />
</svg>

After

Width:  |  Height:  |  Size: 786 B

@@ -0,0 +1,592 @@
header .brand,
header h3 a {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: auto;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 1.6rem
}
@media screen and (max-width:854px) {
.menu-btn,
.table,
.tr,
header .nav {
display: flex
}
.td.cbi-value-field,
.td[data-title]~.td.cbi-section-actions {
align-self: flex-start
}
.td[data-title]::before,
[data-name=bridge-vlan] .td[data-title]::before {
content: attr(data-title)
}
.dropdown-menu>li>a {
padding: .8rem 1.5rem .8rem 3rem
}
body {
grid-template-areas: "header" "main" "footer";
grid-template-columns: 1fr
}
#maincontent {
margin-left: 0
}
footer {
padding-left: 1rem;
padding-right: 1rem
}
.modal {
margin: 1.5em auto;
/* margin-top: 160px; */
}
.modal.login {
margin-top: 160px;
}
h3 {
font-size: 1.2em;
margin: 0
}
.cbi-tabmenu,
.tabs {
margin: 1px 1px 10px;
border-radius: 1rem
}
.cbi-tabmenu>li,
.tabs>li {
height: 32px
}
.cbi-tabmenu>li>a,
.tabs>li>a {
line-height: 30px;
border-radius: 1rem
}
.table {
flex-direction: column;
width: 100%;
padding: 5px 0
}
.tr {
flex-direction: row;
flex-wrap: wrap;
align-items: flex-end;
border-top: 0 solid var(--border-color-medium);
padding: 5px;
margin: 0 -3px
}
.table .td,
.table .th,
.table .tr::before {
flex: 1 1 50%;
align-self: flex-start;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
display: inline-block;
border-top: none;
padding: 3px;
box-sizing: border-box
}
.table .td.cbi-dropdown-open {
overflow: visible
}
.col-1 {
flex: 1 1 30px !important;
-webkit-flex: 1 1 30px !important
}
.col-2 {
flex: 2 2 60px !important;
-webkit-flex: 2 2 60px !important
}
.col-3 {
flex: 3 3 90px !important;
-webkit-flex: 3 3 90px !important
}
.col-4 {
flex: 4 4 120px !important;
-webkit-flex: 4 4 120px !important
}
.col-5 {
flex: 5 5 150px !important;
-webkit-flex: 5 5 150px !important
}
.col-6 {
flex: 6 6 180px !important;
-webkit-flex: 6 6 180px !important
}
.col-7 {
flex: 7 7 210px !important;
-webkit-flex: 7 7 210px !important
}
.col-8 {
flex: 8 8 240px !important;
-webkit-flex: 8 8 240px !important
}
.col-9 {
flex: 9 9 270px !important;
-webkit-flex: 9 9 270px !important
}
.col-10 {
flex: 10 10 300px !important;
-webkit-flex: 10 10 300px !important
}
.td select {
word-wrap: normal
}
.td[data-widget=button],
.td[data-widget=fvalue] {
flex: 1 1 17%;
text-align: left
}
.td.cbi-value-field {
flex-basis: 100%;
margin-bottom: .3em
}
.td.cbi-value-field .cbi-button {
width: 100%
}
.table.cbi-section-table {
border: none;
/* background: 0 0; */
margin: 0
}
.cbi-section-table-descr,
.cbi-section-table-titles,
.cbi-value.hidden,
.tr.table-titles,
[data-page=admin-status-processes] .td[data-widget=button]::before,
[data-page=admin-system-fstab] .td[data-widget=button]::before,
[data-page=admin-system-startup] .td[data-widget=button]::before {
display: none
}
.cbi-section-table-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 0 .5em
}
.cbi-section-table+.cbi-section-create {
/* padding-top: 0; */
}
.tr[data-title]::before {
display: block;
flex: 1 1 100%;
background: var(--background-color-low) !important;
font-size: 16px;
border-bottom: 1px solid var(--border-color-medium)
}
.td[data-title]::before {
font-weight: 700;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre-line
}
.td[data-description]::after {
display: block
}
.td.cbi-section-actions {
overflow: initial;
max-width: 100%;
padding: 3px 2px;
flex-basis: 100%
}
.hide-sm,
.hide-xs {
display: none !important
}
.td.cbi-value-field[data-widget="CBI.Button"],
.td.cbi-value-field[data-widget="CBI.DummyValue"],
.td.cbi-value-field[data-widget="CBI.FlagValue"],
.td.cbi-value-field[data-widget=button],
.td.cbi-value-field[data-widget=dvalue],
.td.cbi-value-field[data-widget=fvalue] {
flex-basis: 50%;
text-align: left
}
.cbi-section-table .tr:hover .td,
.cbi-section-table .tr:hover .th,
.cbi-section-table .tr:hover::before {
background-color: transparent
}
.cbi-tab-descr {
margin: 0 0 .5em
}
.cbi-value {
padding-bottom: .5em;
border-bottom: 1px solid var(--border-color-high);
margin-bottom: .5em;
display: block
}
.cbi-value .cbi-value:last-child {
border-bottom: none;
padding-bottom: inherit;
margin-bottom: inherit
}
.cbi-value label.cbi-value-title {
font-weight: 700
}
.cbi-dropdown:not(.btn):not(.cbi-button),
.cbi-dynlist,
.cbi-select,
.cbi-value-field {
width: 100%;
margin: 0
}
.cbi-dynlist>.item {
min-height: 30px;
display: flex;
align-items: center
}
.cbi-dropdown>ul>li,
input,
select,
textarea {
font-size: 16px !important;
height: auto
}
.cbi-dropdown>ul>li input[type=text] {
line-height: 28px
}
.cbi-dropdown>ul>li input[type=text],
.cbi-value-field .control-group,
input[type=password],
input[type=text],
select {
width: 100%;
height: 30px
}
input.cbi-input-password {
width: calc(100% - 25px)
}
input[type=password]+.cbi-button,
input[type=text]+.cbi-button,
select+.cbi-button {
text-overflow: ellipsis
}
.cbi-value-field input[type=checkbox],
.cbi-value-field input[type=radio] {
margin: 0
}
.btn,
.cbi-button,
button {
font-size: 14px !important;
padding: 0 8px
}
.actions,
.cbi-page-actions {
border-top: none;
padding: 8px
}
[data-name=bridge-vlan] .td[data-title],
[data-page=admin-status-overview] .td[width="33%"] {
flex-basis: 33%
}
header .pull-right .label {
white-space: normal;
display: inline-block;
text-align: center;
line-height: 12px;
margin: 1px 0
}
header>.fill {
padding: 1px
}
header>.fill>.container {
display: flex;
flex-direction: row
}
header .nav {
flex: 3 3 80%;
margin: 2px 5px 2px 0;
flex-wrap: wrap;
justify-content: flex-start
}
header .nav a {
padding: 2px 6px
}
header .pull-right {
padding: 0 8px;
margin: .2em 0 .2em auto;
position: relative;
z-index: 1
}
.dropdown-menu,
.menu-dropdown {
top: 23px
}
.cbi-optionals,
.cbi-section-create {
padding: 7px 0;
}
header .menu-btn {
position: relative;
border-radius: 50%;
z-index: 1
}
header .menu-btn:hover {
background: rgba(0, 0, 0, .05);
transition: .3s
}
.sidebar {
transform: translateX(-100%);
z-index: 900;
border-radius: 0 1rem 1rem 0
}
.sidebar.active {
transform: translateX(0)
}
.sidebar.active::after {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1
}
#cbi-network-switch_vlan .td,
#cbi-network-switch_vlan .th {
flex-basis: 12%
}
#cbi-network-switch_vlan .td.cbi-section-actions>* {
width: auto;
display: block
}
#wifi_assoclist_table .td,
[data-page=admin-status-processes] .td {
flex-basis: 50% !important
}
[data-page=admin-status-processes] .td[data-widget=button] {
flex-basis: 33% !important
}
[data-page=admin-status-processes] .td[data-name=PID],
[data-page=admin-status-processes] .td[data-name=USER] {
flex-basis: 25% !important
}
[data-name=bridge-vlan]>div {
overflow: visible !important
}
[data-page=admin-network-network] .td[data-name="_ifacebox"] {
flex-basis: 40%
}
[data-page=admin-network-network] .td[data-name="_ifacestat"] {
flex-basis: 60%
}
[data-page^=admin-services-openclash] .cbi-value {
overflow-x: auto
}
/* [data-page^=admin-services-openclash] div#cbi-openclash {
overflow-x: scroll
} */
.sidebar-overlay {
opacity: 0;
visibility: hidden
}
.sidebar-overlay.active {
opacity: 1;
visibility: visible
}
header.with-shadow {
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%)
}
header [data-indicator] {
width: 32px;
height: 32px;
padding: 0;
margin: 0 4px;
font-size: 0;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative
}
header [data-indicator="poll-status"]::before {
content: '';
position: absolute;
inset: 0;
margin: auto;
width: 24px;
height: 24px;
}
header [data-indicator="uci-changes"]::before {
content: '';
position: absolute;
inset: 0;
margin: auto;
width: 20px;
height: 20px;
}
}
@media screen and (max-width:330px) {
.cbi-page-actions button,
.cbi-section-actions .cbi-button {
overflow: hidden;
text-overflow: ellipsis
}
.cbi-page-actions {
display: flex;
justify-content: space-between;
margin: 0 -1px;
padding: 0
}
.cbi-page-actions .cbi-button {
flex: 1;
margin: 1px !important;
line-height: 2em
}
.cbi-page-actions .cbi-button-apply,
.cbi-page-actions .cbi-button-negative,
.cbi-page-actions .cbi-button-primary {
flex: 3
}
.cbi-section-actions .cbi-button {
margin: 1px !important
}
body[data-page=admin-network-wireless] .td[data-name="_badge"] {
max-width: 50px;
align-self: center
}
body[data-page=admin-network-wireless] .td[data-name="_badge"] .ifacebadge {
display: flex;
align-items: center;
flex-direction: column
}
body[data-page=admin-network-wireless] .td[data-name="_stat"] {
flex-basis: 60%
}
body[data-page=admin-network-network] .td.cbi-section-actions::before,
body[data-page=admin-network-wireless] .td.cbi-section-actions::before {
content: none !important
}
[data-page=admin-network-network] .td[data-name="_ifacebox"],
[data-page=admin-network-network] .td[data-name="_ifacestat"] {
flex-basis: 100%
}
.td .ifacebox {
width: 100%;
margin: 0 !important;
flex-direction: row
}
.td .ifacebox .ifacebox-head {
min-width: 25%;
justify-content: space-around;
border-radius: 1rem;
}
.td .ifacebox .ifacebox-body,
.td .ifacebox .ifacebox-head {
display: flex;
border-bottom: none;
align-items: center
}
.ifacebox .ifacebox-body>*,
.td .ifacebox .ifacebox-head>* {
margin: .125em
}
}
@@ -0,0 +1,253 @@
'use strict';
'require baseclass';
'require ui';
return baseclass.extend({
__init__: function () {
ui.menu.load().then(L.bind(this.render, this));
this.initMenuToggle();
this.initRippleEffect();
this.initHeaderShadow();
},
createRipple: function (event, target) {
// 移除已有的水波纹
var oldRipple = target.querySelector('.ripple');
if (oldRipple) {
oldRipple.remove();
}
// 创建并设置水波纹
var ripple = document.createElement('div');
ripple.className = 'ripple';
ripple.style.left = event.clientX - target.getBoundingClientRect().left + 'px';
ripple.style.top = event.clientY - target.getBoundingClientRect().top + 'px';
// 添加水波纹并设置自动移除
target.appendChild(ripple);
ripple.addEventListener('animationend', function () {
ripple.remove();
});
},
initMenuToggle: function () {
var menuBtn = document.querySelector('.menu-btn');
var sidebar = document.querySelector('.sidebar');
var body = document.body;
// 创建遮罩层
var overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
document.body.appendChild(overlay);
if (menuBtn && sidebar) {
// 点击菜单按钮
menuBtn.addEventListener('click', function () {
menuBtn.classList.toggle('active');
sidebar.classList.toggle('active');
overlay.classList.toggle('active');
body.style.overflow = sidebar.classList.contains('active') ? 'hidden' : '';
});
// 点击遮罩层关闭菜单
overlay.addEventListener('click', function () {
menuBtn.classList.remove('active');
sidebar.classList.remove('active');
overlay.classList.remove('active');
body.style.overflow = '';
});
}
},
// 关闭其他展开的菜单
closeOtherMenus: function (currentMenu) {
var openMenus = document.querySelectorAll('#topmenu > li.open');
openMenus.forEach(function (menu) {
if (menu !== currentMenu) {
var submenu = menu.querySelector('.dropdown-menu');
if (submenu) {
// 先设置实际高度,以便动画正常工作
submenu.style.height = submenu.scrollHeight + 'px';
// 强制重排
submenu.offsetHeight;
// 开始收起动画
submenu.style.height = '0px';
menu.classList.remove('open');
}
}
});
},
render: function (tree) {
var node = tree,
url = '';
this.renderModeMenu(tree);
if (L.env.dispatchpath.length >= 3) {
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);
}
},
renderTabMenu: function (tree, url, level) {
var container = document.querySelector('#tabmenu'),
ul = E('ul', { 'class': 'tabs' }),
children = ui.menu.getChildren(tree),
activeNode = null;
for (var i = 0; i < children.length; i++) {
var isActive = (L.env.dispatchpath[3 + (level || 0)] == children[i].name),
activeClass = isActive ? ' active' : '',
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].name === 'nas' ? 'NAS' : _(children[i].title)])]));
if (isActive)
activeNode = children[i];
}
if (ul.children.length == 0)
return E([]);
container.appendChild(ul);
container.style.display = '';
if (activeNode)
this.renderTabMenu(activeNode, url + '/' + activeNode.name, (level || 0) + 1);
return ul;
},
renderMainMenu: function (tree, url, level) {
var self = this;
var ul = level ? E('ul', { 'class': 'dropdown-menu' }) : document.querySelector('#topmenu'),
children = ui.menu.getChildren(tree);
if (children.length == 0 || level > 1)
return E([]);
for (var i = 0; i < children.length; i++) {
var submenu = this.renderMainMenu(children[i], url + '/' + children[i].name, (level || 0) + 1),
subclass = (!level && submenu.firstElementChild) ? 'dropdown' : null,
linkclass = (!level && submenu.firstElementChild) ? 'menu' : null,
linkurl = submenu.firstElementChild ? '#' : L.url(url, children[i].name);
var currentPath = L.env.requestpath.join('/');
var itemPath = (url + '/' + children[i].name).replace(/^\/+/, '');
var isActive = currentPath.startsWith(itemPath);
if (isActive && submenu.firstElementChild) {
subclass = 'dropdown open active';
// 直接设置展开状态
submenu.style.display = 'block';
submenu.style.height = 'auto';
}
else if (isActive) {
subclass = 'active';
}
else if (submenu.firstElementChild) {
subclass = 'dropdown';
submenu.style.height = '0px';
}
var li = E('li', {
'class': subclass,
'data-path': itemPath
}, [
E('a', {
'class': linkclass,
'href': linkurl,
'click': (function (submenu, hasSubmenu, targetUrl, ev) {
// 添加水波纹效果
self.createRipple(ev, ev.currentTarget);
if (hasSubmenu) {
ev.preventDefault();
ev.stopPropagation();
var parentLi = ev.currentTarget.parentNode;
var dropdownMenu = submenu;
if (parentLi.classList.contains('open')) {
// 先获取当前高度
dropdownMenu.style.height = dropdownMenu.scrollHeight + 'px';
// 强制重排
dropdownMenu.offsetHeight;
// 开始收起动画
parentLi.classList.remove('open');
dropdownMenu.style.height = '0px';
} else {
self.closeOtherMenus(parentLi);
parentLi.classList.add('open');
// 移除auto和display设置,以便动画生效
dropdownMenu.style.display = '';
dropdownMenu.style.height = dropdownMenu.scrollHeight + 'px';
}
}
else if (targetUrl) {
location.href = targetUrl;
}
}).bind(null, submenu, !!submenu.firstElementChild, linkurl)
}, [children[i].name === 'nas' ? 'NAS' : _(children[i].title)]),
submenu
]);
ul.appendChild(li);
}
ul.style.display = '';
return ul;
},
renderModeMenu: function (tree) {
var ul = document.querySelector('#modemenu'),
children = ui.menu.getChildren(tree);
for (var i = 0; i < children.length; i++) {
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].name === 'nas' ? 'NAS' : _(children[i].title)])
]));
if (isActive)
this.renderMainMenu(children[i], children[i].name);
}
if (ul.children.length > 1)
ul.style.display = '';
},
initRippleEffect: function () {
var self = this;
document.addEventListener('click', function (e) {
// 排除一级菜单的点击,因为它们已经在自己的点击事件中处理了水波纹
var target = e.target.closest('.dropdown-menu>li>a, .tabs>li, .cbi-tabmenu>li');
if (!target) return;
self.createRipple(e, target);
});
},
initHeaderShadow: function () {
var header = document.querySelector('header');
var scrollThreshold = 10; // 滚动阈值
window.addEventListener('scroll', function () {
if (window.scrollY > scrollThreshold) {
header.classList.add('with-shadow');
} else {
header.classList.remove('with-shadow');
}
});
}
});
@@ -0,0 +1,34 @@
'use strict';
'require ui';
'require view';
return view.extend({
render: function () {
var form = document.querySelector('form'),
btn = document.querySelector('button');
var dlg = ui.showModal(
_('Authorization Required'),
[].slice.call(document.querySelectorAll('section > *')),
'login'
);
form.addEventListener('keypress', function (ev) {
if (ev.key == 'Enter')
btn.click();
});
btn.addEventListener('click', function () {
dlg.querySelectorAll('*').forEach(function (node) { node.style.display = 'none' });
dlg.appendChild(E('div', { 'class': 'spinning' }, _('Logging in…')));
form.submit()
});
document.querySelector('input[type="password"]').focus();
return '';
},
addFooter: function () { }
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

@@ -0,0 +1,15 @@
#!/bin/sh
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
exit 0
@@ -0,0 +1 @@
material3
@@ -0,0 +1 @@
material3
@@ -0,0 +1 @@
material3
@@ -0,0 +1,20 @@
{% if (!blank_page): %}
</div>
<footer>
<span>
Powered by
<a href="https://github.com/openwrt/luci" target="_blank" rel="noreferrer">
{{ version.luciname }} ({{ version.luciversion }})</a>
/
<a href="{{ entityencode(version.disturl ?? '#', true) }}" target="_blank" rel="noreferrer">
{{ version.distname }} {{ version.distversion }} ({{ version.distrevision }})</a>
{% if (lua_active): %}
/ {{ _('Lua compatibility mode active') }}
{% endif %}
</span>
<ul class="breadcrumb pull-right" id="modemenu" style="display:none"></ul>
</footer>
<script>L.require('menu-material3')</script>
{% endif %}
</body>
</html>
@@ -0,0 +1,341 @@
{#
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2012 David Menting <david@nut-bolt.nl>
Copyright 2008-2022 Jo-Philipp Wich <jo@mein.io>
Licensed to the public under the Apache License 2.0.
-#}
{%
import { getuid, getspnam } from 'luci.core';
const boardinfo = ubus.call('system', 'board');
const darkpref = uci.get('luci', 'main', 'darkmode') || 'auto';
let themepref = null;
switch (theme) {
case 'material3-blue': themepref = 'blue'; break;
case 'material3-green': themepref = 'green'; break;
case 'material3-red': themepref = 'red'; break;
}
http.prepare_content('text/html; charset=UTF-8');
-%}
<!DOCTYPE html>
<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>
<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" />
<link rel="icon" href="{{ media }}/logo.svg" sizes="any">
{% if (node?.css): %}
<link rel="stylesheet" href="{{ resource }}/{{ node.css }}">
{% endif %}
{% if (css): %}
<style title="text/css">{{ css }}</style>
{% 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) }}">
{% if (!blank_page): %}
<header>
<div class="menu-btn">
<span aria-label="{{ _('Toggle navigation menu') }}"></span>
</div>
<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>
<div id="maincontent" class="container">
{% if (getuid() == 0 && getspnam('root')?.pwdp === ''): %}
<div class="alert-message warning">
<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>
{% endif %}
</div>
{% endif %}
{% if (boardinfo.rootfs_type == "initramfs"): %}
<div class="alert-message warning">
<h4>{{ _('System running in recovery (initramfs) mode.') }}</h4>
<p>{{ _('No changes to settings will be stored and are lost after rebooting. This mode should only be used to install a firmware upgrade') }}</p>
{% if (dispatcher.lookup("admin/system/flash")): %}
<div class="right"><a class="btn" href="{{ dispatcher.build_url("admin/system/flash") }}">{{ _('Go to firmware upgrade...') }}</a></div>
{% endif %}
</div>
{% endif %}
<noscript>
<div class="alert-message warning">
<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>
{% endif %}
@@ -0,0 +1,48 @@
{#
Copyright 2022 Jo-Philipp Wich <jo@mein.io>
Licensed to the public under the Apache License 2.0.
-#}
{% include('header', { blank_page: true }) %}
<section hidden>
<form method="post" class="cbi-map">
<div class="cbi-section">
<div class="cbi-section-node">
<div class="cbi-value">
<label class="cbi-value-title" for="luci_username">{{ _('Username') }}</label>
<div class="cbi-value-field">
<input name="luci_username" id="luci_username" type="text" autocomplete="username" value="{{ entityencode(duser, true) }}">
</div>
</div>
<div class="cbi-value">
<label class="cbi-value-title" for="luci_password">{{ _('Password') }}</label>
<div class="cbi-value-field">
<input name="luci_password" id="luci_password" type="password" autocomplete="current-password">
</div>
</div>
</div>
</div>
</form>
<hr>
{% if (fuser): %}
<div class="alert-message error">
{{ _('Invalid username and/or password! Please try again.') }}
</div>
{% endif %}
<button class="btn cbi-button-positive important">{{ _('Log in') }}</button>
</section>
<div id="view">
<div class="spinning">{{ _('Loading view…') }}</div>
<script>
L.require('ui').then(function(ui) {
ui.instantiateView('material3.sysauth');
});
</script>
</div>
{% include('footer', { blank_page: true }) %}