mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
🌈 Sync 2026-08-27 14:56:13
This commit is contained in:
@@ -10,7 +10,7 @@ PKG_ARCH_DOCKERMANAGER:=$(ARCH)
|
|||||||
|
|
||||||
PKG_NAME:=dockermanager
|
PKG_NAME:=dockermanager
|
||||||
PKG_VERSION:=dockermanager-runtime-v0.1.1
|
PKG_VERSION:=dockermanager-runtime-v0.1.1
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/dockermanager-runtime-v$(PKG_VERSION)/
|
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/dockermanager-runtime-v$(PKG_VERSION)/
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
@@ -26,7 +26,7 @@ define Package/$(PKG_NAME)
|
|||||||
CATEGORY:=System
|
CATEGORY:=System
|
||||||
TITLE:=Docker Manager
|
TITLE:=Docker Manager
|
||||||
URL:=https://github.com/linkease/linkease-docker-manager
|
URL:=https://github.com/linkease/linkease-docker-manager
|
||||||
DEPENDS:=+docker +dockerd +ca-bundle +luci-lib-linkeaseauth
|
DEPENDS:=+docker +dockerd +ca-bundle
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/$(PKG_NAME)/description
|
define Package/$(PKG_NAME)/description
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
|||||||
PKG_NAME:=linkease-common-bin
|
PKG_NAME:=linkease-common-bin
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=1.7.5
|
PKG_SOURCE_DATE:=1.7.5
|
||||||
PKG_RELEASE:=16
|
PKG_RELEASE:=17
|
||||||
|
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
|||||||
ARCH_HEXCODE=1b0c
|
ARCH_HEXCODE=1b0c
|
||||||
endif
|
endif
|
||||||
|
|
||||||
PKG_SOURCE_VERSION:=29037745f8dba998fb03142d5c349700672b8531
|
PKG_SOURCE_VERSION:=260240d2b75aeefecf9f6f474bcb6e3ae16e8bdc
|
||||||
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
|||||||
PKG_NAME:=linkease
|
PKG_NAME:=linkease
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=1.7.5
|
PKG_SOURCE_DATE:=1.7.5
|
||||||
PKG_RELEASE:=19
|
PKG_RELEASE:=20
|
||||||
|
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
|||||||
ARCH_HEXCODE=1b0c
|
ARCH_HEXCODE=1b0c
|
||||||
endif
|
endif
|
||||||
|
|
||||||
PKG_SOURCE_VERSION:=29037745f8dba998fb03142d5c349700672b8531
|
PKG_SOURCE_VERSION:=260240d2b75aeefecf9f6f474bcb6e3ae16e8bdc
|
||||||
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
|||||||
PKG_NAME:=linkeasefull
|
PKG_NAME:=linkeasefull
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=3.0.13
|
PKG_SOURCE_DATE:=3.0.13
|
||||||
PKG_RELEASE:=18
|
PKG_RELEASE:=19
|
||||||
|
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
|
|||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
endif
|
endif
|
||||||
|
|
||||||
PKG_SOURCE_VERSION:=29037745f8dba998fb03142d5c349700672b8531
|
PKG_SOURCE_VERSION:=260240d2b75aeefecf9f6f474bcb6e3ae16e8bdc
|
||||||
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
|
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2026 LinkEase Contributors
|
||||||
|
#
|
||||||
|
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||||
|
#
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI support for Docker Manager
|
||||||
|
LUCI_DEPENDS:=+dockermanager +luci-lib-linkeaseauth
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
PKG_VERSION:=0.1.1
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
# call BuildPackage - OpenWrt buildroot signature
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
module("luci.controller.dockermanager", package.seeall)
|
||||||
|
|
||||||
|
local APPS_PROXY_PREFIX = "/apps=http://127.0.0.1:19290"
|
||||||
|
|
||||||
|
function index()
|
||||||
|
entry({"admin", "services", "dockermanager_status"}, call("dockermanager_status"))
|
||||||
|
local open = entry({"admin", "services", "dockermanager", "open"}, call("dockermanager_open"))
|
||||||
|
open.leaf = true
|
||||||
|
open.dependent = false
|
||||||
|
open.sysauth = "root"
|
||||||
|
open.sysauth_authenticator = "htmlauth"
|
||||||
|
|
||||||
|
if not nixio.fs.access("/etc/config/dockermanager") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
entry({"admin", "services", "dockermanager"}, cbi("dockermanager"), _("Docker Manager"), 22).dependent = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function uhttpd_has_apps_proxy_prefix()
|
||||||
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
local mappings = uci:get_list("uhttpd", "main", "proxy_prefix") or {}
|
||||||
|
|
||||||
|
for _, mapping in ipairs(mappings) do
|
||||||
|
if mapping == APPS_PROXY_PREFIX then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function uhttpd_supports_proxy_prefix()
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
return sys.call("grep -qr 'proxy_prefix' /etc/init.d/uhttpd /lib/functions /usr/share/uhttpd 2>/dev/null") == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function uhttpd_apps_proxy_available()
|
||||||
|
return uhttpd_supports_proxy_prefix() and uhttpd_has_apps_proxy_prefix()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function normalized_base_path(path)
|
||||||
|
path = path or "/apps/dockermanager/"
|
||||||
|
if path:sub(1, 1) ~= "/" then
|
||||||
|
path = "/" .. path
|
||||||
|
end
|
||||||
|
if path:sub(-1) ~= "/" then
|
||||||
|
path = path .. "/"
|
||||||
|
end
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cookie_encode(value)
|
||||||
|
return tostring(value or ""):gsub("([^A-Za-z0-9._~-])", function(char)
|
||||||
|
return string.format("%%%02X", char:byte())
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function authority_host(authority)
|
||||||
|
if not authority or authority == "" then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
if authority:sub(1, 1) == "[" then
|
||||||
|
return authority:match("^%[([^%]]+)%]") or ""
|
||||||
|
end
|
||||||
|
return authority:match("^([^:]+)") or authority
|
||||||
|
end
|
||||||
|
|
||||||
|
local function url_authority(host, port)
|
||||||
|
if not host or host == "" then
|
||||||
|
host = "127.0.0.1"
|
||||||
|
end
|
||||||
|
if host:find(":") and host:sub(1, 1) ~= "[" then
|
||||||
|
host = "[" .. host .. "]"
|
||||||
|
end
|
||||||
|
return host .. ":" .. tostring(port)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function request_or_lan_host()
|
||||||
|
local http = require "luci.http"
|
||||||
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
local host = authority_host(http.getenv("HTTP_HOST") or "")
|
||||||
|
if host ~= "" then
|
||||||
|
return host
|
||||||
|
end
|
||||||
|
return uci:get("network", "lan", "ipaddr") or "127.0.0.1"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function linkease_auth_url(name)
|
||||||
|
local dispatcher = require "luci.dispatcher"
|
||||||
|
return dispatcher.build_url("admin", "services", "linkease_auth", name)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dockermanager_config()
|
||||||
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
local port = uci:get_first("dockermanager", "dockermanager", "port") or "8192"
|
||||||
|
local base_path = normalized_base_path(uci:get_first("dockermanager", "dockermanager", "base_path"))
|
||||||
|
return port, base_path
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dockermanager_entry_url()
|
||||||
|
local port, base_path = dockermanager_config()
|
||||||
|
if uhttpd_apps_proxy_available() then
|
||||||
|
return base_path
|
||||||
|
end
|
||||||
|
return "http://" .. url_authority(request_or_lan_host(), port) .. base_path
|
||||||
|
end
|
||||||
|
|
||||||
|
function dockermanager_status()
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
local port, base_path = dockermanager_config()
|
||||||
|
|
||||||
|
local status = {
|
||||||
|
running = (sys.call("pidof docker-manager >/dev/null") == 0),
|
||||||
|
port = port,
|
||||||
|
base_path = base_path,
|
||||||
|
lan_ip = uci:get("network", "lan", "ipaddr") or "",
|
||||||
|
proxy_prefix_supported = uhttpd_supports_proxy_prefix(),
|
||||||
|
proxy_prefix_enabled = uhttpd_apps_proxy_available()
|
||||||
|
}
|
||||||
|
|
||||||
|
luci.http.prepare_content("application/json")
|
||||||
|
luci.http.write_json(status)
|
||||||
|
end
|
||||||
|
|
||||||
|
function dockermanager_open()
|
||||||
|
local http = require "luci.http"
|
||||||
|
http.redirect(linkease_auth_url("auth") .. "?return=" .. cookie_encode(dockermanager_entry_url()))
|
||||||
|
end
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
local m, s
|
||||||
|
|
||||||
|
m = Map("dockermanager", translate("Docker Manager"), translate("Docker Manager provides a LinkEase Desktop web module and HTTP API for Docker management."))
|
||||||
|
m:section(SimpleSection).template = "dockermanager_status"
|
||||||
|
|
||||||
|
s = m:section(TypedSection, "dockermanager", translate("Global settings"))
|
||||||
|
s.addremove = false
|
||||||
|
s.anonymous = true
|
||||||
|
|
||||||
|
s:option(Flag, "enabled", translate("Enable")).rmempty = false
|
||||||
|
|
||||||
|
local data_dir = s:option(Value, "data_dir", translate("Data directory"))
|
||||||
|
data_dir.rmempty = false
|
||||||
|
|
||||||
|
local port = s:option(Value, "port", translate("Listen port"))
|
||||||
|
port.default = "8192"
|
||||||
|
port.rmempty = false
|
||||||
|
port.datatype = "port"
|
||||||
|
|
||||||
|
local bind_addr = s:option(Value, "bind_addr", translate("Bind address"))
|
||||||
|
bind_addr.default = "0.0.0.0"
|
||||||
|
bind_addr.rmempty = false
|
||||||
|
|
||||||
|
local base_path = s:option(Value, "base_path", translate("Base path"))
|
||||||
|
base_path.default = "/apps/dockermanager/"
|
||||||
|
base_path.rmempty = false
|
||||||
|
|
||||||
|
return m
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script type="text/javascript">//<![CDATA[
|
||||||
|
XHR.poll(5, '<%=url("admin/services/dockermanager_status")%>', null,
|
||||||
|
function(x, st)
|
||||||
|
{
|
||||||
|
var tb = document.getElementById('dockermanager_status');
|
||||||
|
if (st && tb)
|
||||||
|
{
|
||||||
|
var status = st.running
|
||||||
|
? '<br/><em style=\"color:green\"><%:The Docker Manager service is running.%></em>'
|
||||||
|
: '<br/><em style=\"color:red\"><%:The Docker Manager service is not running.%></em>';
|
||||||
|
tb.innerHTML = status;
|
||||||
|
|
||||||
|
if (st.running)
|
||||||
|
{
|
||||||
|
tb.innerHTML = tb.innerHTML
|
||||||
|
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Open Docker Manager%> \" onclick=\"window.open('<%=url("admin/services/dockermanager/open")%>')\"/>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
//]]></script>
|
||||||
|
|
||||||
|
<fieldset class="cbi-section">
|
||||||
|
<legend><%:Docker Manager Status%></legend>
|
||||||
|
<p id="dockermanager_status">
|
||||||
|
<em><%:Collecting data...%></em>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall
|
PKG_NAME:=luci-app-passwall
|
||||||
PKG_VERSION:=26.8.26
|
PKG_VERSION:=26.8.26
|
||||||
PKG_RELEASE:=246
|
PKG_RELEASE:=247
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
local api = require "luci.passwall.api"
|
local api = require "luci.passwall.api"
|
||||||
local fs = api.fs
|
local fs = api.fs
|
||||||
local sys = api.sys
|
local sys = api.sys
|
||||||
local uci = api.uci
|
|
||||||
local datatypes = api.datatypes
|
local datatypes = api.datatypes
|
||||||
local path = string.format("/usr/share/%s/rules/", api.c_config)
|
local path = string.format("/usr/share/%s/rules/", api.appname)
|
||||||
local gfwlist_path = "/usr/share/passwall/rules/gfwlist"
|
local gfwlist_path = path .. "gfwlist"
|
||||||
local chnlist_path = "/usr/share/passwall/rules/chnlist"
|
local chnlist_path = path .. "chnlist"
|
||||||
local chnroute_path = "/usr/share/passwall/rules/chnroute"
|
local chnroute_path = path .. "chnroute"
|
||||||
|
|
||||||
api.set_default_cbi()
|
m = Map(api.appname)
|
||||||
|
|
||||||
m = Map()
|
|
||||||
|
|
||||||
function clean_text(text)
|
function clean_text(text)
|
||||||
local nbsp = string.char(0xC2, 0xA0) -- 不间断空格(U+00A0)
|
local nbsp = string.char(0xC2, 0xA0) -- 不间断空格(U+00A0)
|
||||||
@@ -317,7 +314,7 @@ if fs.access(chnroute_path) then
|
|||||||
]], translate("Read List"))
|
]], translate("Read List"))
|
||||||
end
|
end
|
||||||
|
|
||||||
m:appendTemplate("/rule_list/js")
|
m:append(Template(api.appname .. "/rule_list/js"))
|
||||||
|
|
||||||
local geo_dir = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/")
|
local geo_dir = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/")
|
||||||
local geosite_path = geo_dir .. "/geosite.dat"
|
local geosite_path = geo_dir .. "/geosite.dat"
|
||||||
@@ -327,7 +324,7 @@ if api.finded_com("geoview") and fs.access(geosite_path) and fs.access(geoip_pat
|
|||||||
s:tab("geoview", translate("Geo View"))
|
s:tab("geoview", translate("Geo View"))
|
||||||
o = s:taboption("geoview", DummyValue, "_geoview_fieldset")
|
o = s:taboption("geoview", DummyValue, "_geoview_fieldset")
|
||||||
o.rawhtml = true
|
o.rawhtml = true
|
||||||
o.template = m:template_path("/rule_list/geoview")
|
o.template = api.appname .. "/rule_list/geoview"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -335,4 +332,4 @@ m.on_before_save = function(self)
|
|||||||
m:set("@global[0]", "flush_set", "1")
|
m:set("@global[0]", "flush_set", "1")
|
||||||
end
|
end
|
||||||
|
|
||||||
return api.return_map(m)
|
return m
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ local map = self.map
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<% if luci.http.formvalue("cbi.apply") == "1" and map.is_js_luci then -%>
|
<% if luci.http.formvalue("cbi.apply") == "1" and map.is_js_luci then -%>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
document.addEventListener("luci-loaded", function() {
|
document.addEventListener("luci-loaded", function() {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<%
|
<%
|
||||||
local map = self.map
|
local api = require "luci.passwall.api"
|
||||||
local api = map.api
|
|
||||||
-%>
|
-%>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<%
|
<%
|
||||||
local map = self.map
|
local api = require "luci.passwall.api"
|
||||||
local api = map.api
|
|
||||||
-%>
|
-%>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
//<![CDATA[
|
//<![CDATA[
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ LUCI_TITLE:=Antitethering module for luci-app-firewall
|
|||||||
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_VERSION:=0.0.7
|
PKG_VERSION:=0.0.7
|
||||||
PKG_RELEASE:=4
|
PKG_RELEASE:=5
|
||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ handle_section(){
|
|||||||
config_get iface "$s" iface
|
config_get iface "$s" iface
|
||||||
config_get proxy "$s" proxy
|
config_get proxy "$s" proxy
|
||||||
|
|
||||||
|
case "$inet" in
|
||||||
|
ipv4) family="ip"; IPT="iptables" ;;
|
||||||
|
ipv6) family="ip6"; IPT="ip6tables" ;;
|
||||||
|
*) family="ip ip6"; IPT="iptables ip6tables" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
[ -n "$iface" ] && ifn="$iface" || ifn="lan"
|
[ -n "$iface" ] && ifn="$iface" || ifn="lan"
|
||||||
DEV=$(ifstatus "$ifn" | jsonfilter -e '@["l3_device"]')
|
DEV=$(ifstatus "$ifn" | jsonfilter -e '@["l3_device"]')
|
||||||
|
|
||||||
@@ -22,6 +28,35 @@ handle_section(){
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Input validation ---
|
||||||
|
validate_ttl(){
|
||||||
|
echo "$1" | grep -qE '^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' || {
|
||||||
|
logger -t ttl "Invalid TTL value: '$1' (must be 0-255)"; exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_ports(){
|
||||||
|
case "$1" in
|
||||||
|
all|http|"") return 0 ;;
|
||||||
|
esac
|
||||||
|
echo "$1" | grep -qE '^[0-9]+(,[0-9]+)*$' || {
|
||||||
|
logger -t ttl "Invalid ports value: '$1'"; exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_iface(){
|
||||||
|
echo "$1" | grep -qE '^[a-zA-Z0-9._-]{1,15}$' || {
|
||||||
|
logger -t ttl "Invalid iface value: '$1'"; exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_proxy(){
|
||||||
|
echo "$1" | grep -qE '^(\[?[0-9a-fA-F:.]+\]?):([0-9]{1,5})$' || {
|
||||||
|
logger -t ttl "Invalid proxy value: '$1'"; exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# --- End validation ---
|
||||||
|
|
||||||
config_load ttl
|
config_load ttl
|
||||||
|
|
||||||
# Choose firewall backend: nft takes priority over iptables
|
# Choose firewall backend: nft takes priority over iptables
|
||||||
|
|||||||
@@ -1,34 +1,4 @@
|
|||||||
# --- Input validation ---
|
# IPTABLES backend
|
||||||
validate_ttl(){
|
|
||||||
echo "$1" | grep -qE '^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' || {
|
|
||||||
logger -t ttl "Invalid TTL value: '$1' (must be 0-255)"; exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_ports(){
|
|
||||||
# accepts: "all", "http", or comma-separated port numbers
|
|
||||||
case "$1" in
|
|
||||||
all|http|"") return 0 ;;
|
|
||||||
esac
|
|
||||||
echo "$1" | grep -qE '^[0-9]+(,[0-9]+)*$' || {
|
|
||||||
logger -t ttl "Invalid ports value: '$1'"; exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_iface(){
|
|
||||||
# interface name: alphanumeric, dash, dot, underscore, max 15 chars
|
|
||||||
echo "$1" | grep -qE '^[a-zA-Z0-9._-]{1,15}$' || {
|
|
||||||
logger -t ttl "Invalid iface value: '$1'"; exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_proxy(){
|
|
||||||
# IP:port or [IPv6]:port
|
|
||||||
echo "$1" | grep -qE '^(\[?[0-9a-fA-F:.]+\]?):([0-9]{1,5})$' || {
|
|
||||||
logger -t ttl "Invalid proxy value: '$1'"; exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# --- End validation ---
|
|
||||||
|
|
||||||
method_ttl(){
|
method_ttl(){
|
||||||
|
|
||||||
@@ -74,6 +44,9 @@ method_proxy(){
|
|||||||
[ -n "$proxy" ] && validate_proxy "$proxy"
|
[ -n "$proxy" ] && validate_proxy "$proxy"
|
||||||
[ -n "$iface" ] && validate_iface "$iface"
|
[ -n "$iface" ] && validate_iface "$iface"
|
||||||
|
|
||||||
|
# check nat66 module
|
||||||
|
[ -f /lib/modules/$(uname -r)/ip6table_nat.ko ] || IPT="iptables"
|
||||||
|
|
||||||
for T in $IPT; do
|
for T in $IPT; do
|
||||||
[ "$proxy" ] && {
|
[ "$proxy" ] && {
|
||||||
IPADDR=${proxy%:*}
|
IPADDR=${proxy%:*}
|
||||||
@@ -123,13 +96,6 @@ method_proxy(){
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# check nat66 module
|
|
||||||
if [ -f /lib/modules/$(uname -r)/ip6table_nat.ko ]; then
|
|
||||||
IPT="iptables ip6tables"
|
|
||||||
else
|
|
||||||
IPT="iptables"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create and flush mangle table
|
# Create and flush mangle table
|
||||||
for T in $IPT; do
|
for T in $IPT; do
|
||||||
|
|||||||
@@ -1,31 +1,4 @@
|
|||||||
# --- Input validation ---
|
# nftables backend
|
||||||
validate_ttl(){
|
|
||||||
echo "$1" | grep -qE '^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' || {
|
|
||||||
logger -t ttl "Invalid TTL value: '$1' (must be 0-255)"; exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_ports(){
|
|
||||||
case "$1" in
|
|
||||||
all|http|"") return 0 ;;
|
|
||||||
esac
|
|
||||||
echo "$1" | grep -qE '^[0-9]+(,[0-9]+)*$' || {
|
|
||||||
logger -t ttl "Invalid ports value: '$1'"; exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_iface(){
|
|
||||||
echo "$1" | grep -qE '^[a-zA-Z0-9._-]{1,15}$' || {
|
|
||||||
logger -t ttl "Invalid iface value: '$1'"; exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_proxy(){
|
|
||||||
echo "$1" | grep -qE '^(\[?[0-9a-fA-F:.]+\]?):([0-9]{1,5})$' || {
|
|
||||||
logger -t ttl "Invalid proxy value: '$1'"; exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# --- End validation ---
|
|
||||||
|
|
||||||
method_ttl(){
|
method_ttl(){
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
|
|||||||
FOOTSTRAP_VERSION?=
|
FOOTSTRAP_VERSION?=
|
||||||
ifneq ($(FOOTSTRAP_VERSION),)
|
ifneq ($(FOOTSTRAP_VERSION),)
|
||||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||||
PKG_RELEASE:=37
|
PKG_RELEASE:=38
|
||||||
endif
|
endif
|
||||||
|
|
||||||
LUCI_TITLE:=Footstrap Theme
|
LUCI_TITLE:=Footstrap Theme
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ function fittersEnabled() {
|
|||||||
function run() {
|
function run() {
|
||||||
if (!fittersEnabled()) return;
|
if (!fittersEnabled()) return;
|
||||||
runAll(_fitters, 'fitter');
|
runAll(_fitters, 'fitter');
|
||||||
/* the page is settled: this is the height the next tick may not go below, and the position the
|
/* make the document whole again before anything lays it out, then take the position the next
|
||||||
* next mutation is measured against — unless a correction is already on its way, which would
|
* mutation is measured against — unless a correction is already on its way, which would make
|
||||||
* make this reference the drifted one */
|
* this reference the drifted one */
|
||||||
holdFloor();
|
holdFloor();
|
||||||
if (!_anchorPending) rememberRest();
|
if (!_anchorPending) rememberRest();
|
||||||
}
|
}
|
||||||
@@ -77,34 +77,44 @@ function run() {
|
|||||||
/* ---- the document may not get shorter while a tick is in flight ----
|
/* ---- the document may not get shorter while a tick is in flight ----
|
||||||
*
|
*
|
||||||
* `dom.content()` — what every LuCI poll calls to refresh a section — empties the container before
|
* `dom.content()` — what every LuCI poll calls to refresh a section — empties the container before
|
||||||
* it refills it, and the engine clamps the reader's offset into the briefly shorter document.
|
* it refills it, and a layout taken while it is empty clamps the reader's offset into a document
|
||||||
* Nothing puts that back, because the shortening was never real.
|
* that was never really that short. Nothing puts that back.
|
||||||
*
|
*
|
||||||
* So the content column keeps a floor: `min-height` at the height it had at the last settled
|
* So each container that a poll empties carries a floor: `min-height` at the height it had at the
|
||||||
* moment. A section emptying inside it takes nothing off the document, so there is nothing to clamp
|
* last settled moment, written BEFORE the tick rather than during it. That distinction is the whole
|
||||||
* into. Re-measured after every settled batch, so a page that genuinely got shorter follows one
|
* mechanism — pinning the container from inside the same statement sequence does nothing, because
|
||||||
* frame later.
|
* `dom.content()` performs no layout and no layout ever sees the pin (measured: 1882px still
|
||||||
|
* clamped away with the pin in place). A floor already standing when the container empties needs no
|
||||||
|
* layout to be seen.
|
||||||
*
|
*
|
||||||
* The floor is on `.fs-content` and not on the container being swapped: pinning the container in
|
* The floor is on those containers and NOT on the column around them, which is where it used to be.
|
||||||
* the same statement sequence does nothing, since `dom.content()` performs no layout and no layout
|
* `min-height` on an ancestor of the engine's own anchor is a suppression trigger —
|
||||||
* ever sees the pin (measured: 1882px still clamped away with the pin in place). Wrapping
|
* css-scroll-anchoring-1 §2.2.2 lists it, and Blink's list (css_properties.json5,
|
||||||
* `dom.content()` itself works, at the price of patching a luci-base API every app shares and up to
|
* `invalidate: [..., "scroll-anchor"]`) is wider still — so a floor on the column bought the clamp
|
||||||
* seven read/write pairs per call; one theme-owned element measured once per batch holds the reader
|
* back by turning the engine's anchoring off: 120px grew above the reader and the page moved all
|
||||||
* just as still (0px against a 337px drift), at 16 measurements against 154 wrapped calls.
|
* 120px under them, on Chromium and Firefox alike. The suppression walks only the path from the
|
||||||
|
* anchor to the scroller, and a container that empties is never on it: either the anchor was inside
|
||||||
|
* it, in which case the engine has lost the anchor anyway, or the anchor is elsewhere and this
|
||||||
|
* container is a sibling.
|
||||||
*
|
*
|
||||||
* Not while the reader scrolls: clearing the floor to re-measure is a layout read, and the floor
|
* Wrapping `dom.content()` itself also works, at the price of patching a luci-base API every app
|
||||||
* staying where it was is still a floor. */
|
* shares and up to seven read/write pairs per call. */
|
||||||
|
const SHRINKS = '.cbi-section > div, .table';
|
||||||
|
|
||||||
|
/* The floor is the height the next tick may not go below, one per container. Cleared before the
|
||||||
|
* read, or each floor measures itself and never comes down; batched into one clear, one read pass
|
||||||
|
* and one write pass, so the whole sweep costs a single forced layout rather than one per element.
|
||||||
|
*
|
||||||
|
* Not while the reader scrolls: clearing to re-measure is a layout read, and a floor staying where
|
||||||
|
* it was is still a floor. */
|
||||||
function holdFloor() {
|
function holdFloor() {
|
||||||
/* only where the theme is responsible: an engine that anchors by itself reads the held height as
|
if (scrolling()) return;
|
||||||
* one more thing that moved (15px off with the floor held, 0px without), and gains nothing from
|
const host = document.getElementById('view');
|
||||||
* it */
|
if (!host) return; /* the login page has no view */
|
||||||
if (ENGINE_ANCHORS || scrolling()) return;
|
const els = host.querySelectorAll(SHRINKS), hs = [];
|
||||||
const el = document.querySelector('.fs-content');
|
els.forEach((el) => { el.style.minHeight = ''; });
|
||||||
if (!el) return; /* the login page has no content column */
|
els.forEach((el) => hs.push(el.offsetHeight));
|
||||||
/* cleared before the read, or the floor measures itself and never comes down */
|
els.forEach((el, i) => { if (hs[i] > 0) el.style.minHeight = hs[i] + 'px'; });
|
||||||
el.style.minHeight = '';
|
|
||||||
const h = el.offsetHeight;
|
|
||||||
if (h > 0) el.style.minHeight = h + 'px';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- is the page moving right now? asked of the position, never of the events ----
|
/* ---- is the page moving right now? asked of the position, never of the events ----
|
||||||
@@ -172,20 +182,14 @@ function scrollTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scrolling() { return Date.now() < _movingUntil; }
|
function scrolling() { return Date.now() < _movingUntil; }
|
||||||
/* how many times the offset has moved in this stretch: >1 is a scroll, 1 is a compensation.
|
|
||||||
* `lateDrift()` is the only caller that needs to tell the two apart. */
|
|
||||||
let _steps = 0;
|
|
||||||
|
|
||||||
function sampleMotion() {
|
function sampleMotion() {
|
||||||
const y = scrollTop();
|
const y = scrollTop();
|
||||||
if (_lastOffset === null || y !== _lastOffset) {
|
if (_lastOffset === null || y !== _lastOffset) {
|
||||||
if (_lastOffset !== null) _steps++;
|
|
||||||
_lastOffset = y;
|
_lastOffset = y;
|
||||||
_movingUntil = Date.now() + SCROLL_IDLE;
|
_movingUntil = Date.now() + SCROLL_IDLE;
|
||||||
}
|
}
|
||||||
if (scrolling()) { requestAnimationFrame(sampleMotion); return; }
|
if (scrolling()) { requestAnimationFrame(sampleMotion); return; }
|
||||||
_sampling = false;
|
_sampling = false;
|
||||||
_steps = 0;
|
|
||||||
/* the reader has stopped, so the floor and the reference both belong to where the page now
|
/* the reader has stopped, so the floor and the reference both belong to where the page now
|
||||||
* stands */
|
* stands */
|
||||||
holdFloor();
|
holdFloor();
|
||||||
@@ -524,12 +528,22 @@ function lateDrift(ref) {
|
|||||||
_lateFrame = requestAnimationFrame(() => {
|
_lateFrame = requestAnimationFrame(() => {
|
||||||
_lateFrame = requestAnimationFrame(() => {
|
_lateFrame = requestAnimationFrame(() => {
|
||||||
_lateFrame = 0;
|
_lateFrame = 0;
|
||||||
/* not `scrolling()`: the engine's own compensation moves the offset and starts the
|
if (!anchorEnabled() || Date.now() < _userUntil) return;
|
||||||
* motion sampler, so gating on it skips every tick this exists for. Two narrower
|
|
||||||
* questions instead — is the reader driving (a gesture), and is the offset streaming
|
|
||||||
* (moving repeatedly, which a one-shot compensation never does). */
|
|
||||||
if (!anchorEnabled() || Date.now() < _userUntil || (scrolling() && _steps > 1)) return;
|
|
||||||
if (_restPage !== pageStamp()) return;
|
if (_restPage !== pageStamp()) return;
|
||||||
|
/* THE OFFSET, NOT THE EVENT STREAM. `scrolling()` cannot answer this one: the engine's
|
||||||
|
* own compensation moves the offset and starts the motion sampler, so gating on it
|
||||||
|
* skips every tick this exists for — and in WebKit a programmatic scroll's event
|
||||||
|
* arrives up to 1.2s late, so the sampler is often not running at all when a flick is
|
||||||
|
* in progress. Asking where the offset stands answers both: the reference was taken
|
||||||
|
* with the reference on a still page, so an offset anywhere else means the reader has
|
||||||
|
* moved since, and whatever this would put back they have already scrolled past. A
|
||||||
|
* correction landing inside a flick is itself a jump (161px, webkit/Overview).
|
||||||
|
*
|
||||||
|
* `ref.at` and not `_restAt`: run() re-remembers between the mutation and this frame,
|
||||||
|
* and where the sampler has not started yet — WebKit again — that re-take records the
|
||||||
|
* offset the reader has already flicked to, so comparing against it compares a value
|
||||||
|
* with itself and lets the correction through (320px, @1440 side, .fs-main scrolling). */
|
||||||
|
if (scrollTop() !== ref.at) return;
|
||||||
/* the tick usually replaces the element this was taken on, so without the section
|
/* the tick usually replaces the element this was taken on, so without the section
|
||||||
* fallback the correction does nothing on the tick it exists for */
|
* fallback the correction does nothing on the tick it exists for */
|
||||||
let el = ref.el, was = ref.top;
|
let el = ref.el, was = ref.top;
|
||||||
@@ -617,23 +631,22 @@ function applyAnchor(ref) {
|
|||||||
function observeContent() {
|
function observeContent() {
|
||||||
if (_mo) return;
|
if (_mo) return;
|
||||||
_mo = new MutationObserver(() => {
|
_mo = new MutationObserver(() => {
|
||||||
/* One correction per engine, and which one depends on whether the engine does the job.
|
/* The theme corrects only where the engine will not. Where it anchors, growth above the
|
||||||
|
* reader is the engine's job and the floor covers the collapse, so there is nothing left for
|
||||||
|
* a correction to do: one written here would read its reference in the same instant the poll
|
||||||
|
* mutated the page, and after a scroll WebKit hands back the new `scrollTop` before the
|
||||||
|
* layout that goes with it, so the drift measures the reader's own move and the correction
|
||||||
|
* undoes it — measured, the page went back to 0 from 591 on every run. A residual check two
|
||||||
|
* frames later was carried for that engine and is gone: with the floor on the containers
|
||||||
|
* rather than on the column the collapse it answered no longer happens, and its own
|
||||||
|
* correction landed inside a flick (161px, webkit/Overview, scroll-anchor).
|
||||||
*
|
*
|
||||||
* Where it anchors, an immediate correction is harmful: its reference is read here, in the
|
* Where the engine does not anchor at all — Safari before 27 — nobody puts the reader back
|
||||||
* same instant the poll mutated the page, and after a scroll WebKit hands back the new
|
* within the frame, so the immediate correction stays, measured against the reference from
|
||||||
* `scrollTop` before the layout that goes with it, so the drift measures the reader's own
|
* the last still page. */
|
||||||
* move and the correction undoes it — measured, the page went back to 0 from 591 on every
|
|
||||||
* run. That engine needs the RESIDUE two frames later, once its own anchoring has run —
|
|
||||||
* lateDrift().
|
|
||||||
*
|
|
||||||
* Where it does not anchor, nobody else puts the reader back within the frame, so the
|
|
||||||
* immediate correction stays, measured against the reference from the last still page. */
|
|
||||||
const settled = _rest;
|
const settled = _rest;
|
||||||
const ref = ENGINE_ANCHORS ? null : anchorFor();
|
const ref = ENGINE_ANCHORS ? null : anchorFor();
|
||||||
run();
|
run();
|
||||||
/* one or the other, never a fallthrough: where the engine does not anchor, `anchorFor()`
|
|
||||||
* returning nothing means it DECIDED not to correct this tick, and handing that case to the
|
|
||||||
* deferred path lands the correction inside a flick instead */
|
|
||||||
if (ENGINE_ANCHORS) lateDrift(settled);
|
if (ENGINE_ANCHORS) lateDrift(settled);
|
||||||
else scheduleAnchor(ref);
|
else scheduleAnchor(ref);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,14 +54,16 @@ function wirePageModules() {
|
|||||||
* ordering the two. This file is required by the footer on every page and evaluates before the
|
* ordering the two. This file is required by the footer on every page and evaluates before the
|
||||||
* router exists.
|
* router exists.
|
||||||
*
|
*
|
||||||
* Bodies are verbatim from upstream except L.itemlist -> window.L.itemlist (the two-L trap,
|
* Bodies are verbatim from upstream except for two deltas: L.itemlist -> window.L.itemlist (the
|
||||||
* docs/spa-router.md). The typeof guards make each a no-op on a full page load, where the
|
* two-L trap, docs/spa-router.md), and renderBox's `[title]` — dom.append parses a scalar child as
|
||||||
* template's own copies win the race. */
|
* innerHTML (luci.js:1395) and an array member as text (:1383), and this file defines the global on
|
||||||
|
* every admin page where upstream defines it on Status -> Overview alone. Same output: nothing in
|
||||||
|
* 24.10, 25.12 or master calls renderBox. The typeof guards make each a no-op on a full page load,
|
||||||
|
* where the template's own copies win the race. */
|
||||||
function ensureOverviewHelpers() {
|
function ensureOverviewHelpers() {
|
||||||
/* eslint-disable no-var -- these three bodies are verbatim copies of LuCI's
|
/* eslint-disable no-var -- these three bodies are copies of LuCI's admin_status/index.ut so
|
||||||
admin_status/index.ut so they can be diffed against upstream when it changes.
|
they can be diffed against upstream when it changes. Modernising the `var`s would break
|
||||||
Modernising the `var`s would break that property, which is what makes carrying
|
that property, which is what makes carrying the copies safe. */
|
||||||
the copies safe. */
|
|
||||||
if (typeof window.progressbar !== 'function')
|
if (typeof window.progressbar !== 'function')
|
||||||
window.progressbar = function(query, value, max, byte) {
|
window.progressbar = function(query, value, max, byte) {
|
||||||
var pg = document.querySelector(query),
|
var pg = document.querySelector(query),
|
||||||
@@ -81,7 +83,7 @@ function ensureOverviewHelpers() {
|
|||||||
childs.unshift(window.L.itemlist(E('span'), [].slice.call(arguments, 3)));
|
childs.unshift(window.L.itemlist(E('span'), [].slice.call(arguments, 3)));
|
||||||
return E('div', { class: 'ifacebox' }, [
|
return E('div', { class: 'ifacebox' }, [
|
||||||
E('div', { class: 'ifacebox-head center ' + (active ? 'active' : '') },
|
E('div', { class: 'ifacebox-head center ' + (active ? 'active' : '') },
|
||||||
E('strong', title)),
|
E('strong', [title])),
|
||||||
E('div', { class: 'ifacebox-body left' }, childs)
|
E('div', { class: 'ifacebox-body left' }, childs)
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,8 +72,12 @@
|
|||||||
* language (measured). The theme's own CSS was rewriting the string the theme's own CSS keyed
|
* language (measured). The theme's own CSS was rewriting the string the theme's own CSS keyed
|
||||||
* off. Columns: 1 Package name | 2 Version | 3 Size | 4 Description | 5 actions. */
|
* off. Columns: 1 Package name | 2 Version | 3 Size | 4 Description | 5 actions. */
|
||||||
/* `overflow-wrap` dropped: base/40-tables.css breaks every cell now and this copy moved nothing
|
/* `overflow-wrap` dropped: base/40-tables.css breaks every cell now and this copy moved nothing
|
||||||
* against 100 matching cells. `word-break` stays — switching IT off moved all 100. */
|
* against 100 matching cells. `word-break` has now gone the same way for a different reason: its
|
||||||
#packages.fs-stacked .td:nth-child(4) { display: block; text-align: start; word-break: break-word; }
|
* neutraliser in theme/30-tables.css stopped excusing carded tables, so this sat under a flag and
|
||||||
|
* could not be read. Nothing renders differently — `break-word` is `word-break: normal` plus
|
||||||
|
* `overflow-wrap: anywhere` and the theme already gives every stacked cell the second half;
|
||||||
|
* measured at 390px, this cell is 322 x 103 px on either side of the change. */
|
||||||
|
#packages.fs-stacked .td:nth-child(4) { display: block; text-align: start; }
|
||||||
#packages.fs-stacked .td:nth-child(4)::before { display: block; margin-bottom: var(--fs-space-1); }
|
#packages.fs-stacked .td:nth-child(4)::before { display: block; margin-bottom: var(--fs-space-1); }
|
||||||
#packages.fs-stacked .td.cbi-section-actions { justify-content: flex-end; margin-top: var(--fs-space-1-5); }
|
#packages.fs-stacked .td.cbi-section-actions { justify-content: flex-end; margin-top: var(--fs-space-1-5); }
|
||||||
#packages.fs-stacked .td.cbi-section-actions::before,
|
#packages.fs-stacked .td.cbi-section-actions::before,
|
||||||
|
|||||||
@@ -167,9 +167,17 @@
|
|||||||
* a value being read.
|
* a value being read.
|
||||||
*
|
*
|
||||||
* The `!important` is the sanctioned kind: it fights an inline `style=`, the one adversary a
|
* The `!important` is the sanctioned kind: it fights an inline `style=`, the one adversary a
|
||||||
* layer cannot reach. */
|
* layer cannot reach.
|
||||||
.table.fs-dt:not(.fs-stacked) .td,
|
*
|
||||||
.table.fs-dt:not(.fs-stacked) .td :not(button):not(.btn):not(input):not(select):not(textarea) {
|
* A CARD IS NOT EXEMPT, though `:not(.fs-stacked)` stood here. The inline alias damages the
|
||||||
|
* MEASUREMENT, and a carded table is not measured — but it is not the only adversary. LuCI's
|
||||||
|
* luci-mod-dashboard ships `.Dashboard … .devices-info .tr .td:first-child { word-break:
|
||||||
|
* break-all }` in view/dashboard/css/custom.css with no layer around it, and `break-all` splits a
|
||||||
|
* token that would have fitted: measured at 390px, `t3e-syp.home.iot` over six lines and the
|
||||||
|
* caption `HOSTNAME` cut as `HOSTNA`/`ME`. A card's own breaking is unchanged — `.td` below keeps
|
||||||
|
* `overflow-wrap: anywhere`, so a token too long for the line still breaks. */
|
||||||
|
.table.fs-dt .td,
|
||||||
|
.table.fs-dt .td :not(button):not(.btn):not(input):not(select):not(textarea) {
|
||||||
word-break: normal !important;
|
word-break: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +518,12 @@
|
|||||||
* a flex-start neighbour stays short and its separator paints 52px higher — a broken line
|
* a flex-start neighbour stays short and its separator paints 52px higher — a broken line
|
||||||
* mid-card (measured). Stretched, both cells take the flex line's height and the
|
* mid-card (measured). Stretched, both cells take the flex line's height and the
|
||||||
* separators meet; content still sits at the top (the cells are blocks). */
|
* separators meet; content still sits at the top (the cells are blocks). */
|
||||||
.table.fs-stacked .tr { display: flex; flex-wrap: wrap; }
|
/* A `<tfoot>` with no `.tr` inside it IS the row — see the footer block below for which view
|
||||||
|
* writes which shape. Here rather than in a rule of its own: `css-dup` compares bodies of three
|
||||||
|
* declarations or more, so a second copy of these two would sit unflagged until they diverged. */
|
||||||
|
.table.fs-stacked .tr,
|
||||||
|
.table.fs-stacked tfoot:not(:has(> .tr)),
|
||||||
|
.table.fs-stacked .tfoot:not(:has(> .tr)) { display: flex; flex-wrap: wrap; }
|
||||||
.table.fs-stacked .td {
|
.table.fs-stacked .td {
|
||||||
display: block; min-width: 0; width: auto;
|
display: block; min-width: 0; width: auto;
|
||||||
padding-block: var(--fs-space-1-5); padding-inline: 0 var(--fs-space-2-5); border: 0; text-align: start;
|
padding-block: var(--fs-space-1-5); padding-inline: 0 var(--fs-space-2-5); border: 0; text-align: start;
|
||||||
@@ -529,9 +542,15 @@
|
|||||||
text-transform: uppercase; color: var(--fs-eyebrow-color);
|
text-transform: uppercase; color: var(--fs-eyebrow-color);
|
||||||
/* @endmirror */
|
/* @endmirror */
|
||||||
}
|
}
|
||||||
|
/* A footer cell is the fourth member here, and it has to carry `[data-title]` in its own selector
|
||||||
|
* to win: `.table.fs-stacked tfoot .td::before` is (0,3,1) against the caption rule's (0,4,0) and
|
||||||
|
* loses wherever it is written. Measured on the wifi footer with the titles stamped: `content`
|
||||||
|
* computed to "SSID" over `Total:`. With the attribute it is (0,4,1) and (0,5,0). */
|
||||||
.table.fs-stacked .td.cbi-section-actions::before,
|
.table.fs-stacked .td.cbi-section-actions::before,
|
||||||
.table.fs-stacked .td:not([data-title])::before,
|
.table.fs-stacked .td:not([data-title])::before,
|
||||||
.table.fs-stacked .td[data-title=""]::before { content: none; }
|
.table.fs-stacked .td[data-title=""]::before,
|
||||||
|
.table.fs-stacked tfoot .td[data-title]::before,
|
||||||
|
.table.fs-stacked .tfoot .td[data-title]::before { content: none; }
|
||||||
/* The row's buttons are not a label/value pair — full width below the pairs.
|
/* The row's buttons are not a label/value pair — full width below the pairs.
|
||||||
* PINNED with the config table's copy — see above. */
|
* PINNED with the config table's copy — see above. */
|
||||||
.table.fs-stacked .td.cbi-section-actions {
|
.table.fs-stacked .td.cbi-section-actions {
|
||||||
@@ -555,6 +574,23 @@
|
|||||||
.table.fs-stacked .td > *,
|
.table.fs-stacked .td > *,
|
||||||
.table.fs-stacked .td .ifacebadge { min-width: 0; max-width: 100%; }
|
.table.fs-stacked .td .ifacebadge { min-width: 0; max-width: 100%; }
|
||||||
|
|
||||||
|
/* ---- the footer, in both shapes upstream writes it ----
|
||||||
|
*
|
||||||
|
* A `<tfoot>` is still a table-footer-group inside a `display: block` table, so in a card it drew
|
||||||
|
* as a box beside the cards: measured at 390px on the station list, 186px wide against 354.
|
||||||
|
* `20_lan.js` builds `tfoot > tr > td` and needs only the footer out of the way, the `.tr` above
|
||||||
|
* already being a flex row; `30_wifi.js` appends the `<td>`s to the `<tfoot>` through E(), where
|
||||||
|
* no parser runs and no row is implied, so there the footer has to become the row — that arm is
|
||||||
|
* up with the `.tr` rule. `:has(> .tr)` tells them apart.
|
||||||
|
*
|
||||||
|
* An empty footer cell goes entirely: a card is a list of facts and an empty cell is not one. The
|
||||||
|
* captions are suppressed up in the `content: none` group, for the specificity reason stated
|
||||||
|
* there. */
|
||||||
|
.table.fs-stacked tfoot:has(> .tr),
|
||||||
|
.table.fs-stacked .tfoot:has(> .tr) { display: block; }
|
||||||
|
.table.fs-stacked tfoot .td:empty,
|
||||||
|
.table.fs-stacked .tfoot .td:empty { display: none; }
|
||||||
|
|
||||||
/* --- the DATA table's flavour (L.ui.Table, tagged .fs-dt) ---
|
/* --- the DATA table's flavour (L.ui.Table, tagged .fs-dt) ---
|
||||||
* Clip rows to the frame's corners: zebra paints its background on the .tr and the frame is
|
* Clip rows to the frame's corners: zebra paints its background on the .tr and the frame is
|
||||||
* overflow:visible, so a striped row's square background pokes past the corners. Safe once
|
* overflow:visible, so a striped row's square background pokes past the corners. Safe once
|
||||||
@@ -572,10 +608,28 @@
|
|||||||
* card's row inset left 41px of the 63px the plate is: measured on Wireless as 10px of text
|
* card's row inset left 41px of the 63px the plate is: measured on Wireless as 10px of text
|
||||||
* hanging below the frame once the table stacked. A row with no rows in it has nothing to inset. */
|
* hanging below the frame once the table stacked. A row with no rows in it has nothing to inset. */
|
||||||
.table.fs-dt.fs-stacked .tr:not(.placeholder) { padding: var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-3); }
|
.table.fs-dt.fs-stacked .tr:not(.placeholder) { padding: var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-3); }
|
||||||
/* `.fs-stacked .td`, not `.table.fs-dt.fs-stacked .td` — the same elements (the class is only
|
/* ---- the pair width, and the adversary specificity cannot reach ----
|
||||||
* ever written onto a .table.fs-dt), but at (0,2,0) instead of (0,4,0): low enough that the
|
*
|
||||||
* unguarded `.table .td.col-N` below, at (0,3,0), beats it on SPECIFICITY, not source order. */
|
* Half the row is what makes the card a card. It was `.fs-stacked .td` at (0,2,0), deliberately
|
||||||
.fs-stacked .td { flex: 1 1 50%; }
|
* low so the unguarded `.table .td.col-N` below, at (0,3,0), could beat it on specificity rather
|
||||||
|
* than source order — which holds against every rule in a LAYER and none outside one.
|
||||||
|
* luci-mod-dashboard ships `@media (max-width: 640px) { .Dashboard … .tr .td { flex: 2 2 10% } }`
|
||||||
|
* unlayered, and that beats every layered rule at any specificity: measured at 390px, four cells
|
||||||
|
* of a station row at 81px on one line, `flex-basis` computing to 10% — the pairs never formed.
|
||||||
|
*
|
||||||
|
* Hence the flag, the sanctioned kind. Spent narrowly, and by SELECTOR because a flag would
|
||||||
|
* outrank these two families as well: `.cbi-section-actions` is a full-width line of buttons,
|
||||||
|
* `[class*="col-"]` is the view author's own weight. The third `:not()` is not one of those — it
|
||||||
|
* is how this rule loses to the meter below, both being flagged: without it (0,5,0) beats (0,4,0)
|
||||||
|
* and the meter silently keeps half a line. */
|
||||||
|
.table.fs-stacked .td:not(.cbi-section-actions):not([class*="col-"]):not(:has(.cbi-progressbar)) {
|
||||||
|
flex: 1 1 50% !important;
|
||||||
|
}
|
||||||
|
/* A meter takes the whole line: `.cbi-progressbar::after` prints the bar's title above its right
|
||||||
|
* edge (25-progressbar.css), which in a card is where the cell's own caption sits — measured at
|
||||||
|
* 390px, `SIGNAL STRENGTH` and `RSSI: 65% (-57dBm)` drawn over each other. Beside the bar instead
|
||||||
|
* would be paid out of the bar, too short at half a row to read as one. */
|
||||||
|
.table.fs-stacked .td:has(.cbi-progressbar) { flex: 1 1 100% !important; }
|
||||||
|
|
||||||
/* ---- column weights: one unguarded block, unguarded on purpose ----
|
/* ---- column weights: one unguarded block, unguarded on purpose ----
|
||||||
* A weight (1-10) is a share of a CARDED row — the JS says which cell is a narrow badge and
|
* A weight (1-10) is a share of a CARDED row — the JS says which cell is a narrow badge and
|
||||||
|
|||||||
@@ -68,6 +68,10 @@
|
|||||||
session_setup() accepted the credentials. Gating the credentials alert on `!auth_plugin` and
|
session_setup() accepted the credentials. Gating the credentials alert on `!auth_plugin` and
|
||||||
rendering the message beside it covers all four states — wrong password, wrong password with a
|
rendering the message beside it covers all four states — wrong password, wrong password with a
|
||||||
challenge pending, the OTP prompt, a failed OTP.
|
challenge pending, the OTP prompt, a failed OTP.
|
||||||
|
|
||||||
|
entityencode where upstream prints raw: without the `!fuser` gate the message renders on the
|
||||||
|
request that posted the form too, and this page is unauthenticated. A plugin's markup travels
|
||||||
|
in `auth_html` (dispatcher.uc:952), so this variable is text.
|
||||||
-#}
|
-#}
|
||||||
{% if (fuser && !auth_plugin): %}
|
{% if (fuser && !auth_plugin): %}
|
||||||
<div class="alert-message error">
|
<div class="alert-message error">
|
||||||
@@ -76,7 +80,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if (auth_message): %}
|
{% if (auth_message): %}
|
||||||
<div class="alert-message{% if (auth_plugin): %} warning{% endif %}">
|
<div class="alert-message{% if (auth_plugin): %} warning{% endif %}">
|
||||||
<p>{{ auth_message }}</p>
|
<p>{{ entityencode(auth_message, true) }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=nexttrace
|
PKG_NAME:=nexttrace
|
||||||
PKG_VERSION:=1.7.2
|
PKG_VERSION:=1.7.3
|
||||||
PKG_RELEASE:=15
|
PKG_RELEASE:=16
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/nxtrace/NTrace-core/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/nxtrace/NTrace-core/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=shadowsocks-rust
|
PKG_NAME:=shadowsocks-rust
|
||||||
PKG_VERSION:=1.24.0
|
PKG_VERSION:=1.25.0
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/shadowsocks/shadowsocks-rust/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/shadowsocks/shadowsocks-rust/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
Reference in New Issue
Block a user