mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 17:52:51 +08:00
✨ Sync 2026-04-17 08:55:06
This commit is contained in:
1 parent
74d1b9fd57
commit
b5fc8fe2c8
5 files changed
+818
-31
No files matched your search
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.4.15
|
||||
PKG_RELEASE:=75
|
||||
PKG_RELEASE:=76
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -9,7 +9,7 @@ if not arg[1] or not m:get(arg[1]) then
|
||||
luci.http.redirect(m.redirect)
|
||||
end
|
||||
|
||||
m:append(Template(appname .. "/cbi/nodes_multivalue_com"))
|
||||
m:append(Template(appname .. "/cbi/nodes_dynamiclist_com"))
|
||||
m:append(Template(appname .. "/cbi/nodes_listvalue_com"))
|
||||
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
@@ -96,44 +96,28 @@ o.datatype = "min(1)"
|
||||
o.default = 1
|
||||
o:depends("enable_autoswitch", true)
|
||||
|
||||
o = s:option(MultiValue, "autoswitch_backup_node", translate("List of backup nodes"))
|
||||
o = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes"))
|
||||
o:depends("enable_autoswitch", true)
|
||||
o.widget = "checkbox"
|
||||
o.template = appname .. "/cbi/nodes_multivalue"
|
||||
o.template = appname .. "/cbi/nodes_dynamiclist"
|
||||
o.group = {}
|
||||
o.write = function(self, section, value)
|
||||
local n = s.fields["node"]:formvalue(section)
|
||||
local v = type(value) == "table" and value or { value }
|
||||
local t = {}
|
||||
for _, x in ipairs(v) do
|
||||
if x and x ~= n then t[#t+1] = x end
|
||||
end
|
||||
return DynamicList.write(self, section, t)
|
||||
end
|
||||
for i, v in pairs(nodes_table) do
|
||||
if v.protocol ~= "_shunt" then
|
||||
o:value(v.id, v.remark)
|
||||
o.group[#o.group+1] = v.group or ""
|
||||
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
s.fields["enable_autoswitch"]:depends({ node = v.id })
|
||||
end
|
||||
socks_node:value(v.id, v["remark"])
|
||||
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
-- 读取旧 DynamicList
|
||||
function o.cfgvalue(self, section)
|
||||
return m.uci:get_list(appname, section, "autoswitch_backup_node") or {}
|
||||
end
|
||||
-- 写入保持 DynamicList
|
||||
function o.write(self, section, value)
|
||||
local old = m.uci:get_list(appname, section, "autoswitch_backup_node") or {}
|
||||
local new, set = {}, {}
|
||||
for v in value:gmatch("%S+") do
|
||||
new[#new + 1] = v
|
||||
set[v] = 1
|
||||
end
|
||||
for _, v in ipairs(old) do
|
||||
if not set[v] then
|
||||
m.uci:set_list(appname, section, "autoswitch_backup_node", new)
|
||||
return
|
||||
end
|
||||
set[v] = nil
|
||||
end
|
||||
for _ in pairs(set) do
|
||||
m.uci:set_list(appname, section, "autoswitch_backup_node", new)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node."))
|
||||
o:depends("enable_autoswitch", true)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<%+cbi/valueheader%>
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- - snowie2000
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
|
||||
local json = require "luci.jsonc"
|
||||
|
||||
-- 读取值(keylist/vallist/group)
|
||||
local values = {}
|
||||
for i, key in pairs(self.keylist) do
|
||||
values[#values + 1] = {
|
||||
key = key,
|
||||
label = self.vallist[i] or key,
|
||||
group = self.group and self.group[i] or nil
|
||||
}
|
||||
end
|
||||
|
||||
-- 获取当前配置值
|
||||
local current_key = {}
|
||||
local cval = self:cfgvalue(section)
|
||||
if type(cval) == "table" then
|
||||
for _, v in ipairs(cval) do
|
||||
for item in string.gmatch(v, "[^,]+") do
|
||||
current_key[#current_key+1] = item
|
||||
end
|
||||
end
|
||||
elseif type(cval) == "string" then
|
||||
for item in string.gmatch(cval, "[^,]+") do
|
||||
current_key[#current_key+1] = item
|
||||
end
|
||||
end
|
||||
|
||||
-- 分离无组节点(ungrouped)与有组节点(grouped),并保持原有顺序
|
||||
local ungrouped = {}
|
||||
local groups = {}
|
||||
local group_order = {}
|
||||
for _, item in ipairs(values) do
|
||||
if not item.group or item.group == "" then
|
||||
table.insert(ungrouped, item)
|
||||
else
|
||||
local g = item.group
|
||||
if not groups[g] then
|
||||
groups[g] = {}
|
||||
table.insert(group_order, g)
|
||||
end
|
||||
table.insert(groups[g], item)
|
||||
end
|
||||
end
|
||||
|
||||
-- 打包数据给 JS
|
||||
local dropdown_data = {
|
||||
current_key = current_key,
|
||||
ungrouped = ungrouped,
|
||||
groups = groups,
|
||||
group_order = group_order,
|
||||
cbid = cbid,
|
||||
}
|
||||
%>
|
||||
<script>
|
||||
window.dl_dropdown_data = window.dl_dropdown_data || {};
|
||||
window.dl_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
||||
</script>
|
||||
|
||||
<div id="<%=cbid%>.main" class="dl-dropdown-container">
|
||||
<select id="<%=cbid%>.ref" class="cbi-input-select" style="display:none !important;">
|
||||
<option value>placeholder</option>
|
||||
</select>
|
||||
|
||||
<!-- 已选节点显示区 -->
|
||||
<div id="<%=cbid%>.selected" class="dl-selected-list"></div>
|
||||
|
||||
<!-- 模拟 listvalue 控件外观 -->
|
||||
<div class="cbi-input-value cbi-input-select dl-dropdown-display" id="<%=cbid%>.display" tabindex="0">
|
||||
<span id="<%=cbid%>.label" class="dl-dropdown-label">
|
||||
<%=translate("-- Please choose --")%>
|
||||
</span>
|
||||
<span class="dl-arrow-down"></span>
|
||||
</div>
|
||||
<!-- 下拉面板 -->
|
||||
<div id="<%=cbid%>.panel" class="cbi-dynamiclist-panel dl-dropdown-panel" style="display:none;">
|
||||
<!-- 搜索框 -->
|
||||
<div style="padding:8px;border-bottom:1px solid #f0f0f0;">
|
||||
<input id="<%=cbid%>.search" class="cbi-input-text dl-dropdown-search" type="text" placeholder="🔍 <%:Search nodes...%>" inputmode="search" enterkeyhint="done" />
|
||||
</div>
|
||||
<!-- 列表容器 -->
|
||||
<div id="<%=cbid%>.list" style="padding:8px;">
|
||||
<!-- 首次点击 display 时由 JS 填充 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function(){
|
||||
const cbid = "<%=cbid%>";
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const labelSpan = document.getElementById(cbid + ".label");
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const listContainer = document.getElementById(cbid + ".list");
|
||||
|
||||
function init(){
|
||||
const hidden = document.getElementById(cbid + ".hidden");
|
||||
const data = window.dl_dropdown_data[cbid];
|
||||
let arr = data.current_key;
|
||||
if (!arr || arr.length === 0) return;
|
||||
arr.forEach(key=>{
|
||||
let label = "";
|
||||
data.ungrouped.concat(...Object.values(data.groups)).forEach(v=>{
|
||||
if (v.key == key) label = v.label;
|
||||
});
|
||||
if (label)
|
||||
dl_add_item(cbid, key, label);
|
||||
});
|
||||
}
|
||||
|
||||
// 点击 display
|
||||
display.addEventListener("click", function(e){
|
||||
e.stopPropagation();
|
||||
dl_render_dropdown_list(cbid,panel,listContainer,labelSpan,searchInput,display);
|
||||
document.querySelectorAll(".cbi-dynamiclist-panel").forEach(p=>{
|
||||
if (p !== panel) p.style.display = "none";
|
||||
});
|
||||
if (panel.style.display !== "none") {
|
||||
dl_closePanel(cbid,panel,listContainer,searchInput);
|
||||
} else {
|
||||
dl_openPanel(cbid,display,panel,listContainer,searchInput);
|
||||
}
|
||||
});
|
||||
|
||||
dl_registerAdaptive(cbid);
|
||||
|
||||
init();
|
||||
|
||||
new Sortable(document.getElementById(cbid + ".selected"), {
|
||||
animation: 150,
|
||||
ghostClass: "dragging-row",
|
||||
onEnd: function(){
|
||||
dl_update_hidden(cbid);
|
||||
}
|
||||
});
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -0,0 +1,654 @@
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- - snowie2000
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
-- It is the common part of the template and cannot be used independently
|
||||
%>
|
||||
<script src="<%=resource%>/view/passwall/Sortable.min.js"></script>
|
||||
<style>
|
||||
/* 主下拉按钮的下箭头 */
|
||||
.dl-arrow-down {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 6px solid #666;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的右箭头(折叠) */
|
||||
.dl-arrow-right {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的下箭头(展开) */
|
||||
.dl-arrow-down-small {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 基础列表项样式 */
|
||||
.cbi-dynamiclist-panel li[data-key] {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
/* 鼠标悬停效果 - 使用透明度避免覆盖问题 */
|
||||
.cbi-dynamiclist-panel li[data-key]:hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.dl-dropdown-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
min-width: 220px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 1152px) {
|
||||
.dl-dropdown-container {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.dl-dropdown-container {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.dl-dropdown-display {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dl-dropdown-label,
|
||||
.dl-selected-label {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.dl-dropdown-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2147483647;
|
||||
border: 1px solid #dcdcdc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.dl-dropdown-search {
|
||||
width: 100%;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dl-group-title {
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dl-group-list {
|
||||
list-style: none;
|
||||
margin: 6px 0 0 8px;
|
||||
padding: 0;
|
||||
}
|
||||
.dl-group-item {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left !important;
|
||||
}
|
||||
.dl-selected-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
.dl-remove {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
color: red;
|
||||
}
|
||||
.dragging-row {
|
||||
background-color: rgba(131, 191, 255, 0.7) !important;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
// css helper functions
|
||||
function dl_camelToKebab(str) {
|
||||
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()
|
||||
}
|
||||
|
||||
function dl_style2Css(styleDeclaration, properties) {
|
||||
const cssRules = properties.map(prop => {
|
||||
const kebabCaseProp = dl_camelToKebab(prop);[1, 5]
|
||||
const value = styleDeclaration[prop]
|
||||
if (value) {
|
||||
return `${kebabCaseProp}: ${value};`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
// Filter out any empty strings and join the rules
|
||||
return cssRules.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
const dl_parseColorToRgba = (function() {
|
||||
// Create canvas and context once (Closure)
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
return function(colorStr) {
|
||||
if (!colorStr)
|
||||
return null;
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
// 2. Apply the color
|
||||
ctx.fillStyle = colorStr;
|
||||
// 3. Fill a single pixel
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
// 4. Extract pixel data [R, G, B, A]
|
||||
const data = ctx.getImageData(0, 0, 1, 1).data;
|
||||
return {
|
||||
r: data[0],
|
||||
g: data[1],
|
||||
b: data[2],
|
||||
// Convert alpha from 0-255 to 0-1 (rounded to 3 decimal places)
|
||||
a: Math.round((data[3] / 255) * 1000) / 1000
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
// Helper to convert back to Hex (for output consistency)
|
||||
function dl_rgbToHex(r, g, b) {
|
||||
const toHex = (n) => {
|
||||
const hex = Math.max(0, Math.min(255, n)).toString(16)
|
||||
return hex.length === 1 ? '0' + hex : hex
|
||||
}
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
function dl_isTransparent(color) {
|
||||
const cleanColor = dl_parseColorToRgba(color);
|
||||
// check #RRGGBBAA for transparency
|
||||
return !cleanColor || (cleanColor.a !== undefined && !cleanColor.a);
|
||||
}
|
||||
|
||||
function dl_getColorSchema(color) {
|
||||
const rgb = dl_parseColorToRgba(color);
|
||||
if (!rgb) return 'unknown'; // Handle invalid colors
|
||||
// Calculate YIQ brightness (human eye perception)
|
||||
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
|
||||
|
||||
return brightness > 128 ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
function dl_lighter(color, amount) {
|
||||
const rgb = dl_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Add amount to each channel
|
||||
const r = rgb.r + amount;
|
||||
const g = rgb.g + amount;
|
||||
const b = rgb.b + amount;
|
||||
// Convert back to Hex (clamping happens inside rgbToHex)
|
||||
return dl_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
function dl_darker(color, amount) {
|
||||
const rgb = dl_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Subtract amount from each channel
|
||||
const r = rgb.r - amount;
|
||||
const g = rgb.g - amount;
|
||||
const b = rgb.b - amount;
|
||||
|
||||
return dl_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
// copy select styles
|
||||
function dl_adaptiveStyle(cbid) {
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const hiddenRef = document.getElementById(cbid + ".ref");
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
if (hiddenRef && display) {
|
||||
const elOption = hiddenRef.getElementsByTagName("option")[0]
|
||||
const styleSelect = window.getComputedStyle(hiddenRef)
|
||||
const styleOption = window.getComputedStyle(elOption)
|
||||
const styleBody = window.getComputedStyle(document.body)
|
||||
|
||||
const styleNode = document.createElement('style')
|
||||
const styleNames = ["width", "color", "height", "padding", "margin", "lineHeight", "border", "borderRadius", "minWidth", "minHeight"]
|
||||
if (styleSelect.borderBottomStyle !== "none") {
|
||||
styleNames.push("borderBottomWidth", "borderBottomStyle", "borderBottomColor");
|
||||
}
|
||||
document.head.appendChild(styleNode)
|
||||
// trace back from option -> select -> body for background color
|
||||
const panelRadius = styleSelect.borderRadius;
|
||||
const optionColor = !dl_isTransparent(styleOption.backgroundColor) ? styleOption.backgroundColor : !dl_isTransparent(styleSelect.backgroundColor) ? styleSelect.backgroundColor : styleBody.backgroundColor
|
||||
const titleColor = dl_getColorSchema(optionColor) === "light" ? dl_darker(optionColor, 30) : dl_lighter(optionColor, 30)
|
||||
const selectStyleCSS = [`#${CSS.escape(cbid + ".display")}, .dl-selected-item {`, dl_style2Css(styleSelect, styleNames), dl_style2Css(styleSelect, ["backgroundColor"]), "}"]
|
||||
const optionStyleCSS = [`#${CSS.escape(cbid + ".panel")} {`, dl_style2Css(styleOption, styleNames), `background-color: ${optionColor};`, `border-radius: ${panelRadius};`, "}"]
|
||||
const titleStyleCSS = [`#${CSS.escape(cbid + ".panel")} .dl-group-title {`, `background-color: ${titleColor} !important;`, "}"]
|
||||
styleNode.textContent = [].concat(selectStyleCSS, optionStyleCSS, titleStyleCSS).join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
function dl_idSafe(id) {
|
||||
return id
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[\x00-\x1F\x7F]/g, "");
|
||||
}
|
||||
|
||||
// 更新组内选中计数
|
||||
function dl_updateGroupCounts(cbid, listContainer, searchInput) {
|
||||
const groups = listContainer.querySelectorAll(".dl-group");
|
||||
const isSearching = searchInput.value.trim() !== "";
|
||||
groups.forEach(group => {
|
||||
const gname = group.getAttribute("data-group");
|
||||
const items = group.querySelectorAll("li[data-key]");
|
||||
const span = document.getElementById("group-count-" + cbid + "-" + gname);
|
||||
if (!span) return;
|
||||
if (isSearching) {
|
||||
// 搜索状态:显示匹配数量
|
||||
let matchCount = 0;
|
||||
items.forEach(li => {
|
||||
if (li.style.display !== "none") matchCount++;
|
||||
});
|
||||
span.textContent = "(" + matchCount + "/" + items.length + ")";
|
||||
if (matchCount > 0) {
|
||||
span.style.color = "#28a745";
|
||||
span.style.fontWeight = "600";
|
||||
} else {
|
||||
span.style.color = "#dc3545";
|
||||
span.style.fontWeight = "normal";
|
||||
}
|
||||
} else {
|
||||
// 默认状态
|
||||
span.textContent = "(" + items.length + ")";
|
||||
span.style.color = "";
|
||||
span.style.fontWeight = "normal";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//搜索过滤器:按 name 或 label 做模糊匹配,搜索时自动展开所有组并隐藏不匹配条目
|
||||
function dl_filterList(keyword, cbid, listContainer, searchInput) {
|
||||
keyword = (keyword || "").toLowerCase().trim();
|
||||
const topItems = listContainer.querySelectorAll("ul li[data-key]");
|
||||
topItems.forEach(li=>{
|
||||
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "block";
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
const groups = listContainer.querySelectorAll(".dl-group");
|
||||
groups.forEach(group=>{
|
||||
const items = group.querySelectorAll("li[data-key]");
|
||||
let matchCount = 0;
|
||||
items.forEach(li=>{
|
||||
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "block";
|
||||
matchCount++;
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
group.style.display = (matchCount === 0 && keyword !== "") ? "none" : "block";
|
||||
const ul = group.querySelector(".dl-group-list");
|
||||
const gname = group.getAttribute("data-group");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + gname);
|
||||
if (keyword) {
|
||||
if (ul) ul.style.display = (matchCount > 0 ? "block" : "none");
|
||||
if (arrow) arrow.className = (matchCount > 0 ? "dl-arrow-down-small" : "dl-arrow-right");
|
||||
} else {
|
||||
if (ul) ul.style.display = "none";
|
||||
if (arrow) arrow.className = "dl-arrow-right";
|
||||
}
|
||||
});
|
||||
dl_updateGroupCounts(cbid, listContainer, searchInput);
|
||||
}
|
||||
|
||||
// 切换单个组(点击组标题)
|
||||
function dl_toggleGroup(listContainer, cbid, g) {
|
||||
g = dl_idSafe(g);
|
||||
const group = listContainer.querySelector(".dl-group[data-group='" + g + "']");
|
||||
if (!group) return;
|
||||
const ul = group.querySelector(".dl-group-list");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + g);
|
||||
if (!ul) return;
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const isSearching = searchInput?.value.trim() !== "";
|
||||
const isExpanded = ul.style.display !== "none";
|
||||
|
||||
if (isExpanded) {
|
||||
ul.style.display = "none";
|
||||
if (arrow) arrow.className = "dl-arrow-right";
|
||||
} else {
|
||||
ul.style.display = "block";
|
||||
if (arrow) arrow.className = "dl-arrow-down-small";
|
||||
|
||||
if (!isSearching) {
|
||||
const allGroups = listContainer.querySelectorAll(".dl-group");
|
||||
allGroups.forEach(otherGroup => {
|
||||
if (otherGroup !== group) {
|
||||
const otherUl = otherGroup.querySelector(".dl-group-list");
|
||||
const otherGname = otherGroup.getAttribute("data-group");
|
||||
const otherArrow = document.getElementById("arrow-" + cbid + "-" + otherGname);
|
||||
if (otherUl) otherUl.style.display = "none";
|
||||
if (otherArrow) otherArrow.className = "dl-arrow-right";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算panel位置
|
||||
function dl_repositionPanel(panel, display) {
|
||||
if (!panel || panel.style.display === "none") return;
|
||||
const rect = display.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
panel.style.visibility = "hidden";
|
||||
panel.style.display = "block";
|
||||
panel.style.minHeight = "100px";
|
||||
panel.style.maxHeight = Math.min(0.5*viewportHeight, 550) + "px";
|
||||
const panelHeight = panel.offsetHeight;
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
let top, isUp = false;
|
||||
if (spaceBelow >= panelHeight) {
|
||||
top = rect.bottom + 2;
|
||||
isUp = false;
|
||||
} else if (spaceAbove >= panelHeight) {
|
||||
top = rect.top - panelHeight - 2;
|
||||
isUp = true;
|
||||
} else {
|
||||
if (spaceBelow >= spaceAbove) {
|
||||
top = Math.max(rect.bottom - 2, viewportHeight - panelHeight - 2);
|
||||
isUp = false;
|
||||
} else {
|
||||
top = Math.min(rect.top - panelHeight + 2, 2);
|
||||
isUp = true;
|
||||
}
|
||||
}
|
||||
panel.style.left = rect.left + "px";
|
||||
panel.style.top = top + "px";
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const displayWidth = rect.width;
|
||||
const remainingWidth = window.innerWidth - panelRect.left - 12;
|
||||
const maxWidth = Math.max(displayWidth, Math.floor(remainingWidth));
|
||||
panel.style.maxWidth = maxWidth + "px";
|
||||
panel.style.minWidth = displayWidth + "px";
|
||||
panel.style.width = "auto";
|
||||
panel.style.visibility = "";
|
||||
}
|
||||
|
||||
// 打开/关闭面板
|
||||
function dl_openPanel(cbid, display, panel, listContainer, searchInput) {
|
||||
if (!panel._moved) {
|
||||
document.body.appendChild(panel);
|
||||
panel._moved = true;
|
||||
}
|
||||
panel.style.display = "block";
|
||||
dl_repositionPanel(panel, display);
|
||||
// 失焦监听
|
||||
const handler = function(e){
|
||||
const target = e.target;
|
||||
if (panel.style.display !== "none") {
|
||||
if (!panel.contains(target) && !display.contains(target)) {
|
||||
dl_closePanel(cbid, panel, listContainer, searchInput, display);
|
||||
}
|
||||
}
|
||||
}
|
||||
panel._docClickHandler = handler;
|
||||
document.addEventListener("click", handler);
|
||||
// 滚动 / resize 自动 reposition
|
||||
let ticking = false;
|
||||
const repositionHandler = function () {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(function () {
|
||||
ticking = false;
|
||||
dl_repositionPanel(panel, display);
|
||||
});
|
||||
};
|
||||
panel._repositionHandler = repositionHandler;
|
||||
window.addEventListener("scroll", repositionHandler, true);
|
||||
window.addEventListener("resize", repositionHandler);
|
||||
}
|
||||
function dl_closePanel(cbid, panel, listContainer, searchInput) {
|
||||
panel.style.display = "none";
|
||||
searchInput.value = "";
|
||||
dl_filterList("", cbid, listContainer, searchInput);
|
||||
// document click
|
||||
if (panel._docClickHandler) {
|
||||
document.removeEventListener("click", panel._docClickHandler);
|
||||
panel._docClickHandler = null;
|
||||
}
|
||||
// scroll / resize
|
||||
if (panel._repositionHandler) {
|
||||
window.removeEventListener("scroll", panel._repositionHandler, true);
|
||||
window.removeEventListener("resize", panel._repositionHandler);
|
||||
panel._repositionHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
function dl_add_item(cbid, key, label) {
|
||||
const container = document.getElementById(cbid + ".selected");
|
||||
const hidden = document.getElementById(cbid + ".hidden");
|
||||
if (container.querySelector(`[data-key="${key}"]`)) return;
|
||||
const item = document.createElement("div");
|
||||
item.className = "cbi-input-value cbi-input-select dl-dropdown-display dl-selected-item";
|
||||
item.setAttribute("data-key", key);
|
||||
item.innerHTML = `
|
||||
<span class="dl-selected-label" style="cursor:grab;" title="${label}">${label}</span>
|
||||
<span class="dl-remove">×</span>
|
||||
`;
|
||||
item.querySelector(".dl-remove").onclick = function(){
|
||||
item.remove();
|
||||
dl_update_hidden(cbid);
|
||||
};
|
||||
container.appendChild(item);
|
||||
dl_update_hidden(cbid);
|
||||
}
|
||||
|
||||
function dl_update_hidden(cbid) {
|
||||
const main = document.getElementById(cbid + ".main");
|
||||
const container = document.getElementById(cbid + ".selected");
|
||||
main.querySelectorAll(`input[type="hidden"][name="${cbid}"]`).forEach(el => el.remove());
|
||||
let items = container.querySelectorAll(".dl-selected-item");
|
||||
items.forEach(el => {
|
||||
let key = el.getAttribute("data-key");
|
||||
let input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = cbid;
|
||||
input.value = key;
|
||||
main.insertBefore(input, container);
|
||||
});
|
||||
}
|
||||
|
||||
// 动态生成下拉框
|
||||
window.dl_dropdown_rendered = {};
|
||||
function dl_escape_html(s) {
|
||||
return s.replace(/[&<>"']/g, c => ({
|
||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||
}[c]));
|
||||
}
|
||||
|
||||
function dl_render_dropdown_list(cbid, panel, listContainer, labelSpan, searchInput, display) {
|
||||
if (window.dl_dropdown_rendered[cbid]) return;
|
||||
const data = window.dl_dropdown_data[cbid];
|
||||
if (!data) return;
|
||||
if (!listContainer) return;
|
||||
|
||||
let html = "";
|
||||
|
||||
// 无组项
|
||||
if (data.ungrouped && data.ungrouped.length > 0) {
|
||||
html += `<ul style="list-style:none;padding:0;margin:0 0 8px 0;">`;
|
||||
|
||||
data.ungrouped.forEach(item => {
|
||||
html += `
|
||||
<li data-key="${item.key}"
|
||||
data-node-name="${dl_escape_html(item.label.toLowerCase())}"
|
||||
class="list-item">
|
||||
<span class="dl-item-label" style="margin-left:12px;">
|
||||
${dl_escape_html(item.label)}
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `</ul>`;
|
||||
}
|
||||
|
||||
// 分组项
|
||||
data.group_order.forEach(gname => {
|
||||
const items = data.groups[gname];
|
||||
|
||||
html += `
|
||||
<div class="dl-group" data-group="${dl_idSafe(gname)}" style="margin-bottom:8px;">
|
||||
<div class="dl-group-title" data-group-name="${dl_idSafe(gname)}">
|
||||
<span class="dl-arrow-right" id="arrow-${cbid}-${dl_idSafe(gname)}"></span>
|
||||
<b style="margin-left:6px;">${dl_escape_html(gname)}</b>
|
||||
<span id="group-count-${cbid}-${dl_idSafe(gname)}"
|
||||
style="margin-left:8px;">(${items.length})</span>
|
||||
</div>
|
||||
<ul id="group-${cbid}-${dl_idSafe(gname)}" class="dl-group-list" style="display:none">
|
||||
`;
|
||||
|
||||
items.forEach(item => {
|
||||
html += `
|
||||
<li data-key="${item.key}"
|
||||
data-node-name="${dl_escape_html(item.label.toLowerCase())}"
|
||||
class="dl-group-item">
|
||||
<span class="dl-item-label" title="${dl_escape_html(item.label)}">
|
||||
${dl_escape_html(item.label)}
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
|
||||
window.dl_dropdown_rendered[cbid] = true;
|
||||
|
||||
dl_adaptiveStyle(cbid);
|
||||
|
||||
// 点击项(无组与组内项都使用 li[data-key])
|
||||
listContainer.addEventListener("click", function(e){
|
||||
let li = e.target;
|
||||
while(li && li !== listContainer && !li.hasAttribute('data-key')) li = li.parentNode;
|
||||
if(!li || li === listContainer) return;
|
||||
const key = li.getAttribute('data-key') || "";
|
||||
const text = li.querySelector(".dl-item-label")?.textContent || li.textContent || key;
|
||||
//改值
|
||||
dl_add_item(cbid, key, text);
|
||||
labelSpan.innerText = "<%=translate("-- Please choose --")%>";
|
||||
dl_closePanel(cbid,panel,listContainer,searchInput);
|
||||
});
|
||||
|
||||
// 搜索功能
|
||||
searchInput.addEventListener("input", function() {
|
||||
dl_filterList(this.value, cbid, listContainer, searchInput);
|
||||
dl_repositionPanel(panel, display);
|
||||
});
|
||||
searchInput.addEventListener('keydown', function(e) {
|
||||
const isEnter = e.key === "Enter" || e.keyCode === 13;
|
||||
if (!isEnter) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
searchInput.blur();
|
||||
});
|
||||
|
||||
// 切换组
|
||||
listContainer.querySelectorAll(".dl-group-title").forEach(title => {
|
||||
title.addEventListener("click", function() {
|
||||
const g = this.closest(".dl-group")?.getAttribute("data-group");
|
||||
if (g) {
|
||||
dl_toggleGroup(listContainer, cbid, g);
|
||||
dl_repositionPanel(panel, display);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 防止 panel 惯性滚动穿透
|
||||
panel.addEventListener('wheel', function (e) {
|
||||
const deltaY = e.deltaY;
|
||||
const scrollTop = panel.scrollTop;
|
||||
const scrollHeight = panel.scrollHeight;
|
||||
const clientHeight = panel.clientHeight;
|
||||
const isAtTop = scrollTop === 0;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
if (deltaY < 0 && isAtTop) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (deltaY > 0 && isAtBottom) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
const dl_adaptiveControls = new Set();
|
||||
function dl_registerAdaptive(cbid) {
|
||||
dl_adaptiveControls.add(cbid);
|
||||
dl_adaptiveStyle(cbid);
|
||||
}
|
||||
let dl_adaptiveTicking = false;
|
||||
window.addEventListener("resize", () => {
|
||||
if (!dl_adaptiveTicking) {
|
||||
dl_adaptiveTicking = true;
|
||||
requestAnimationFrame(() => {
|
||||
dl_adaptiveControls.forEach(cbid => {
|
||||
dl_adaptiveStyle(cbid);
|
||||
});
|
||||
dl_adaptiveTicking = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.4.10
|
||||
PKG_RELEASE:=31
|
||||
PKG_RELEASE:=32
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
Reference in new issue
Block a user