update 2025-10-25 20:33:15
| @ -28,6 +28,5 @@ | ||||
| 	    }, | ||||
|             "uci": ["ddns-go"] | ||||
|         } | ||||
|     }, | ||||
|     { "user": "ddns-go", "access": { "network.interface.*": { "methods": [ "status" ] } } } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | 
| @ -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 | 
| @ -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 | 
| @ -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 | 
| @ -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%) | ||||
| 	} | ||||
|  | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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 | ||||
| @ -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> | ||||
|  | ||||
 actions-user
					actions-user