diff --git a/ddns-go/Makefile b/ddns-go/Makefile index 217edf02..1c4fc1e5 100644 --- a/ddns-go/Makefile +++ b/ddns-go/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ddns-go -PKG_VERSION:=6.17.0 -PKG_RELEASE:=12 +PKG_VERSION:=6.17.1 +PKG_RELEASE:=13 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)? diff --git a/luci-theme-material3/Readme.md b/luci-theme-material3/Readme.md index 1fa4935c..09021210 100644 --- a/luci-theme-material3/Readme.md +++ b/luci-theme-material3/Readme.md @@ -72,9 +72,18 @@ This is a personal project based on the LuCI Bootstrap theme, imitating the Mate ## 📦 Release Download prebuilt packages from [GitHub Releases](https://github.com/KawaiiHachimi/luci-theme-material3/releases). +Install the matching package on your router: - `luci-theme-material3-*.apk` -> OpenWrt LuCI 25.12+ + + ```sh + apk add --allow-untrusted luci-theme-material3-*.apk + ``` - `luci-theme-material3_*.ipk` -> OpenWrt LuCI 23.05~24.10 + ```sh + opkg install luci-theme-material3_*.ipk + ``` + ## 🙏 Credits - [OpenWrt Luci](https://github.com/openwrt/luci) - [Cursor](https://www.cursor.com/) diff --git a/luci-theme-material3/htdocs/luci-static/material3/cascade.css b/luci-theme-material3/htdocs/luci-static/material3/cascade.css index 483699cc..a719662b 100644 --- a/luci-theme-material3/htdocs/luci-static/material3/cascade.css +++ b/luci-theme-material3/htdocs/luci-static/material3/cascade.css @@ -2081,6 +2081,7 @@ header .brand { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + transition: color .3s ease; } header .pull-right { @@ -2147,8 +2148,8 @@ body.sidebar-open header { .menu-btn span::after { display: block; width: 20px; - height: 2px; - border-radius: 1px; + height: 0.1rem; + border-radius: 0.05rem; background: currentColor; transition: transform .3s ease, opacity .3s ease; } @@ -2286,16 +2287,15 @@ body.sidebar-open header { width: 100%; min-width: 100%; } +} - .cbi-dropdown[open].btn>ul.dropdown, - .cbi-dropdown[open].cbi-button>ul.dropdown { - left: 0 !important; - right: auto !important; - width: max-content; - max-width: calc(100vw - 24px); - min-width: 100%; - } - +.cbi-dropdown[open].btn>ul.dropdown, +.cbi-dropdown[open].cbi-button>ul.dropdown { + left: 0 !important; + right: auto !important; + width: max-content; + max-width: calc(100vw - 24px); + min-width: 100%; } .nav { @@ -4670,6 +4670,14 @@ div.cbi-value var.cbi-tooltip-container, color: var(--on-surface-variant); } +:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .router-status-info .settings-info p span:nth-child(2) { + max-width: none; +} + +:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .settings-info p span:nth-child(2) { + vertical-align: top; +} + :is(body[data-page="admin-status-iptables"], body[data-page="admin-status-nftables-iptables"]) .cbi-tabmenu { margin-top: 28px; } @@ -4677,4 +4685,53 @@ div.cbi-value var.cbi-tooltip-container, [data-darkmode="true"] :is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard img.svgmonotone, [data-mode="dark"] :is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard img.svgmonotone { filter: invert(1) brightness(1.65) contrast(.82) opacity(.86); +} + +[data-darkmode="true"] body[data-page^="admin-services-dockerman"] img[src*="dockerman/"][src$=".svg"], +[data-mode="dark"] body[data-page^="admin-services-dockerman"] img[src*="dockerman/"][src$=".svg"] { + filter: invert(1) brightness(1.65) contrast(.82) opacity(.9); +} + +[data-darkmode="true"] body[data-page^="admin-services-dockerman"] img[src*="dockerman/containers.svg"][src$=".svg"], +[data-mode="dark"] body[data-page^="admin-services-dockerman"] img[src*="dockerman/containers.svg"][src$=".svg"] { + filter: none !important; + opacity: .96; +} + +body[data-page^="admin-services-dockerman"] #view a[href*="/dockerman/"] { + color: inherit; +} + +body[data-page^="admin-services-dockerman"] #view div[style*="grid-template-columns"][style*="minmax(300px"] { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) !important; + gap: 10px !important; +} + +body[data-page^="admin-services-dockerman"] #view a[href*="/dockerman/"]>div[style*="min-height: 120px"] { + border: 1px solid var(--border-color-low) !important; + border-radius: 12px !important; + background: var(--background-color-high) !important; + box-shadow: none !important; + transition: background-color .2s ease, border-color .2s ease; +} + +body[data-page^="admin-services-dockerman"] #view a[href*="/dockerman/"]>div[style*="min-height: 120px"]:hover { + border-color: var(--border-color-medium) !important; + background: var(--background-color-low) !important; +} + +body[data-page^="admin-services-dockerman"] #view a[href*="/dockerman/"] div[style*="color: #333"] { + color: var(--on-surface) !important; +} + +body[data-page^="admin-services-dockerman"] #view a[href*="/dockerman/"] span[style*="color: #666"] { + color: var(--on-surface-variant) !important; +} + +body[data-page^="admin-services-dockerman"] #view a[href*="/dockerman/"] strong[style*="color: #0066cc"] { + color: var(--primary-color-high) !important; +} + +body[data-page^="admin-services-dockerman"] #view a[href*="/dockerman/"] strong[style*="color: #28a745"] { + color: var(--success-color-high) !important; } \ No newline at end of file diff --git a/luci-theme-material3/htdocs/luci-static/material3/mobile.css b/luci-theme-material3/htdocs/luci-static/material3/mobile.css index a73a576f..4a120cad 100644 --- a/luci-theme-material3/htdocs/luci-static/material3/mobile.css +++ b/luci-theme-material3/htdocs/luci-static/material3/mobile.css @@ -36,6 +36,24 @@ header h3 a { border-radius: var(--material3-card-radius); } + body[data-page^="admin-services-dockerman"] #view div[style*="grid-template-columns"][style*="minmax(300px"] { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + } + + body[data-page^="admin-services-dockerman"] #view a[href*="/dockerman/"]>div[style*="min-height: 120px"] { + min-height: 96px !important; + padding: 10px !important; + } + + body[data-page^="admin-services-dockerman"] #view a[href*="/dockerman/"] img[src*="dockerman/"][src$=".svg"] { + width: 52px !important; + height: 52px !important; + } + + body[data-page^="admin-services-dockerman"] #view a[href*="/dockerman/"] div[style*="margin-right: 15px"] { + margin-right: 8px !important; + } + .table { display: flex; flex-direction: column; @@ -148,6 +166,8 @@ header h3 a { .tr.table-titles, .tr.dashboard-table-titles, + .thead.dashboard-table-titles, + thead.dashboard-table-titles, .cbi-section-table-titles, .cbi-section-table-descr { display: none; diff --git a/luci-theme-material3/htdocs/luci-static/resources/menu-material3.js b/luci-theme-material3/htdocs/luci-static/resources/menu-material3.js index 3b3f3c1c..8b0e2fd2 100644 --- a/luci-theme-material3/htdocs/luci-static/resources/menu-material3.js +++ b/luci-theme-material3/htdocs/luci-static/resources/menu-material3.js @@ -4,6 +4,7 @@ return baseclass.extend({ __init__() { + this.suppressClick = null; ui.menu.load().then((tree) => this.render(tree)); this.initNavigationShell(); this.initDashboardTables(); @@ -43,6 +44,66 @@ return baseclass.extend({ } }, + shouldSuppressClick(ev) { + const click = this.suppressClick; + + if (!click || Date.now() >= click.until) + return false; + + const dx = Math.abs(ev.clientX - click.x); + const dy = Math.abs(ev.clientY - click.y); + const suppress = dx < 24 && dy < 24; + + if (suppress) + this.suppressClick = null; + + return suppress; + }, + + bindFastTap(target, handler) { + let startX = 0; + let startY = 0; + let moved = false; + + target.addEventListener('pointerdown', ev => { + if (ev.pointerType == 'mouse' || ev.button) + return; + + startX = ev.clientX; + startY = ev.clientY; + moved = false; + }, { passive: true }); + + target.addEventListener('pointermove', ev => { + if (Math.abs(ev.clientX - startX) > 8 || Math.abs(ev.clientY - startY) > 8) + moved = true; + }, { passive: true }); + + target.addEventListener('pointerup', ev => { + if (ev.pointerType == 'mouse' || ev.button || moved) + return; + + this.suppressClick = { + x: ev.clientX, + y: ev.clientY, + until: Date.now() + 500 + }; + ev.preventDefault(); + ev.stopPropagation(); + handler(ev); + }); + + target.addEventListener('click', ev => { + if (this.shouldSuppressClick(ev)) { + ev.preventDefault(); + ev.stopPropagation(); + return; + } + + handler(ev); + }); + }, + closeOtherMenus(currentMenu) { document.querySelectorAll('#topmenu > li.open').forEach(menu => { if (menu === currentMenu) @@ -80,6 +141,14 @@ return baseclass.extend({ if (!button || !sidebar || !overlay) return; + document.addEventListener('click', ev => { + if (!this.shouldSuppressClick(ev)) + return; + + ev.preventDefault(); + ev.stopPropagation(); + }, true); + const close = () => { sidebar.classList.remove('active'); overlay.classList.remove('active'); @@ -89,7 +158,7 @@ return baseclass.extend({ button.setAttribute('aria-expanded', 'false'); }; - button.addEventListener('click', () => { + this.bindFastTap(button, () => { sidebar.classList.toggle('active'); overlay.classList.toggle('active'); const isOpen = sidebar.classList.contains('active'); @@ -107,24 +176,31 @@ return baseclass.extend({ close(); }); - sidebar.addEventListener('click', ev => { + sidebar.addEventListener('pointerdown', ev => { const link = ev.target.closest('a[href]'); if (!link) return; this.createRipple(ev, link); + }, { passive: true }); + + sidebar.addEventListener('click', ev => { + const link = ev.target.closest('a[href]'); + + if (!link) + return; if (link.getAttribute('href') != '#') close(); }); - document.addEventListener('click', ev => { + document.addEventListener('pointerdown', ev => { const target = ev.target.closest('.tabs > li, .cbi-tabmenu > li'); if (target) this.createRipple(ev, target); - }); + }, { passive: true }); sidebar.addEventListener('mouseover', ev => { const link = ev.target.closest('.nav a'); @@ -180,23 +256,29 @@ return baseclass.extend({ updateDashboardTables() { document.querySelectorAll('.Dashboard, .dashboard-bg.box-s1').forEach(scope => { scope.querySelectorAll('.table').forEach(table => { - const rows = Array.prototype.filter.call(table.children, child => child.classList && child.classList.contains('tr')); - const headerRow = rows.find(row => row.querySelector('.th')); + const rows = Array.prototype.filter.call(table.querySelectorAll('.tr, tr'), row => + row.closest('.table') === table); + const headerRow = Array.prototype.find.call(table.children, child => + child.querySelector && child.querySelector('.th, th')) || + rows.find(row => row.querySelector('.th, th')); if (!headerRow) return; - const titles = Array.prototype.map.call(headerRow.children, cell => - cell.classList && cell.classList.contains('th') ? cell.textContent.trim() : ''); + const titleCells = headerRow.querySelectorAll('.th, th'); + const titles = Array.prototype.map.call(titleCells, cell => cell.textContent.trim()); headerRow.classList.add('dashboard-table-titles'); rows.forEach(row => { - if (row === headerRow) + if (row === headerRow || row.querySelector('.th, th')) return; - Array.prototype.forEach.call(row.children, (cell, index) => { - if (!cell.classList || !cell.classList.contains('td') || !titles[index]) + const cells = Array.prototype.filter.call(row.children, cell => + (cell.classList && cell.classList.contains('td')) || cell.tagName == 'TD'); + + cells.forEach((cell, index) => { + if (!titles[index]) return; if (!cell.getAttribute('data-title')) @@ -283,13 +365,6 @@ return baseclass.extend({ 'href': linkurl }; - if (!level && hasSubmenu) { - linkAttrs.click = ev => { - ev.preventDefault(); - this.toggleDropdown(ev.currentTarget.parentNode); - }; - } - const li = E('li', attrs, [ E('a', linkAttrs, [ E('span', { 'class': 'nav-menu-title' }, [_(child.title)]), @@ -297,6 +372,12 @@ return baseclass.extend({ submenu ]); + if (!level && hasSubmenu) + this.bindFastTap(li.firstElementChild, ev => { + ev.preventDefault(); + this.toggleDropdown(li); + }); + if (!level && hasSubmenu && isActive) submenu.style.height = 'auto'; diff --git a/luci-theme-material3/ucode/template/themes/material3/header.ut b/luci-theme-material3/ucode/template/themes/material3/header.ut index 7db55a79..6fa30271 100644 --- a/luci-theme-material3/ucode/template/themes/material3/header.ut +++ b/luci-theme-material3/ucode/template/themes/material3/header.ut @@ -22,23 +22,71 @@ (function() { var mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'), modeKey = 'material3-mode-debug', - rootElement = document.querySelector(':root'), - setDarkMode = function(match) { - var mode = localStorage.getItem(modeKey); + themeKey = 'material3-theme-debug', + rootElement = document.documentElement, + themeColors = { + 'baseline': '#ADA4BB', + 'blue': '#9EA9BA', + 'green': '#9DAD9C', + 'teal': '#96ADAC', + 'orange': '#C0A48B', + 'vivid-orange': '#C5A190', + 'sakura': '#C4A09B', + 'rose': '#C0A0A7', + 'gray': '#B4B4B7' + }; - if (mode == 'light' || mode == 'dark') { - rootElement.setAttribute('data-mode', mode); - rootElement.setAttribute('data-darkmode', mode == 'dark' ? 'true' : 'false'); - } - else { - rootElement.removeAttribute('data-mode'); - rootElement.setAttribute('data-darkmode', match.matches ? 'true' : 'false'); - } - }; + var updateFavicon = function() { + var theme = rootElement.getAttribute('data-theme') || 'baseline', + container = themeColors[theme] || themeColors['baseline'], + svg = '', + href = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); + + document.querySelectorAll('link[rel="icon"]').forEach(function(link) { + if (link.href !== href) { + var newLink = link.cloneNode(); + newLink.href = href; + link.parentNode.replaceChild(newLink, link); + } + }); + document.querySelectorAll('img.material3-logo').forEach(function(img) { + if (img.src !== href) img.src = href; + }); + }; + + var setDarkMode = function(match) { + var mode = localStorage.getItem(modeKey); + + if (mode == 'light' || mode == 'dark') { + rootElement.setAttribute('data-mode', mode); + rootElement.setAttribute('data-darkmode', mode == 'dark' ? 'true' : 'false'); + } + else { + rootElement.removeAttribute('data-mode'); + rootElement.setAttribute('data-darkmode', match.matches ? 'true' : 'false'); + } + }; window.material3ApplyColorMode = setDarkMode; + window.material3UpdateFavicon = updateFavicon; + mediaQuery.addEventListener('change', setDarkMode); setDarkMode(mediaQuery); + + var savedTheme = localStorage.getItem(themeKey); + if (savedTheme) { + rootElement.setAttribute('data-theme', savedTheme); + } + + window.addEventListener('load', function() { + updateFavicon(); + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(m) { + if (m.attributeName === 'data-theme') updateFavicon(); + }); + }); + observer.observe(rootElement, { attributes: true, attributeFilter: ['data-theme'] }); + }); })(); diff --git a/luci-theme-material3/ucode/template/themes/material3/sysauth.ut b/luci-theme-material3/ucode/template/themes/material3/sysauth.ut index 132faf60..d7ed20a5 100644 --- a/luci-theme-material3/ucode/template/themes/material3/sysauth.ut +++ b/luci-theme-material3/ucode/template/themes/material3/sysauth.ut @@ -7,7 +7,7 @@