🎄 Sync 2026-08-30 03:21:07

This commit is contained in:
github-actions[bot]
2026-08-30 03:21:07 +08:00
parent eea38c835c
commit 5093acfe28
30 changed files with 647 additions and 93 deletions
+75
View File
@@ -0,0 +1,75 @@
#
# Copyright (C) 2026 jjm2473@gmail.com
#
# This is free software, licensed under the GNU General Public License v3.
#
include $(TOPDIR)/rules.mk
AGENTFLOW_ARCH_x86_64:=amd64
AGENTFLOW_ARCH_aarch64:=arm64
AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH))
AGENTFLOW_HASH_x86_64:=8ba4f42dc45a38ee54d9716cbe39201bb3d9e947c4a6075d9d7af999ab5d098e
AGENTFLOW_HASH_aarch64:=beb19ca378ea1f15a90a907c4de3244353188cf01287b85bfea68b80b1ed8925
PKG_NAME:=agentflow
PKG_VERSION:=0.3.0-20260824-c588d794
PKG_RELEASE:=1
AGENTFLOW_SOURCE:=agentflow-linux-$(AGENTFLOW_ARCH)
AGENTFLOW_DOWNLOAD:=$(PKG_NAME)-$(PKG_VERSION)-linux-$(AGENTFLOW_ARCH)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
STRIP:=true
RSTRIP:=:
include $(INCLUDE_DIR)/package.mk
define Download/agentflow
URL:=https://fw.koolcenter.com/binary/geili/agentflow/build/
URL_FILE:=$(AGENTFLOW_SOURCE)
FILE:=$(AGENTFLOW_DOWNLOAD)
HASH:=skip
endef
define Package/$(PKG_NAME)
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
TITLE:=AgentFlow
DEPENDS:=@(x86_64||aarch64) +git +mise
URL:=https://agentflow.geili.ai/
endef
define Package/$(PKG_NAME)/description
AgentFlow provides a Web UI for orchestrating coding agents and workflows.
endef
define Package/$(PKG_NAME)/conffiles
/etc/config/agentflow
endef
define Build/Prepare
$(INSTALL_DIR) $(PKG_BUILD_DIR)
$(CP) $(DL_DIR)/$(AGENTFLOW_DOWNLOAD) $(PKG_BUILD_DIR)/agentflow
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/etc/config
$(INSTALL_BIN) $(PKG_BUILD_DIR)/agentflow $(1)/usr/bin/agentflow
$(INSTALL_BIN) ./files/agentflow.init $(1)/etc/init.d/agentflow
$(INSTALL_BIN) ./files/agentflow.uci-default $(1)/etc/uci-defaults/09-agentflow
$(INSTALL_CONF) ./files/agentflow.config $(1)/etc/config/agentflow
endef
$(eval $(call Download,agentflow))
$(eval $(call BuildPackage,$(PKG_NAME)))
+5
View File
@@ -0,0 +1,5 @@
config agentflow
option enabled '0'
option data_dir ''
option host '0.0.0.0'
option port '9000'
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh /etc/rc.common
START=93
USE_PROCD=1
get_config() {
config_get_bool enabled "$1" enabled 0
config_get data_dir "$1" data_dir ""
config_get host "$1" host "0.0.0.0"
config_get port "$1" port "9000"
}
set_mise_env() {
procd_set_param env \
"$@" \
MISE_INSTALLS_DIR="$data_dir/globalmise/mise-installs" \
MISE_DATA_DIR="$data_dir/globalmise/mise-data" \
MISE_CACHE_DIR="$data_dir/globalmise/mise-cache" \
MISE_STATE_DIR="$data_dir/globalmise/mise-state" \
NPM_CONFIG_CACHE="$data_dir/globalmise/npm-cache" \
NPM_CONFIG_USERCONFIG="$data_dir/globalmise/.npmrc" \
PATH="$data_dir/globalmise/mise-data/shims:$PATH"
}
start_service() {
config_load agentflow
config_foreach get_config agentflow
[ "$enabled" = 1 ] || return 1
[ -x /usr/bin/agentflow ] || {
logger -t agentflow "missing executable: /usr/bin/agentflow"
return 1
}
mkdir -p "$data_dir" || return 1
logger -t agentflow "starting AgentFlow on $host:$port"
procd_open_instance
procd_set_param command /usr/bin/agentflow
set_mise_env \
"AGENT_FLOW_DATA=$data_dir/data" \
"AGENT_FLOW_HOST=$host" \
"AGENT_FLOW_PORT=$port"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "agentflow"
}
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
[ ! -f "/usr/share/ucitrack/luci-app-agentflow.json" ] && {
cat > /usr/share/ucitrack/luci-app-agentflow.json << EEOF
{
"config": "agentflow",
"init": "agentflow"
}
EEOF
}
uci -q batch <<-EOF >/dev/null
delete ucitrack.@agentflow[-1]
add ucitrack agentflow
set ucitrack.@agentflow[-1].init=agentflow
commit ucitrack
EOF
/etc/init.d/agentflow enable
/etc/init.d/agentflow start
exit 0
+28
View File
@@ -0,0 +1,28 @@
#
# Copyright (C) 2008-2014 The LuCI Team <luci@lists.subsignal.org>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=AgentFlow
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
LUCI_DEPENDS:=+agentflow +luci-compat
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
define Package/luci-app-agentflow/conffiles
/etc/config/agentflow
endef
define Package/luci-app-agentflow/postrm
#!/bin/sh
rm -f /tmp/luci-indexcache
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,29 @@
local http = require "luci.http"
module("luci.controller.agentflow", package.seeall)
function index()
if not nixio.fs.access("/etc/config/agentflow") then
return
end
local page = entry({"admin", "services", "agentflow"}, cbi("agentflow"), _("AgentFlow"), 100)
page.dependent = true
entry({"admin", "services", "agentflow_status"}, call("agentflow_status"))
end
function agentflow_status()
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
local port = tonumber(uci:get_first("agentflow", "agentflow", "port")) or 9000
if port < 1 or port > 65535 then
port = 9000
end
local status = {
running = (sys.call("pidof agentflow >/dev/null") == 0),
port = port
}
http.prepare_content("application/json")
http.write_json(status)
end
@@ -0,0 +1,48 @@
local jsonc = require "luci.jsonc"
local agentflow = {}
agentflow.blocks = function()
local f = io.popen("lsblk -s -f -b -o NAME,FSSIZE,MOUNTPOINT --json", "r")
local vals = {}
if f then
local ret = f:read("*all")
f:close()
local obj = jsonc.parse(ret)
for _, val in pairs(obj and obj["blockdevices"] or {}) do
local fsize = val["fssize"]
if fsize ~= nil and string.len(fsize) > 10 and val["mountpoint"] then
vals[#vals + 1] = val["mountpoint"]
end
end
end
return vals
end
agentflow.home = function()
local uci = require "luci.model.uci".cursor()
local home_dirs = {}
home_dirs["main_dir"] = uci:get_first("quickstart", "main", "main_dir", "/root")
home_dirs["Configs"] = uci:get_first("quickstart", "main", "conf_dir", home_dirs["main_dir"] .. "/Configs")
return home_dirs
end
agentflow.find_paths = function(blocks, home_dirs)
local default_path = home_dirs["Configs"] .. "/AgentFlow"
local paths = {}
if #blocks == 0 then
table.insert(paths, default_path)
else
for _, val in pairs(blocks) do
table.insert(paths, val .. "/Configs/AgentFlow")
end
if default_path == "/root/Configs/AgentFlow" then
default_path = paths[1]
end
end
return paths, default_path
end
return agentflow
@@ -0,0 +1,32 @@
local m, s
m = Map("agentflow", translate("AgentFlow"), translate("AgentFlow provides a Web UI for orchestrating coding agents and workflows."))
m:section(SimpleSection).template = "agentflow/status"
s = m:section(TypedSection, "agentflow", translate("Global settings"))
s.addremove = false
s.anonymous = true
s:option(Flag, "enabled", translate("Enable")).rmempty = false
local agentflow_model = require "luci.model.agentflow"
local blocks = agentflow_model.blocks()
local home = agentflow_model.home()
local data_dir = s:option(Value, "data_dir", translate("Data directory"))
data_dir.rmempty = false
data_dir.description = translate("Required. AgentFlow stores its configuration, database and workspace data under this directory.")
local paths, default_path = agentflow_model.find_paths(blocks, home)
for _, val in pairs(paths) do
data_dir:value(val, val)
end
data_dir.default = default_path
local port = s:option(Value, "port", translate("Listen port"))
port.default = "9000"
port.rmempty = false
port.datatype = "port"
port.description = translate("Port for the AgentFlow HTTP server.")
return m
@@ -0,0 +1,20 @@
<script type="text/javascript">//<![CDATA[
XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st) {
var el = document.getElementById('agentflow_status');
if (st && el) {
if (!st.running) {
el.innerHTML = '<br/><em style="color:red"><%:The AgentFlow service is not running.%></em>';
} else {
el.innerHTML = '<br/><em style="color:green"><%:The AgentFlow service is running.%></em>'
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open AgentFlow%> \" onclick=\"window.open('http://" + window.location.hostname + ":" + st.port + "/')\"/>";
}
}
});
//]]></script>
<fieldset class="cbi-section">
<legend><%:Status%></legend>
<p id="agentflow_status">
<em><%:Collecting data...%></em>
</p>
</fieldset>
+41
View File
@@ -0,0 +1,41 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "AgentFlow"
msgstr "AgentFlow"
msgid "AgentFlow provides a Web UI for orchestrating coding agents and workflows."
msgstr "AgentFlow 提供用于编排编码智能体和工作流的 Web 管理界面。"
msgid "Click to open AgentFlow"
msgstr "点击打开 AgentFlow"
msgid "Collecting data..."
msgstr "正在获取数据……"
msgid "Data directory"
msgstr "数据目录"
msgid "Enable"
msgstr "启用"
msgid "Global settings"
msgstr "全局设置"
msgid "Listen port"
msgstr "监听端口"
msgid "Port for the AgentFlow HTTP server."
msgstr "AgentFlow HTTP 服务的端口号。"
msgid "Required. AgentFlow stores its configuration, database and workspace data under this directory."
msgstr "必需。AgentFlow 在此目录中保存配置、数据库和工作区数据。"
msgid "Status"
msgstr "状态"
msgid "The AgentFlow service is not running."
msgstr "AgentFlow 服务未运行。"
msgid "The AgentFlow service is running."
msgstr "AgentFlow 服务运行中。"
+1
View File
@@ -0,0 +1 @@
zh-cn
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.8.27
PKG_RELEASE:=104
PKG_RELEASE:=105
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -685,6 +685,8 @@ function rollback_rules()
local geo_dir = (uci_get("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/")
fs.move(bak_dir .. arg_type .. ".dat", geo_dir .. arg_type .. ".dat")
fs.rmdir(bak_dir)
uci_set("@global[0]", "flush_set", "1")
uci_save(true)
http_write_json_ok()
end
@@ -164,6 +164,22 @@ node_socks_bind_local:depends({ node = "", ["!reverse"] = true })
s:tab("DNS", translate("DNS"))
o = s:taboption("DNS", ListValue, "direct_dns_protocol", translate("Direct DNS Protocol"))
o:value("", translate("Auto"))
--o:value("tcp", "TCP")
o:value("udp", "UDP")
o = s:taboption("DNS", Value, "direct_dns", translate("Direct DNS"))
o.datatype = "or(ipaddr,ipaddrport(1))"
o.default = "223.5.5.5"
o:value("223.5.5.5")
o:value("223.6.6.6")
o:value("114.114.114.114")
o:value("119.29.29.29")
o:value("180.76.76.76")
o:depends("direct_dns_protocol", "tcp")
o:depends("direct_dns_protocol", "udp")
o = s:taboption("DNS", ListValue, "direct_dns_query_strategy", translate("Direct Query Strategy"))
o.default = "UseIP"
o:value("UseIP")
@@ -419,6 +419,10 @@ if singbox_tags:find("with_quic") then
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ hysteria2_realms = "1" })
o = s:option(Flag, "hysteria2_realm_upnp", translate("Enable") .. " UPnP/NAT-PMP", translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
o.default = "0"
o:depends({ hysteria2_realms = "1" })
o = s:option(Value, "hysteria2_auth_password", translate("Auth Password"))
o.password = true
o:depends({ protocol = "hysteria2"})
@@ -355,6 +355,10 @@ o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ hysteria2_realms = "1" })
o = s:option(Flag, "hysteria2_realm_upnp", translate("Enable") .. " UPnP/NAT-PMP", translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
o.default = "0"
o:depends({ hysteria2_realms = "1" })
o = s:option(Value, "hysteria2_auth_password", translate("Auth Password"))
o.password = true
o:depends({ protocol = "hysteria2"})
@@ -183,6 +183,10 @@ if singbox_tags:find("with_quic") then
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ hysteria2_realms = "1" })
o = s:option(Flag, "hysteria2_realm_upnp", translate("Enable") .. " UPnP/NAT-PMP", translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
o.default = "0"
o:depends({ hysteria2_realms = "1" })
o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
@@ -148,6 +148,10 @@ o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ hysteria2_realms = "1" })
o = s:option(Flag, "hysteria2_realm_upnp", translate("Enable") .. " UPnP/NAT-PMP", translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
o.default = "0"
o:depends({ hysteria2_realms = "1" })
o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
@@ -68,13 +68,6 @@ local function convert_geofile()
end
local function convert(file_path, prefix, tags)
if next(tags) and fs.access(file_path) then
local md5_file = GEO_VAR.TO_SRS_PATH .. prefix .. ".dat.md5"
local new_md5 = sys.exec("md5sum " .. file_path .. " 2>/dev/null | awk '{print $1}'"):gsub("\n", "")
local old_md5 = sys.exec("[ -f " .. md5_file .. " ] && head -n 1 " .. md5_file .. " | tr -d ' \t\n' || echo ''")
if new_md5 ~= "" and new_md5 ~= old_md5 then
sys.call("printf '%s' " .. new_md5 .. " > " .. md5_file)
sys.call("rm -rf " .. GEO_VAR.TO_SRS_PATH .. prefix .. "-*.srs" )
end
for k in pairs(tags) do
geo_convert_srs({
["geo_path"] = file_path,
@@ -624,6 +617,7 @@ function gen_outbound(flag, node, tag, proxy_table)
realm.scheme = nil
realm.address = nil
realm.port = nil
realm.port_mapping = (node.hysteria2_realm_upnp == "1") and { enabled = true } or nil
return realm
end
return nil
@@ -980,6 +974,7 @@ function gen_config_server(node)
realm.address = nil
realm.port = nil
realm.stun_domain_resolver = "direct"
realm.port_mapping = (node.hysteria2_realm_upnp == "1") and { enabled = true } or nil
return realm
end
return nil
@@ -1122,6 +1117,8 @@ function gen_config(var)
local dns_listen_port = var["dns_listen_port"]
local direct_dns_udp_server = var["direct_dns_udp_server"]
local direct_dns_udp_port = var["direct_dns_udp_port"]
local direct_dns_tcp_server = var["direct_dns_tcp_server"]
local direct_dns_tcp_port = var["direct_dns_tcp_port"]
local direct_dns_query_strategy = var["direct_dns_query_strategy"]
local direct_ipset = var["direct_ipset"]
local direct_nftset = var["direct_nftset"]
@@ -1834,10 +1831,18 @@ function gen_config(var)
server_port = tonumber(direct_dns_udp_port) or 53,
detour = "direct",
})
elseif direct_dns_tcp_server then
table.insert(dns.servers, {
tag = "direct",
type = "tcp",
server = direct_dns_tcp_server,
server_port = tonumber(direct_dns_tcp_port) or 53,
detour = "direct",
})
end
for i, v in pairs(GLOBAL.DNS_SERVER) do
if direct_dns_udp_server then
if direct_dns_udp_server or direct_dns_tcp_server then
v.server.domain_resolver = "direct"
end
table.insert(dns.servers, v.server)
@@ -1902,7 +1907,7 @@ function gen_config(var)
end
end
if direct_dns_udp_server then
if direct_dns_udp_server or direct_dns_tcp_server then
local nodes_domain = {}
local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
@@ -2089,6 +2094,7 @@ function gen_config(var)
fakedns_dns_rule.query_type = { "A", "AAAA" }
end
fakedns_dns_rule.server = fakedns_tag
fakedns_dns_rule.rewrite_ttl = 1
fakedns_dns_rule.disable_cache = true
fakedns_dns_rule.client_subnet = nil
table.insert(dns.rules, fakedns_dns_rule)
@@ -2125,7 +2131,7 @@ function gen_config(var)
query_type = dns_rule_query_type,
server = fakedns_tag,
disable_cache = true,
rewrite_ttl = tonumber(remote_rewrite_ttl)
rewrite_ttl = 1
}
table.insert(dns.rules, fakedns_dns_rule)
end
@@ -318,7 +318,8 @@ function gen_outbound(flag, node, tag, proxy_table)
type = "realm",
settings = {
url = url,
stunServers = stun
stunServers = stun,
portMapping = (node.hysteria2_realm_upnp == "1") and { enabled = true } or nil
}
}
udp[#udp+1] = r
@@ -788,7 +789,8 @@ function gen_config_server(node)
type = "realm",
settings = {
url = url,
stunServers = stun
stunServers = stun,
portMapping = (node.hysteria2_realm_upnp == "1") and { enabled = true } or nil
}
}
udp[#udp+1] = r
@@ -887,6 +889,8 @@ function gen_config(var)
local dns_listen_port = var["dns_listen_port"]
local direct_dns_udp_server = var["direct_dns_udp_server"]
local direct_dns_udp_port = var["direct_dns_udp_port"]
local direct_dns_tcp_server = var["direct_dns_tcp_server"]
local direct_dns_tcp_port = var["direct_dns_tcp_port"]
local direct_dns_query_strategy = var["direct_dns_query_strategy"]
local direct_ipset = var["direct_ipset"]
local direct_nftset = var["direct_nftset"]
@@ -1560,13 +1564,24 @@ function gen_config(var)
port = tonumber(direct_dns_udp_port) or 53,
queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
}
if _direct_dns.address then
table.insert(dns_servers, {
outboundTag = "direct",
server = _direct_dns
})
table.insert(dns_servers, {
outboundTag = "direct",
server = _direct_dns
})
elseif direct_dns_tcp_server then
if api.is_ipv6(direct_dns_tcp_server) then
direct_dns_tcp_server = api.get_ipv6_full(direct_dns_tcp_server)
end
_direct_dns = {
tag = direct_dns_tag,
address = "tcp://" .. direct_dns_tcp_server .. ":" .. tonumber(direct_dns_tcp_port) or 53,
port = tonumber(direct_dns_tcp_port) or 53,
queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
}
table.insert(dns_servers, {
outboundTag = "direct",
server = _direct_dns
})
end
if next(GLOBAL.DNS_HOSTNAME) then
@@ -1687,7 +1702,7 @@ function gen_config(var)
})
end
if direct_dns_udp_server then
if direct_dns_udp_server or direct_dns_tcp_server then
local domain = {}
local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
@@ -23,8 +23,8 @@ local map = self.map
<% if luci.http.formvalue("cbi.apply") == "1" and map.is_js_luci then -%>
<script type="text/javascript">
document.addEventListener("luci-loaded", function() {
L.env.apply_display = 2; //Write whatever you want, it's all just a fake progress bar anyway.
L.env.apply_holdoff = 1;
//L.env.apply_display = 2; //Write whatever you want, it's all just a fake progress bar anyway.
//L.env.apply_holdoff = 1;
let tt;
let flag = 2;
@@ -50,10 +50,12 @@ local map = self.map
}
window.setTimeout(() => {
<% if not map.redirect then -%>
if (tt)
window.clearTimeout(tt);
L.ui.changes.displayStatus(false);
window.location = window.location.href.split('#')[0];
L.ui.changes.displayStatus(false);
<% if not map.redirect then -%>
//window.location = window.location.href.split('#')[0];
window.location = "<%=map.api.url("log")%>";
<%- else %>
window.location.href = "<%=map.redirect%>";
<%- end %>
+3
View File
@@ -2420,6 +2420,9 @@ msgstr "اندازه بسته Gecko (دقیقه)"
msgid "Gecko Packet Size (max)"
msgstr "اندازه بسته Gecko (حداکثر)"
msgid "Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."
msgstr "برای بهبود موفقیت در پانچ کردن حفره، نگاشت پورت UPnP/NAT-PMP را روی گیت‌وی خود فعال کنید."
msgid "valid time (hh:mm)"
msgstr "زمان معتبر (ساعت:میلی‌متر)"
+3
View File
@@ -2418,6 +2418,9 @@ msgstr "Размер упаковки Gecko (мин)"
msgid "Gecko Packet Size (max)"
msgstr "Размер упаковки Gecko (макс)"
msgid "Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."
msgstr "Включите сопоставление портов UPnP/NAT-PMP на вашем шлюзе, чтобы повысить вероятность успешного выполнения операций "пробивания дыр"."
msgid "valid time (hh:mm)"
msgstr "Время действия (чч:мм)"
+3
View File
@@ -2406,6 +2406,9 @@ msgstr "Gecko 包大小(最小)"
msgid "Gecko Packet Size (max)"
msgstr "Gecko 包大小(最大)"
msgid "Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."
msgstr "在网关上启用 UPnP/NAT-PMP 端口映射以提升打洞成功率。"
msgid "valid time (hh:mm)"
msgstr "有效时间(hh:mm"
+3
View File
@@ -2412,6 +2412,9 @@ msgstr "Gecko 包大小(最小)"
msgid "Gecko Packet Size (max)"
msgstr "Gecko 包大小(最大)"
msgid "Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."
msgstr "在網關上啟用 UPnP/NAT-PMP 連接埠對映以提升打洞成功率。"
msgid "valid time (hh:mm)"
msgstr "有效時間(hh:mm"
@@ -99,8 +99,11 @@ run_xray() {
json_add_string "local_http_password" "${http_password}"
}
}
local direct_dns_proto=${DIRECT_DNS_PROTO}
local direct_dns_server=${DIRECT_DNS_SERVER}
local direct_dns_port=${DIRECT_DNS_PORT}
[ -n "$dns_listen_port" ] && {
local dns_msg="DNS[${dns_listen_port}]:($(i18n "Direct DNS: %s" "${AUTO_DNS}")"
local dns_msg="DNS[${dns_listen_port}]:($(i18n "Direct DNS: %s" "${direct_dns_proto}://${direct_dns_server}:${direct_dns_port}")"
json_add_string "dns_listen_port" "${dns_listen_port}"
[ -n "$dns_cache" ] && json_add_string "dns_cache" "${dns_cache}"
[ "${node_protocol}" = "_shunt" ] && local write_ipset_direct=$(config_n_get $node write_ipset_direct 0)
@@ -116,9 +119,10 @@ run_xray() {
local direct_ipset6="psw2_${node}_white6"
local direct_ipset="${direct_ipset4},${direct_ipset6}"
fi
run_ipset_dns_server listen_port=${direct_dnsmasq_listen_port} server_dns=${AUTO_DNS} ipset="${direct_ipset}" nftset="${direct_nftset}" config_file=${direct_ipset_conf}
DIRECT_DNS_UDP_PORT=${direct_dnsmasq_listen_port}
DIRECT_DNS_UDP_SERVER="127.0.0.1"
run_ipset_dns_server listen_port=${direct_dnsmasq_listen_port} proto=${direct_dns_proto} server_dns="${direct_dns_server}#${direct_dns_port}" ipset="${direct_ipset}" nftset="${direct_nftset}" config_file=${direct_ipset_conf}
direct_dns_proto="udp"
direct_dns_server="127.0.0.1"
direct_dns_port=${direct_dnsmasq_listen_port}
[ -n "${direct_ipset}" ] && {
json_add_string "direct_ipset" "${direct_ipset}"
set_cache_var "node_${node}_direct_ipset4" "${direct_ipset4}"
@@ -168,8 +172,8 @@ run_xray() {
[ -n "$remote_dns_client_ip" ] && json_add_string "remote_dns_client_ip" "${remote_dns_client_ip}"
log_out="${dns_msg})"
}
json_add_string "direct_dns_udp_port" "${DIRECT_DNS_UDP_PORT}"
json_add_string "direct_dns_udp_server" "${DIRECT_DNS_UDP_SERVER}"
json_add_string "direct_dns_${direct_dns_proto}_server" "${direct_dns_server}"
json_add_string "direct_dns_${direct_dns_proto}_port" "${direct_dns_port}"
json_add_string "direct_dns_query_strategy" "${direct_dns_query_strategy}"
[ -n "${redir_port}" ] && {
@@ -241,8 +245,11 @@ run_singbox() {
json_add_string "local_http_password" "${http_password}"
}
}
local direct_dns_proto=${DIRECT_DNS_PROTO}
local direct_dns_server=${DIRECT_DNS_SERVER}
local direct_dns_port=${DIRECT_DNS_PORT}
[ -n "$dns_listen_port" ] && {
local dns_msg="DNS[${dns_listen_port}]:($(i18n "Direct DNS: %s" "${AUTO_DNS}")"
local dns_msg="DNS[${dns_listen_port}]:($(i18n "Direct DNS: %s" "${direct_dns_proto}://${direct_dns_server}:${direct_dns_port}")"
json_add_string "dns_listen_port" "${dns_listen_port}"
[ -n "$dns_cache" ] && json_add_string "dns_cache" "${dns_cache}"
[ "${node_protocol}" = "_shunt" ] && local write_ipset_direct=$(config_n_get $node write_ipset_direct 0)
@@ -258,9 +265,10 @@ run_singbox() {
local direct_ipset6="psw2_${node}_white6"
local direct_ipset="${direct_ipset4},${direct_ipset6}"
fi
run_ipset_dns_server listen_port=${direct_dnsmasq_listen_port} server_dns=${AUTO_DNS} ipset="${direct_ipset}" nftset="${direct_nftset}" config_file=${direct_ipset_conf}
DIRECT_DNS_UDP_PORT=${direct_dnsmasq_listen_port}
DIRECT_DNS_UDP_SERVER="127.0.0.1"
run_ipset_dns_server listen_port=${direct_dnsmasq_listen_port} proto=${direct_dns_proto} server_dns="${direct_dns_server}#${direct_dns_port}" ipset="${direct_ipset}" nftset="${direct_nftset}" config_file=${direct_ipset_conf}
direct_dns_proto="udp"
direct_dns_server="127.0.0.1"
direct_dns_port=${direct_dnsmasq_listen_port}
[ -n "${direct_ipset}" ] && {
json_add_string "direct_ipset" "${direct_ipset}"
set_cache_var "node_${node}_direct_ipset4" "${direct_ipset4}"
@@ -318,8 +326,8 @@ run_singbox() {
[ -n "$remote_rewrite_ttl" ] && json_add_string "remote_rewrite_ttl" "${remote_rewrite_ttl}"
log_out="${dns_msg})"
}
json_add_string "direct_dns_udp_port" "${DIRECT_DNS_UDP_PORT}"
json_add_string "direct_dns_udp_server" "${DIRECT_DNS_UDP_SERVER}"
json_add_string "direct_dns_${direct_dns_proto}_server" "${direct_dns_server}"
json_add_string "direct_dns_${direct_dns_proto}_port" "${direct_dns_port}"
json_add_string "direct_dns_query_strategy" "${direct_dns_query_strategy}"
[ -n "${redir_port}" ] && {
@@ -423,8 +431,8 @@ run_socks() {
json_add_string "flag" "${flag}"
json_add_string "local_socks_address" "${bind}"
json_add_string "local_socks_port" "${socks_port}"
json_add_string "direct_dns_udp_port" "${DIRECT_DNS_UDP_PORT}"
json_add_string "direct_dns_udp_server" "${DIRECT_DNS_UDP_SERVER}"
json_add_string "direct_dns_${DIRECT_DNS_PROTO}_server" "${DIRECT_DNS_SERVER}"
json_add_string "direct_dns_${DIRECT_DNS_PROTO}_port" "${DIRECT_DNS_PORT}"
json_add_string "direct_dns_query_strategy" "${DIRECT_DNS_QUERY_STRATEGY}"
local _json_arg="$(json_dump)"
lua $UTIL_SINGBOX gen_config "${_json_arg}" > $config_file
@@ -450,8 +458,8 @@ run_socks() {
json_add_string "flag" "${flag}"
json_add_string "local_socks_address" "${bind}"
json_add_string "local_socks_port" "${socks_port}"
json_add_string "direct_dns_udp_port" "${DIRECT_DNS_UDP_PORT}"
json_add_string "direct_dns_udp_server" "${DIRECT_DNS_UDP_SERVER}"
json_add_string "direct_dns_${DIRECT_DNS_PROTO}_server" "${DIRECT_DNS_SERVER}"
json_add_string "direct_dns_${DIRECT_DNS_PROTO}_port" "${DIRECT_DNS_PORT}"
json_add_string "direct_dns_query_strategy" "${DIRECT_DNS_QUERY_STRATEGY}"
local _json_arg="$(json_dump)"
lua $UTIL_XRAY gen_config "${_json_arg}" > $config_file
@@ -759,7 +767,7 @@ run_ipset_dns_server() {
}
run_ipset_chinadns_ng() {
local listen_port server_dns ipset nftset config_file
local listen_port proto server_dns ipset nftset config_file
eval_set_val $@
[ ! -s "$TMP_ACL_PATH/vpslist" ] && {
node_servers=$(uci show "${CONFIG}" | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2)
@@ -777,14 +785,14 @@ run_ipset_chinadns_ng() {
cat <<-EOF > $config_file
bind-addr 127.0.0.1
bind-port ${listen_port}
china-dns ${server_dns}
trust-dns ${server_dns}
china-dns ${proto}://${server_dns}
trust-dns ${proto}://${server_dns}
filter-qtype 65
add-tagchn-ip ${set_names}
default-tag chn
group vpslist
group-dnl $TMP_ACL_PATH/vpslist
group-upstream ${server_dns}
group-upstream ${proto}://${server_dns}
group-ipset ${vps_set_names}
EOF
ln_run 0 "$(first_type chinadns-ng)" "chinadns-ng" "/dev/null" -C $config_file -v
@@ -1004,13 +1012,27 @@ get_direct_dns() {
DEFAULT_DNS="${DNSMASQ_UPSTREAM_DNS}"
[ -z "${DEFAULT_DNS}" ] && DEFAULT_DNS=$(echo -n $ISP_DNS | tr ' ' '\n' | head -2 | tr '\n' ',' | sed 's/,$//')
AUTO_DNS=${DEFAULT_DNS:-119.29.29.29}
RETURN_DNS=${AUTO_DNS}
local AUTO_DNS_1=$(echo ${AUTO_DNS} | awk -F ',' '{print $1}')
local AUTO_DNS_2=$(echo ${AUTO_DNS} | awk -F ',' '{print $2}')
local AUTO_DNS_ADDRESS=$(echo ${AUTO_DNS_1} | awk -F '#' '{print $1}')
local AUTO_DNS_PORT=$(echo ${AUTO_DNS_1} | awk -F '#' '{print $2}')
DIRECT_DNS_UDP_SERVER=${AUTO_DNS_ADDRESS}
DIRECT_DNS_UDP_PORT=${AUTO_DNS_PORT}
DIRECT_DNS_PROTO="udp"
DIRECT_DNS_SERVER=$(echo ${AUTO_DNS_1} | awk -F '#' '{print $1}')
DIRECT_DNS_PORT=$(echo ${AUTO_DNS_1} | awk -F '#' '{print $2}')
DIRECT_DNS_PORT=${DIRECT_DNS_PORT:-53}
local direct_dns_protocol=$(config_n_get @global[0] direct_dns_protocol)
if [ "${direct_dns_protocol}" = "tcp" ] || [ "${direct_dns_protocol}" = "udp" ]; then
local DIRECT_DNS=$(config_n_get @global[0] direct_dns)
local result=$(lua_api "parseDNS(\"${DIRECT_DNS}\")")
[ "${result}" != "nil" ] && {
DIRECT_DNS_PROTO="${direct_dns_protocol}"
DIRECT_DNS_SERVER=$(echo ${result} | awk '{print $1}')
DIRECT_DNS_PORT=$(echo ${result} | awk '{print $2}')
RETURN_DNS="${RETURN_DNS},${DIRECT_DNS_SERVER}#${DIRECT_DNS_PORT}#${DIRECT_DNS_PROTO}"
}
fi
}
get_config() {
@@ -809,15 +809,17 @@ add_firewall_rule() {
$ip6t_m -A PSW2_OUTPUT $(dst $IPSET_VPS6) -j RETURN
$ip6t_m -A PSW2_OUTPUT -m conntrack --ctdir REPLY -j RETURN
[ -n "$AUTO_DNS" ] && {
for auto_dns in $(echo $AUTO_DNS | tr ',' ' '); do
local dns_address=$(echo $auto_dns | awk -F '#' '{print $1}')
local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}')
[ -n "$RETURN_DNS" ] && {
for _dns in $(echo $RETURN_DNS | tr ',' ' '); do
local dns_address=$(echo $_dns | awk -F '#' '{print $1}')
local dns_port=$(echo $_dns | awk -F '#' '{print $2}')
local dns_proto=$(echo $_dns | awk -F '#' '{print $3}')
dns_proto=${dns_proto:-udp}
if [[ "$dns_address" == *::* ]]; then
$ip6t_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN
$ip6t_m -I PSW2_OUTPUT -p ${dns_proto} -d ${dns_address} --dport ${dns_port:-53} -j RETURN
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "ip6tables" "[${dns_address}]:${dns_port:-53}")"
else
$ipt_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN
$ipt_m -I PSW2_OUTPUT -p ${dns_proto} -d ${dns_address} --dport ${dns_port:-53} -j RETURN
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "iptables" "${dns_address}:${dns_port:-53}")"
fi
done
@@ -862,15 +862,17 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 ct direction reply counter return"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta mark 255 counter return"
[ -n "$AUTO_DNS" ] && {
for auto_dns in $(echo $AUTO_DNS | tr ',' ' '); do
local dns_address=$(echo $auto_dns | awk -F '#' '{print $1}')
local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}')
[ -n "$RETURN_DNS" ] && {
for _dns in $(echo $RETURN_DNS | tr ',' ' '); do
local dns_address=$(echo $_dns | awk -F '#' '{print $1}')
local dns_port=$(echo $_dns | awk -F '#' '{print $2}')
local dns_proto=$(echo $_dns | awk -F '#' '{print $3}')
dns_proto=${dns_proto:-udp}
if [[ "$dns_address" == *::* ]]; then
nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return"
nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto ${dns_proto} ip6 daddr ${dns_address} $(factor ${dns_port:-53} "${dns_proto} dport") counter return"
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "[${dns_address}]:${dns_port:-53}")"
else
nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp ip daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return"
nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol ${dns_proto} ip daddr ${dns_address} $(factor ${dns_port:-53} "${dns_proto} dport") counter return"
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "${dns_address}:${dns_port:-53}")"
fi
done
+1 -1
View File
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
PKG_RELEASE:=42
PKG_RELEASE:=43
endif
LUCI_TITLE:=Footstrap Theme
@@ -99,27 +99,95 @@ function run() {
*
* Wrapping `dom.content()` itself also works, at the price of patching a luci-base API every app
* shares and up to seven read/write pairs per call. */
/* The three things `dom.content()` is called on: a section body, a table, and a TABLE'S BODY. The
* third was missing and cost a release: on 24.10's Overview the section is a table, so nothing here
* matched, the floor held nothing, and a poll emptying it took 58px off the document under the
* reader on ImmortalWrt 24.10 with a webkit engine, where no CI job looks. tools/scroll-anchor.mjs
* looks for the same three and says why. */
/* A section body and a table: what `dom.content()` is called on and this can hold. Its third target,
* a table's BODY, is deliberately absent a floor there holds nothing. `min-height` is undefined on
* a table box (CSS 2.1 §10.7) and WebKit acts on that: a `.table.cbi-section-table` carrying a 313px
* floor still collapsed to 30px and the document lost 284px (webkit, /admin/network/firewall, 24.10
* and 25.12 alike; Chromium holds the 313px), and writing the floor on the `.tbody` instead loses
* the same 284px. 24.10 has no `.tbody` at all: its Overview renders `<table class="table">` with the
* rows directly inside, and the container a poll empties there is `.cbi-section > div`.
* tools/scroll-anchor.mjs looks for all three when it picks a box to collapse, which is a different
* question from which box can carry a floor. */
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.
/* What the container would stand at with no floor under it, asked of the CONTENT rather than by
* taking the floor off and re-measuring.
*
* Not while the reader scrolls: clearing to re-measure is a layout read, and a floor staying where
* it was is still a floor. */
* Clearing `min-height` to re-measure was the obvious way and is the expensive one: `min-height` is
* a scroll-anchoring suppression trigger on the path from the anchor to the scroller
* (css-scroll-anchoring-1 §3.2), so a clear, a forced layout and a write back tell the engine to
* drop its own compensation for that frame the theme switching off the very thing it relies on,
* once per tick.
*
* The span of the children answers the same question with reads alone. It is the box's content
* height plus what the box adds below it; a collapsed margin on the last child can put it a few
* pixels out, which a floor can afford the floor is a lower bound on a height that is about to be
* replaced, not a layout the page is drawn to.
*
* An empty container answers 0, and the caller keeps the standing floor for exactly that reason:
* `dom.content()` empties before it refills, and 0 is the number the floor exists to refuse. */
function naturalHeight(el) {
const last = el.lastElementChild;
if (!last) return 0;
const box = el.getBoundingClientRect();
const end = last.getBoundingClientRect();
if (!end.height && !end.width) return 0; /* a last child out of the flow says nothing */
const cs = window.getComputedStyle(el);
const below = parseFloat(cs.paddingBottom) + parseFloat(cs.borderBottomWidth);
return Math.round(end.bottom - box.top + (below || 0));
}
/* The floor is the height the next tick may not go below, one per container. Read in one pass and
* written in another, so the sweep costs a single forced layout rather than one per element.
*
* WRITTEN ONLY WHERE THE VALUE CHANGES, which is the difference between a floor and a page the
* engine refuses to anchor: see naturalHeight() above. Measured over 25 s of real polling on the
* Overview at 390px, the clear-and-rewrite shape wrote style 1550 times on 25.12 and 170 times on
* ImmortalWrt 24.10, of which 75 and 62 carried a value that had actually moved the rest were
* suppression bought for nothing.
*
* Not while the reader scrolls: a rect read is a forced layout, and a floor staying where it was is
* still a floor. */
/* The floor is the height the next tick may not go below, one per box. Read in one pass and written
* in another, so the sweep costs a single forced layout rather than one per element.
*
* WRITTEN ONLY WHERE THE VALUE CHANGES, which is the difference between a floor and a page the
* engine refuses to anchor: see naturalHeight() above. Measured over 25 s of real polling on the
* Overview at 390px, the clear-and-rewrite shape wrote style 1550 times on 25.12 and 170 times on
* ImmortalWrt 24.10, of which 75 and 62 carried a value that had actually moved the rest were
* suppression bought for nothing. It is 45 writes, all of them real, on both.
*
* AND NOT ON A TABLE BOX, which cannot hold it: `min-height` is undefined there (CSS 2.1 §10.7) and
* WebKit acts on that a `.table.cbi-section-table` wearing a 313px floor still collapsed to 30px
* when its rows went, and the document lost 284px on /admin/network/firewall, the same on 24.10 and
* 25.12, while Chromium held the 313px. The `.tbody` inside it is a table box too and loses the same
* 284px. So the floor climbs to the first box that is not one the section where the same
* emptied `.tbody` costs the document 0px on both engines. `getComputedStyle` resolves style, not
* layout, so the climb adds no forced layout of its own.
*
* Not while the reader scrolls: a rect read is a forced layout, and a floor staying where it was is
* still a floor. */
function holdFloor() {
if (scrolling()) return;
const host = document.getElementById('view');
if (!host) return; /* the login page has no view */
const els = host.querySelectorAll(SHRINKS), hs = [];
els.forEach((el) => { el.style.minHeight = ''; });
els.forEach((el) => hs.push(el.offsetHeight));
els.forEach((el, i) => { if (hs[i] > 0) el.style.minHeight = hs[i] + 'px'; });
const boxes = [], hs = [];
host.querySelectorAll(SHRINKS).forEach((el) => {
let box = el;
while (box && box !== host && window.getComputedStyle(box).display.startsWith('table'))
box = box.parentElement;
/* several tables in one section climb to the same box; it needs one floor, not one each */
if (!box || box === host || boxes.indexOf(box) !== -1) return;
boxes.push(box);
hs.push(naturalHeight(box));
});
boxes.forEach((box, i) => {
/* zero is an empty box, and the floor it already wears is what holds it up the moment this
* whole mechanism exists for */
if (hs[i] <= 0) return;
const px = hs[i] + 'px';
if (box.style.minHeight !== px) box.style.minHeight = px;
});
}
/* ---- is the page moving right now? asked of the position, never of the events ----
@@ -202,6 +270,11 @@ function sampleMotion() {
/* the page has held still for SCROLL_IDLE: whatever was put off may run now */
if (_deferred) {
_deferred = false;
/* where the reference stands BEFORE the put-off pass re-lays the page — see settleDrift() */
const settled = _rest;
const before = (settled && settled.el && settled.el.isConnected)
? settled.el.getBoundingClientRect().top
: ((settled && settled.sec && settled.sec.isConnected) ? settled.sec.getBoundingClientRect().top : null);
/* No correction for this batch. Both available references are wrong for a page the reader
* has just scrolled through: a fresh one is read against an offset WebKit may not have laid
* out yet (the theme then undoes the reader's own move), and the one from the last still
@@ -210,9 +283,59 @@ function sampleMotion() {
* the fitters re-measure what the scroll already showed rather than growing the page and
* the next mutation corrects against a reference taken while the page was still. */
run();
if (ENGINE_ANCHORS) settleDrift(settled, before);
}
}
/* ---- the put-off pass moves the page too, and nothing was looking ----
*
* A tick landing while the offset is in motion leaves its measurements to the block above, and that
* pass then re-lays the tables it could not measure. An anchoring engine answers that layout change
* the way it answers any other and `min-height`, which the floor writes on every container, is
* itself a suppression trigger on the path to the anchor (css-scroll-anchoring-1 §3.2), so the
* engine's compensation can be switched off by the very pass that needs it. Measured on
* ImmortalWrt 24.10/WebKit: this pass's 88 `min-height` writes and a 58px jump of the offset land in
* the SAME frame, 429 ms after the mutation (@390, top layout, large density, Overview).
*
* `lateDrift()` cannot see it: it is scheduled from the mutation on the same SCROLL_IDLE, so it
* measures ALONGSIDE this pass rather than after it it read a drift of zero three milliseconds
* before the page moved, and scroll-anchor reported those 58px on that cell alone out of 48.
*
* MEASURED SYNCHRONOUSLY AROUND THE PASS, not a frame or an idle window later. Two reasons, and the
* second cost a run: the reader cannot scroll between two statements, so what this sees is the
* pass's doing and nothing else a version that looked two frames later corrected inside a flick on
* three cells of the same sweep. And `getBoundingClientRect()` is exactly the operation the spec
* makes the engine flush a pending adjustment before, so the read after the pass sees the engine's
* answer rather than racing it (§2.2: the suppression window ends at the end of the event loop
* iteration, or before the next operation whose result would differ, whichever is sooner). */
function settleDrift(ref, before) {
if (before == null || !ref) return;
if (!anchorEnabled() || Date.now() < _userUntil) return;
if (_restPage !== pageStamp()) return;
const el = (ref.el && ref.el.isConnected) ? ref.el : ((ref.sec && ref.sec.isConnected) ? ref.sec : null);
if (el) putBack(el, before);
}
/* Give the reader back what moved under them: the one write both corrections make, and the rules
* that write obeys.
*
* A drift under a pixel is rounding, and an engine that answered for it reads the same. One
* viewport is the ceiling, a drift that size being a view that replaced its whole subtree rather
* than a tick anchorFor() raises it for the one drift that big with a receipt, a measured clamp.
*
* The write moves the page by exactly the drift measured, so the reference is back at the top it
* was remembered at and the next tick measures zero. Only `_restAt` moves, and the write may have
* been clamped short, so it is re-read rather than assumed; `rememberRest()` cannot do it, since
* the write starts the motion sampler and that function returns early while the page moves. */
function putBack(el, was) {
const drift = el.getBoundingClientRect().top - was;
if (Math.abs(drift) < 1 || Math.abs(drift) > (window.innerHeight || 800)) return;
const sc = scroller();
const at = sc ? sc.scrollTop : window.scrollY;
if (sc) sc.scrollTop = at + drift; else window.scrollTo(0, at + drift);
_restAt = scrollTop();
}
function noteMotion() {
_movingUntil = Date.now() + SCROLL_IDLE;
if (_sampling) return;
@@ -563,23 +686,8 @@ function lateDrift(ref) {
if (scrollTop() !== seen) return;
/* 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 */
let el = ref.el, was = ref.top;
if (!el || !el.isConnected) {
if (!ref.sec || !ref.sec.isConnected || ref.secTop == null) return;
el = ref.sec; was = ref.secTop;
}
const drift = el.getBoundingClientRect().top - was;
if (Math.abs(drift) < 1) return; /* the engine put it back */
if (Math.abs(drift) > (window.innerHeight || 800)) return;
const sc = scroller();
const at = sc ? sc.scrollTop : window.scrollY;
if (sc) sc.scrollTop = at + drift; else window.scrollTo(0, at + drift);
/* The write moves the page by exactly the drift measured, which puts the reference back
* at the top it was remembered at, so `_rest.top` still holds and the next tick
* measures zero. Only `_restAt` changes, and the write may have been clamped short, so
* it is re-read rather than assumed; `rememberRest()` cannot do it, since the write
* starts the motion sampler and that function returns early while the page moves. */
_restAt = scrollTop();
if (ref.el && ref.el.isConnected) putBack(ref.el, ref.top);
else if (ref.sec && ref.sec.isConnected && ref.secTop != null) putBack(ref.sec, ref.secTop);
}, SCROLL_IDLE);
});
}