mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 00:15:13 +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_NAME:=luci-app-passwall
|
||||||
PKG_VERSION:=26.4.15
|
PKG_VERSION:=26.4.15
|
||||||
PKG_RELEASE:=75
|
PKG_RELEASE:=76
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ if not arg[1] or not m:get(arg[1]) then
|
|||||||
luci.http.redirect(m.redirect)
|
luci.http.redirect(m.redirect)
|
||||||
end
|
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"))
|
m:append(Template(appname .. "/cbi/nodes_listvalue_com"))
|
||||||
|
|
||||||
local has_singbox = api.finded_com("sing-box")
|
local has_singbox = api.finded_com("sing-box")
|
||||||
@@ -96,44 +96,28 @@ o.datatype = "min(1)"
|
|||||||
o.default = 1
|
o.default = 1
|
||||||
o:depends("enable_autoswitch", true)
|
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:depends("enable_autoswitch", true)
|
||||||
o.widget = "checkbox"
|
o.template = appname .. "/cbi/nodes_dynamiclist"
|
||||||
o.template = appname .. "/cbi/nodes_multivalue"
|
|
||||||
o.group = {}
|
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
|
for i, v in pairs(nodes_table) do
|
||||||
if v.protocol ~= "_shunt" then
|
if v.protocol ~= "_shunt" then
|
||||||
o:value(v.id, v.remark)
|
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 })
|
s.fields["enable_autoswitch"]:depends({ node = v.id })
|
||||||
end
|
end
|
||||||
socks_node:value(v.id, v["remark"])
|
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")
|
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||||
end
|
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 = 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)
|
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_NAME:=luci-app-passwall2
|
||||||
PKG_VERSION:=26.4.10
|
PKG_VERSION:=26.4.10
|
||||||
PKG_RELEASE:=31
|
PKG_RELEASE:=32
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
Reference in new issue
Block a user