mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-08 05:04:46 +08:00
update 2026-10-24 16:34:19
This commit is contained in:
1 parent
b2346e5950
commit
6d61e8bf50
36 files changed
+3356
-23
No files matched your search
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=clashoo
|
PKG_NAME:=clashoo
|
||||||
PKG_SHORT_SHA:=3f27ef3
|
PKG_SHORT_SHA:=91e85ee
|
||||||
PKG_COMMIT_DATE:=2026.10.21
|
PKG_COMMIT_DATE:=2026.10.24
|
||||||
|
|
||||||
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
|
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=1
|
||||||
@@ -11,8 +11,8 @@ PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
|
|||||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
||||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
|
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
|
||||||
PKG_SOURCE_VERSION:=3f27ef39f8c96bbfbc8b0b9bb10f5c9ef60149d8
|
PKG_SOURCE_VERSION:=91e85eebf0b654c73424f0a215b3c56f6b39614f
|
||||||
PKG_HASH:=b8534ada1cbd486386e1db95c5720f752ec682fb90cf3b3f963e472976b11e63
|
PKG_HASH:=efa0d9d66a589f06f402f50e0dea349ab9923d36d181303769f0575c0779e562
|
||||||
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
|
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
|
||||||
|
|
||||||
PKG_LICENSE:=GPL3.0+
|
PKG_LICENSE:=GPL3.0+
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.arch }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch:
|
||||||
|
- aarch64_generic
|
||||||
|
- i386_pentium-mmx
|
||||||
|
- loongarch64_generic
|
||||||
|
- mips_mips32
|
||||||
|
- mipsel_24kc
|
||||||
|
- mipsel_mips32
|
||||||
|
- x86_64
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@main
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Building packages
|
||||||
|
uses: sbwml/openwrt-gh-action-sdk@go1.24
|
||||||
|
env:
|
||||||
|
ARCH: ${{ matrix.arch }}-openwrt-24.10
|
||||||
|
FEEDNAME: packages_ci
|
||||||
|
PACKAGES: luci-app-eqosplus
|
||||||
|
NO_REFRESH_CHECK: true
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.arch }}
|
||||||
|
path: bin/packages/${{ matrix.arch }}/packages_ci/*.ipk
|
||||||
|
|
||||||
|
- name: Upload packages
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
allowUpdates: true
|
||||||
|
replacesArtifacts: true
|
||||||
|
artifacts: "bin/packages/${{ matrix.arch }}/packages_ci/*.ipk"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 sirpdboy
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2006-2017 OpenWrt.org#
|
||||||
|
# Copyright (C) 2022-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-eqosplus
|
||||||
|
# This is free software, licensed under the GNU General Public License v2.
|
||||||
|
# See /LICENSE for more information.
|
||||||
|
#
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
THEME_NAME:=eqosplus
|
||||||
|
PKG_NAME:=luci-app-$(THEME_NAME)
|
||||||
|
|
||||||
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI support for eqosplus.
|
||||||
|
LUCI_DESCRIPTION:=LuCI support for Easy eqosplus(Support speed limit based on IP address).
|
||||||
|
LUCI_DEPENDS:=+ip-full +tc +nftables +bc +kmod-ifb
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
PKG_VERSION:=1.3.0
|
||||||
|
PKG_RELEASE:=20251128
|
||||||
|
PKG_MAINTAINER:=sirpdboy <herboy2008@gmail.com>
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/postinst
|
||||||
|
#!/bin/sh
|
||||||
|
rm -f /tmp/luci-*
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/conffiles
|
||||||
|
/etc/config/eqosplus
|
||||||
|
endef
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
# call BuildPackage - OpenWrt buildroot signature
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
 [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
|
||||||
|
|
||||||
|
<h1 align="center">
|
||||||
|
<br>luci-app-eqosplus<br>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://openwrt.org"><img alt="OpenWrt" src="https://img.shields.io/badge/OpenWrt-%E2%89%A519.07-ff0000?logo=openwrt&logoColor=white"></a>
|
||||||
|
<a href="https://www.google.com/chrome/"><img alt="Chrome" src="https://img.shields.io/badge/Chrome-%E2%89%A5111-4285F3?logo=googlechrome&logoColor=white"></a>
|
||||||
|
<a href="https://www.apple.com/safari/"><img alt="Safari" src="https://img.shields.io/badge/Safari-%E2%89%A516.4-000000?logo=safari&logoColor=white"></a>
|
||||||
|
<a href="https://www.mozilla.org/firefox/"><img alt="Firefox" src="https://img.shields.io/badge/Firefox-%E2%89%A5128-FF7138?logo=firefoxbrowser&logoColor=white"></a>
|
||||||
|
<a target="_blank" href="https://github.com/sirpdboy/luci-app-eqosplus/releases"> <img alt="GitHub release" src="https://img.shields.io/github/v/release/sirpdboy/luci-app-eqosplus"></a>
|
||||||
|
<a href="https://github.com/sirpdboy/luci-app-eqosplus/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/sirpdboy/luci-app-eqosplus/total"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[中文] | [English](README.md)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Please read this page carefully, which includes precautions and instructions on how to use it.
|
||||||
|
|
||||||
|
## Enhanced version of EQOS with timed speed limit and added features such as timed limit.
|
||||||
|
|
||||||
|
### Method for downloading source code:
|
||||||
|
|
||||||
|
```Brach
|
||||||
|
# downloading
|
||||||
|
git clone https://github.com/sirpdboy/luci-app-eqosplus package/luci-app-eqosplus
|
||||||
|
make menuconfig
|
||||||
|
```
|
||||||
|
### Configuration Menu
|
||||||
|
```Brach
|
||||||
|
make menuconfig
|
||||||
|
# find LuCI -> Applications, select luci-app-eqosplus, save and exit
|
||||||
|
```
|
||||||
|
### compile
|
||||||
|
|
||||||
|
```Brach
|
||||||
|
# compile
|
||||||
|
make package/luci-app-eqosplus/compile V=s
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## describe
|
||||||
|
|
||||||
|
- luci-app-eqosplus : https://github.com/sirpdboy/luci-app-eqosplus
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## interface
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
# My other project
|
||||||
|
|
||||||
|
- Watch Dog : https://github.com/sirpdboy/luci-app-watchdog
|
||||||
|
- Net Speedtest : https://github.com/sirpdboy/luci-app-netspeedtest
|
||||||
|
- Task Plan : https://github.com/sirpdboy/luci-app-taskplan
|
||||||
|
- Power Off Device : https://github.com/sirpdboy/luci-app-poweroffdevice
|
||||||
|
- OpentoPD Theme : https://github.com/sirpdboy/luci-theme-opentopd
|
||||||
|
- KuCat Theme : https://github.com/sirpdboy/luci-theme-kucat
|
||||||
|
- KuCat Theme Config : https://github.com/sirpdboy/luci-app-kucat-config
|
||||||
|
- NFT Time Control : https://github.com/sirpdboy/luci-app-timecontrol
|
||||||
|
- Parent Control: https://github.com/sirpdboy/luci-theme-parentcontrol
|
||||||
|
- Eqos Plus: https://github.com/sirpdboy/luci-app-eqosplus
|
||||||
|
- Advanced : https://github.com/sirpdboy/luci-app-advanced
|
||||||
|
- ddns-go : https://github.com/sirpdboy/luci-app-ddns-go
|
||||||
|
- Advanced Plus): https://github.com/sirpdboy/luci-app-advancedplus
|
||||||
|
- Net Wizard: https://github.com/sirpdboy/luci-app-netwizard
|
||||||
|
- Part Exp: https://github.com/sirpdboy/luci-app-partexp
|
||||||
|
- Lukcy: https://github.com/sirpdboy/luci-app-lukcy
|
||||||
|
|
||||||
|
## HELP
|
||||||
|
|
||||||
|
| <img src="https://img.shields.io/badge/-Alipay-F5F5F5.svg" href="#赞助支持本项目-" height="25" alt="图飞了"/> | <img src="https://img.shields.io/badge/-WeChat-F5F5F5.svg" height="25" alt="图飞了" href="#赞助支持本项目-"/> |
|
||||||
|
| :-----------------: | :-------------: |
|
||||||
|
| |  |
|
||||||
|
|
||||||
|
<a href="#readme">
|
||||||
|
<img src="https://img.shields.io/badge/-TOP-orange.svg" alt="no" title="Return TOP" align="right"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
 [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
 [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
|
||||||
|
|
||||||
|
<h1 align="center">
|
||||||
|
<br>luci-app-eqosplus<br>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
|
||||||
|
<a target="_blank" href="https://github.com/sirpdboy/luci-app-eqosplus/releases">
|
||||||
|
<img src="https://img.shields.io/github/release/sirpdboy/luci-app-eqosplus.svg?style=flat-square&label=luci-app-eqosplus&colorB=green">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[中文] | [English](README.md)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
请 **认真阅读完毕** 本页面,本页面包含注意事项和如何使用。
|
||||||
|
|
||||||
|
## 功能说明:
|
||||||
|
|
||||||
|
### 定时限速1.2.2版
|
||||||
|
#### 2023.7.19 定时限速1.2.2:增加更多日期:工作日和休息日,自定义日期1,2,3中间用逗号分隔;加入MAC地址限速,从此不用担心IPV6和IPV4的限速问题。
|
||||||
|
|
||||||
|
### 定时限速1.0版
|
||||||
|
#### 2022.12.24 定时限速在eqos的加强版,加入定时限制等功能。
|
||||||
|
|
||||||
|
## 编译使用方法 [](#编译使用方法-)
|
||||||
|
|
||||||
|
将luci-app-eqosplus添加至 LEDE/OpenWRT 源码的方法。
|
||||||
|
|
||||||
|
### 下载源码方法一:
|
||||||
|
编辑源码文件夹根目录feeds.conf.default并加入如下内容:
|
||||||
|
|
||||||
|
```Brach
|
||||||
|
# feeds获取源码:
|
||||||
|
src-git eqosplus https://github.com/sirpdboy/luci-app-eqosplus
|
||||||
|
```
|
||||||
|
```Brach
|
||||||
|
# 更新feeds,并安装主题:
|
||||||
|
scripts/feeds update eqosplus
|
||||||
|
scripts/feeds install luci-app-eqosplus
|
||||||
|
```
|
||||||
|
|
||||||
|
### 下载源码方法:
|
||||||
|
```Brach
|
||||||
|
# 下载源码
|
||||||
|
git clone https://github.com/sirpdboy/luci-app-eqosplus package/luci-app-eqosplus
|
||||||
|
make menuconfig
|
||||||
|
```
|
||||||
|
### 配置菜单
|
||||||
|
```Brach
|
||||||
|
make menuconfig
|
||||||
|
# 找到 LuCI -> Applications, 选择 luci-app-eqosplus, 保存后退出。
|
||||||
|
```
|
||||||
|
### 编译
|
||||||
|
```Brach
|
||||||
|
# 编译固件
|
||||||
|
make package/luci-app-eqosplus/compile V=s
|
||||||
|
```
|
||||||
|
|
||||||
|
## 说明 [](#说明-)
|
||||||
|
|
||||||
|
源码来源:https://github.com/sirpdboy/luci-app-eqosplus
|
||||||
|
|
||||||
|
|
||||||
|
- 你可以随意使用其中的源码,但请注明出处。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 界面
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# My other project
|
||||||
|
|
||||||
|
- 路由安全看门狗 :https://github.com/sirpdboy/luci-app-watchdog
|
||||||
|
- 网络速度测试 :https://github.com/sirpdboy/luci-app-netspeedtest
|
||||||
|
- 计划任务插件(原定时设置) : https://github.com/sirpdboy/luci-app-taskplan
|
||||||
|
- 关机功能插件 : https://github.com/sirpdboy/luci-app-poweroffdevice
|
||||||
|
- opentopd主题 : https://github.com/sirpdboy/luci-theme-opentopd
|
||||||
|
- kucat酷猫主题: https://github.com/sirpdboy/luci-theme-kucat
|
||||||
|
- kucat酷猫主题设置工具: https://github.com/sirpdboy/luci-app-kucat-config
|
||||||
|
- NFT版上网时间控制插件: https://github.com/sirpdboy/luci-app-timecontrol
|
||||||
|
- 家长控制: https://github.com/sirpdboy/luci-theme-parentcontrol
|
||||||
|
- 定时限速: https://github.com/sirpdboy/luci-app-eqosplus
|
||||||
|
- 系统高级设置 : https://github.com/sirpdboy/luci-app-advanced
|
||||||
|
- ddns-go动态域名: https://github.com/sirpdboy/luci-app-ddns-go
|
||||||
|
- 进阶设置(系统高级设置+主题设置kucat/agron/opentopd): https://github.com/sirpdboy/luci-app-advancedplus
|
||||||
|
- 网络设置向导: https://github.com/sirpdboy/luci-app-netwizard
|
||||||
|
- 一键分区扩容: https://github.com/sirpdboy/luci-app-partexp
|
||||||
|
- lukcy大吉: https://github.com/sirpdboy/luci-app-lukcy
|
||||||
|
|
||||||
|
## 捐助
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
| <img src="https://img.shields.io/badge/-支付宝-F5F5F5.svg" href="#赞助支持本项目-" height="25" alt="图飞了"/> | <img src="https://img.shields.io/badge/-微信-F5F5F5.svg" height="25" alt="图飞了" href="#赞助支持本项目-"/> |
|
||||||
|
| :-----------------: | :-------------: |
|
||||||
|
| |  |
|
||||||
|
|
||||||
|
<a href="#readme">
|
||||||
|
<img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="图飞了" title="返回顶部" align="right"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
 [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 645 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 664 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
module("luci.controller.eqosplus", package.seeall)
|
||||||
|
-- Copyright 2022-2023 sirpdboy <herboy2008@gmail.com>
|
||||||
|
function index()
|
||||||
|
if not nixio.fs.access("/etc/config/eqosplus") then return end
|
||||||
|
entry({"admin", "control"}, firstchild(), "Control", 44).dependent = false
|
||||||
|
|
||||||
|
local e = entry({"admin", "control", "eqosplus"}, cbi("eqosplus"), _("Eqosplus"), 10)
|
||||||
|
e.dependent=false
|
||||||
|
e.acl_depends = { "luci-app-eqosplus" }
|
||||||
|
entry({"admin", "control", "eqosplus", "status"}, call("act_status")).leaf = true
|
||||||
|
end
|
||||||
|
|
||||||
|
function act_status()
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local e = {}
|
||||||
|
e.status = sys.call(" busybox ps -w | grep eqosplus | grep -v grep >/dev/null ") == 0
|
||||||
|
luci.http.prepare_content("application/json")
|
||||||
|
luci.http.write_json(e)
|
||||||
|
end
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
-- Copyright 2022-2023 sirpdboy <herboy2008@gmail.com>
|
||||||
|
-- Licensed to the public under the Apache License 2.0.
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local ifaces = sys.net:devices()
|
||||||
|
local WADM = require "luci.tools.webadmin"
|
||||||
|
local ipc = require "luci.ip"
|
||||||
|
local a, t, e
|
||||||
|
|
||||||
|
a = Map("eqosplus", translate("Network speed limit"))
|
||||||
|
a.description = translate("Users can limit the network speed for uploading/downloading through MAC, IP, and IP segments (192.168.110.00-192.168.10.200). The speed unit is MB/second.")
|
||||||
|
a.template = "eqosplus/index"
|
||||||
|
|
||||||
|
t = a:section(TypedSection, "eqosplus")
|
||||||
|
t.anonymous = true
|
||||||
|
|
||||||
|
e = t:option(DummyValue, "eqosplus_status", translate("Status"))
|
||||||
|
e.template = "eqosplus/eqosplus"
|
||||||
|
e.value = translate("Collecting data...")
|
||||||
|
|
||||||
|
|
||||||
|
ipi = t:option(ListValue, "ifname", translate("Interface"), translate("Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"))
|
||||||
|
ipi.default = "1"
|
||||||
|
ipi:value(1,translate("Automatic settings"))
|
||||||
|
ipi:value("br-lan", translate("br-lan (LAN Bridge)"))
|
||||||
|
local function get_wan_interfaces()
|
||||||
|
local result = {}
|
||||||
|
local ubus = require "ubus"
|
||||||
|
|
||||||
|
local conn = ubus.connect()
|
||||||
|
if not conn then
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local network_status = conn:call("network.interface", "dump", {})
|
||||||
|
for _, iface in ipairs(network_status.interface) do
|
||||||
|
if iface.interface:match("^wan") or iface.interface:match("^pppoe") or iface.proto == "pppoe" then
|
||||||
|
local dev = iface.l3_device or iface.device
|
||||||
|
if dev then
|
||||||
|
table.insert(result, {
|
||||||
|
name = dev,
|
||||||
|
proto = iface.proto,
|
||||||
|
logical_name = iface.interface
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
conn:close()
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local wan_ifaces = get_wan_interfaces()
|
||||||
|
for _, iface in ipairs(wan_ifaces) do
|
||||||
|
ipi:value(iface.name, translate(iface.name) .. (iface.proto == "pppoe" and " (PPPoE)" or " (WAN)"))
|
||||||
|
end
|
||||||
|
|
||||||
|
t = a:section(TypedSection, "device")
|
||||||
|
t.template = "cbi/tblsection"
|
||||||
|
t.anonymous = true
|
||||||
|
t.addremove = true
|
||||||
|
|
||||||
|
comment = t:option(Value, "comment", translate("Comment"))
|
||||||
|
comment.size = 8
|
||||||
|
|
||||||
|
e = t:option(Flag, "enable", translate("Enabled"))
|
||||||
|
e.rmempty = false
|
||||||
|
e.size = 4
|
||||||
|
|
||||||
|
ip = t:option(Value, "mac", translate("IP/MAC"))
|
||||||
|
ip.size = 8
|
||||||
|
|
||||||
|
local function get_devices()
|
||||||
|
local devices = {}
|
||||||
|
local seen_ips = {}
|
||||||
|
local ubus = require "ubus"
|
||||||
|
local conn = ubus.connect()
|
||||||
|
|
||||||
|
local function get_hostname(ip)
|
||||||
|
local f = io.popen("nslookup "..ip.." 2>/dev/null | grep 'name =' | cut -d'=' -f2 | sed 's/\\.$//'")
|
||||||
|
if f then
|
||||||
|
local name = f:read("*l")
|
||||||
|
f:close()
|
||||||
|
if name and name ~= "" then
|
||||||
|
return name:match("^%s*(.-)%s*$")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local leases_file = io.open("/tmp/dhcp.leases", "r")
|
||||||
|
if leases_file then
|
||||||
|
for line in leases_file:lines() do
|
||||||
|
local mac, ip_lease, _, hostname = line:match("^(%S+)%s+(%S+)%s+(%S+)%s+(%S+)")
|
||||||
|
if ip_lease == ip and hostname ~= "*" then
|
||||||
|
leases_file:close()
|
||||||
|
return hostname
|
||||||
|
end
|
||||||
|
end
|
||||||
|
leases_file:close()
|
||||||
|
end
|
||||||
|
return "unknown"
|
||||||
|
end
|
||||||
|
if conn then
|
||||||
|
local leases = conn:call("dhcp", "ipv4leases", {}) or {}
|
||||||
|
for _, lease in ipairs(leases) do
|
||||||
|
if lease.ipaddr and lease.mac then
|
||||||
|
local hostname = lease.hostname or get_hostname(lease.ipaddr)
|
||||||
|
devices[#devices+1] = {
|
||||||
|
ip = lease.ipaddr,
|
||||||
|
mac = lease.mac:upper(),
|
||||||
|
hostname = hostname,
|
||||||
|
display = string.format("%s (%s) - %s", lease.ipaddr, lease.mac:upper(), hostname)
|
||||||
|
}
|
||||||
|
seen_ips[lease.ipaddr] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
conn:close()
|
||||||
|
end
|
||||||
|
local arp_cmd = io.popen("ip -4 neigh show dev br-lan 2>/dev/null")
|
||||||
|
if arp_cmd then
|
||||||
|
for line in arp_cmd:lines() do
|
||||||
|
local ip_addr, mac = line:match("^(%S+)%s+.+%s+(%S+)%s+")
|
||||||
|
if ip_addr and mac and mac ~= "00:00:00:00:00:00" and not seen_ips[ip_addr] then
|
||||||
|
mac = mac:upper()
|
||||||
|
local hostname = get_hostname(ip_addr)
|
||||||
|
devices[#devices+1] = {
|
||||||
|
ip = ip_addr,
|
||||||
|
mac = mac,
|
||||||
|
hostname = hostname,
|
||||||
|
display = string.format("%s (%s) - %s", ip_addr, mac, hostname)
|
||||||
|
}
|
||||||
|
seen_ips[ip_addr] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
arp_cmd:close()
|
||||||
|
end
|
||||||
|
table.sort(devices, function(a, b) return a.ip < b.ip end)
|
||||||
|
return devices
|
||||||
|
end
|
||||||
|
|
||||||
|
local devices = get_devices()
|
||||||
|
for _, dev in ipairs(devices) do
|
||||||
|
ip:value(dev.ip, dev.display)
|
||||||
|
end
|
||||||
|
dl = t:option(Value, "download", translate("Downloads"))
|
||||||
|
dl.default = '0.1'
|
||||||
|
dl.size = 4
|
||||||
|
|
||||||
|
ul = t:option(Value, "upload", translate("Uploads"))
|
||||||
|
ul.default = '0.1'
|
||||||
|
ul.size = 4
|
||||||
|
function validate_time(self, value, section)
|
||||||
|
local hh, mm, ss
|
||||||
|
hh, mm, ss = string.match (value, "^(%d?%d):(%d%d)$")
|
||||||
|
hh = tonumber (hh)
|
||||||
|
mm = tonumber (mm)
|
||||||
|
if hh and mm and hh <= 23 and mm <= 59 then
|
||||||
|
return value
|
||||||
|
else
|
||||||
|
return nil, "Time HH:MM or space"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
e = t:option(Value, "timestart", translate("Start control time"))
|
||||||
|
e.placeholder = '00:00'
|
||||||
|
e.default = '00:00'
|
||||||
|
e.validate = validate_time
|
||||||
|
e.rmempty = true
|
||||||
|
e.size = 4
|
||||||
|
|
||||||
|
e = t:option(Value, "timeend", translate("Stop control time"))
|
||||||
|
e.placeholder = '00:00'
|
||||||
|
e.default = '00:00'
|
||||||
|
e.validate = validate_time
|
||||||
|
e.rmempty = true
|
||||||
|
e.size = 4
|
||||||
|
|
||||||
|
week=t:option(Value,"week",translate("Week Day(1~7)"))
|
||||||
|
week.rmempty = true
|
||||||
|
week:value('0',translate("Everyday"))
|
||||||
|
week:value(1,translate("Monday"))
|
||||||
|
week:value(2,translate("Tuesday"))
|
||||||
|
week:value(3,translate("Wednesday"))
|
||||||
|
week:value(4,translate("Thursday"))
|
||||||
|
week:value(5,translate("Friday"))
|
||||||
|
week:value(6,translate("Saturday"))
|
||||||
|
week:value(7,translate("Sunday"))
|
||||||
|
week:value('1,2,3,4,5',translate("Workday"))
|
||||||
|
week:value('6,7',translate("Rest Day"))
|
||||||
|
week.default='0'
|
||||||
|
week.size = 6
|
||||||
|
|
||||||
|
return a
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<%+cbi/valueheader%>
|
||||||
|
<font class="eqosplus_status"><%=pcdata(self:cfgvalue(section) or self.default or "")%></font>
|
||||||
|
<%+cbi/valuefooter%>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<% include("cbi/map") %>
|
||||||
|
<script type="text/javascript">//<![CDATA[
|
||||||
|
XHR.poll(2, '<%=luci.dispatcher.build_url("admin", "control", "eqosplus", "status" )%>', null,
|
||||||
|
function (x, result) {
|
||||||
|
var status = document.getElementsByClassName('eqosplus_status')[0];
|
||||||
|
status.setAttribute("style", "font-weight:bold;");
|
||||||
|
status.setAttribute("color", result.status ? "green" : "red");
|
||||||
|
status.innerHTML = result.status ? '<%=translate("RUNNING")%>' : '<%=translate("NOT RUNNING")%>';
|
||||||
|
}
|
||||||
|
)
|
||||||
|
//]]>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
msgid "Eqosplus"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Network speed limit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Running state"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Not running"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Running"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Users can limit the network speed for uploading/downloading through MAC, IP, and IP segments (192.168.110.00-192.168.10.200). The speed unit is MB/second."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "MAC/IP"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Downloads"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Uploads"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Comment"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Upload bandwidth(Mbit/s)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download bandwidth(Mbit/s)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Start control time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Stop control time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Week Day(1~7)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Suggested feedback:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Rest Day"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Workday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Automatic settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
msgid "Eqosplus"
|
||||||
|
msgstr "定时限速"
|
||||||
|
|
||||||
|
msgid "Network speed limit"
|
||||||
|
msgstr "定时网速限制"
|
||||||
|
|
||||||
|
msgid "Running state"
|
||||||
|
msgstr "运行状态"
|
||||||
|
|
||||||
|
msgid "Not running"
|
||||||
|
msgstr "未运行"
|
||||||
|
|
||||||
|
msgid "Running"
|
||||||
|
msgstr "已运行"
|
||||||
|
|
||||||
|
msgid "Users can limit the network speed for uploading/downloading through MAC, IP.The speed unit is MB/second."
|
||||||
|
msgstr "可以通过MAC,IP限制用户上传/下载的网速。速度单位为<font color=\"red\"><b> MB/秒 </b></font>。"
|
||||||
|
|
||||||
|
msgid "MAC/IP"
|
||||||
|
msgstr "MAC/IP"
|
||||||
|
|
||||||
|
msgid "Downloads"
|
||||||
|
msgstr "下载速度"
|
||||||
|
|
||||||
|
msgid "Uploads"
|
||||||
|
msgstr "上传速度"
|
||||||
|
|
||||||
|
msgid "Comment"
|
||||||
|
msgstr "备注"
|
||||||
|
|
||||||
|
msgid "Upload bandwidth(Mbit/s)"
|
||||||
|
msgstr "上传总带宽(Mbit/s)"
|
||||||
|
|
||||||
|
msgid "Download bandwidth(Mbit/s)"
|
||||||
|
msgstr "下载总带宽(Mbit/s)"
|
||||||
|
|
||||||
|
msgid "Start control time"
|
||||||
|
msgstr "起控时间"
|
||||||
|
|
||||||
|
msgid "Stop control time"
|
||||||
|
msgstr "停控时间"
|
||||||
|
|
||||||
|
msgid "Week Day(1~7)"
|
||||||
|
msgstr "星期(1~7)"
|
||||||
|
|
||||||
|
msgid "Suggested feedback:"
|
||||||
|
msgstr "建议反馈:"
|
||||||
|
|
||||||
|
msgid "Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"
|
||||||
|
msgstr "设定用来限制网络速度的接口,拨号使用pppoe-wan,DHCP模式使用WAN口的硬件接口(如:eth1),通用使用br-lan接口"
|
||||||
|
|
||||||
|
msgid "Rest Day"
|
||||||
|
msgstr "休息日"
|
||||||
|
|
||||||
|
msgid "Workday"
|
||||||
|
msgstr "工作日"
|
||||||
|
|
||||||
|
msgid "Automatic settings"
|
||||||
|
msgstr "自动设置"
|
||||||
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
msgid "Eqosplus"
|
||||||
|
msgstr "定时限速"
|
||||||
|
|
||||||
|
msgid "Network speed limit"
|
||||||
|
msgstr "定时网速限制"
|
||||||
|
|
||||||
|
msgid "Running state"
|
||||||
|
msgstr "运行状态"
|
||||||
|
|
||||||
|
msgid "Not running"
|
||||||
|
msgstr "未运行"
|
||||||
|
|
||||||
|
msgid "Running"
|
||||||
|
msgstr "已运行"
|
||||||
|
|
||||||
|
msgid "Users can limit the network speed for uploading/downloading through MAC, IP, and IP segments (192.168.110.00-192.168.10.200). The speed unit is MB/second."
|
||||||
|
msgstr "用户可以通过MAC、IP、IP段(192.168.10.100-192.168.10.200),限制上传/下载的网络速度。速度单位为MB/秒。。"
|
||||||
|
|
||||||
|
msgid "MAC/IP"
|
||||||
|
msgstr "MAC/IP"
|
||||||
|
|
||||||
|
msgid "Downloads"
|
||||||
|
msgstr "下载速度"
|
||||||
|
|
||||||
|
msgid "Uploads"
|
||||||
|
msgstr "上传速度"
|
||||||
|
|
||||||
|
msgid "Comment"
|
||||||
|
msgstr "备注"
|
||||||
|
|
||||||
|
msgid "Upload bandwidth(Mbit/s)"
|
||||||
|
msgstr "上传总带宽(Mbit/s)"
|
||||||
|
|
||||||
|
msgid "Download bandwidth(Mbit/s)"
|
||||||
|
msgstr "下载总带宽(Mbit/s)"
|
||||||
|
|
||||||
|
msgid "Start control time"
|
||||||
|
msgstr "起控时间"
|
||||||
|
|
||||||
|
msgid "Stop control time"
|
||||||
|
msgstr "停控时间"
|
||||||
|
|
||||||
|
msgid "Week Day(1~7)"
|
||||||
|
msgstr "星期(1~7)"
|
||||||
|
|
||||||
|
msgid "Suggested feedback:"
|
||||||
|
msgstr "建议反馈:"
|
||||||
|
|
||||||
|
msgid "Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"
|
||||||
|
msgstr "设定用来限制网络速度的接口,拨号使用pppoe-wan,DHCP模式使用WAN口的硬件接口(如:eth1),通用使用br-lan接口"
|
||||||
|
|
||||||
|
msgid "Rest Day"
|
||||||
|
msgstr "休息日"
|
||||||
|
|
||||||
|
msgid "Workday"
|
||||||
|
msgstr "工作日"
|
||||||
|
|
||||||
|
msgid "Automatic settings"
|
||||||
|
msgstr "自动设置"
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
config eqosplus
|
||||||
|
option enabled '0'
|
||||||
|
option download '1000'
|
||||||
|
option upload '30'
|
||||||
|
option ifname '1'
|
||||||
|
|
||||||
|
config device
|
||||||
|
option timestart '00:00'
|
||||||
|
option week '0'
|
||||||
|
option download '1'
|
||||||
|
option timeend '23:55'
|
||||||
|
option upload '1'
|
||||||
|
option mac '192.168.10.10'
|
||||||
|
option enable '0'
|
||||||
|
|
||||||
|
config device
|
||||||
|
option mac '192.168.10.10-192.168.10.200'
|
||||||
|
option timestart '00:00'
|
||||||
|
option timeend '00:00'
|
||||||
|
option week '0'
|
||||||
|
option download '0.1'
|
||||||
|
option upload '0.1'
|
||||||
|
option enable '0'
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
[ "$ACTION" = "ifup" ] && [ "$INTERFACE" = "wan" ] && {
|
||||||
|
sleep 5
|
||||||
|
/etc/init.d/eqosplus restart
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
#
|
||||||
|
# Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-eqosplus
|
||||||
|
#
|
||||||
|
|
||||||
|
START=99
|
||||||
|
USE_PROCD=1
|
||||||
|
|
||||||
|
NAME=eqosplus
|
||||||
|
LOCK="/var/lock/$NAME.lock"
|
||||||
|
CR=/etc/crontabs/root
|
||||||
|
|
||||||
|
start_instance() {
|
||||||
|
procd_open_instance
|
||||||
|
procd_set_param command /usr/bin/eqosplusctrl
|
||||||
|
procd_set_param respawn
|
||||||
|
procd_set_param stderr 1
|
||||||
|
procd_close_instance
|
||||||
|
}
|
||||||
|
|
||||||
|
_eqosplus_start() {
|
||||||
|
if [ "$(grep -c 'option enable .1.' /etc/config/$NAME 2>/dev/null)" -gt "0" ]; then
|
||||||
|
if [ x$(uci -q get turboacc.config.sw_flow) = 'x1' ] || [ x$(uci -q get turboacc.config.sfe_flow) = 'x1' ] ;then
|
||||||
|
uci -q set turboacc.config.sw_flow='0'
|
||||||
|
uci -q set turboacc.config.sfe_flow='0'
|
||||||
|
uci -q set turboacc.config.hw_flow='0'
|
||||||
|
uci commit turboacc
|
||||||
|
/etc/init.d/turboacc restart
|
||||||
|
fi
|
||||||
|
touch $LOCK
|
||||||
|
eqosplus start
|
||||||
|
start_instance
|
||||||
|
(crontab -l ; echo "00 1 * * * /etc/init.d/eqosplus start") | sort - | uniq - | crontab -
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_service(){
|
||||||
|
[ -f $LOCK ] && exit
|
||||||
|
stop_service
|
||||||
|
_eqosplus_start
|
||||||
|
rm -f $LOCK
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger 'eqosplus'
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service(){
|
||||||
|
kill -9 $(busybox ps -w | grep 'eqosplusctrl' | grep -v 'grep' | awk '{print $1}') >/dev/null 2>&1
|
||||||
|
sed -i '/eqosplus/d' $CR >/dev/null 2>&1
|
||||||
|
rm -f $LOCK 2>/dev/null
|
||||||
|
eqosplus stop
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
chmod +x /etc/init.d/eqosplus /usr/bin/eqosplus*
|
||||||
|
uci -q batch <<-EOF >/dev/null
|
||||||
|
delete ucitrack.@eqosplus[-1]
|
||||||
|
add ucitrack eqosplus
|
||||||
|
set ucitrack.@eqosplus[-1].init=eqosplus
|
||||||
|
commit ucitrack
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ -s /etc/config/eqosplus ] || echo "config eqosplus" > /etc/config/eqosplus
|
||||||
|
|
||||||
|
rm -f /tmp/luci-indexcache
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright (C) 2006 OpenWrt.org
|
||||||
|
# Copyright 2022-2025 sirpdboy <herboy2008@gmail.com>
|
||||||
|
|
||||||
|
crrun=$1
|
||||||
|
crid=$2
|
||||||
|
NAME=eqosplus
|
||||||
|
IDLIST="/var/$NAME.idlist"
|
||||||
|
LOCK="/var/lock/$NAME.lock"
|
||||||
|
TMPID="/var/$NAME.tmpid"
|
||||||
|
LOG="/var/$NAME_log.log"
|
||||||
|
# idlist=`uci show $NAME | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' `
|
||||||
|
|
||||||
|
|
||||||
|
if [ x$(uci get $NAME.@$NAME[0].ifname 2>/dev/null) = 'x1' ]; then
|
||||||
|
ifname=$(uci -q get network.lan.ifname)
|
||||||
|
[ "x$ifname" = "x" ] && ifname="device" || ifname="ifname"
|
||||||
|
dev=$(uci -q get network.wan.$ifname)
|
||||||
|
[ ! "$dev" ] && dev=br-lan
|
||||||
|
else
|
||||||
|
dev=$(uci -q get $NAME.@$NAME[0].ifname 2>/dev/null)
|
||||||
|
[ ! "$dev" ] && dev=br-lan
|
||||||
|
fi
|
||||||
|
|
||||||
|
bin_nft=$(which nft)
|
||||||
|
bin_tc=$(which tc)
|
||||||
|
bin_ip=$(which ip)
|
||||||
|
DEBUG=1
|
||||||
|
|
||||||
|
nft_type() {
|
||||||
|
if command -v nft >/dev/null; then
|
||||||
|
nftables_ver="true"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Debug functions
|
||||||
|
dbg_nft() {
|
||||||
|
[ "${DEBUG:-0}" -eq 0 ] || echo "DEBUG: nft $*"
|
||||||
|
$bin_nft "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
dbg_tc() {
|
||||||
|
[ "${DEBUG:-0}" -eq 0 ] || echo "DEBUG: tc $*"
|
||||||
|
$bin_tc "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
dbg_ip() {
|
||||||
|
[ "${DEBUG:-0}" -eq 0 ] || echo "DEBUG: ip $*"
|
||||||
|
$bin_ip "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_macaddr() {
|
||||||
|
ret=1
|
||||||
|
if echo "$1" | grep -qE '^([0-9A-Fa-f]{2}[-:]){5}[0-9A-Fa-f]{2}$'; then
|
||||||
|
ret=0
|
||||||
|
fi
|
||||||
|
return $ret
|
||||||
|
}
|
||||||
|
|
||||||
|
# 数值比较函数
|
||||||
|
is_gt_zero() {
|
||||||
|
local value=$1
|
||||||
|
# 使用bc进行浮点数比较
|
||||||
|
echo "$value > 0" | bc -l 2>/dev/null | grep -q 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 转换为kbit
|
||||||
|
to_kbit() {
|
||||||
|
local mb=$1
|
||||||
|
# MB/s 转 kbit/s: 1 MB/s = 8192 kbit/s
|
||||||
|
echo "scale=0; $mb * 8192 / 1" | bc 2>/dev/null || echo "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default commands
|
||||||
|
tc="dbg_tc"
|
||||||
|
ip="dbg_ip"
|
||||||
|
nft="dbg_nft"
|
||||||
|
|
||||||
|
nft_type
|
||||||
|
|
||||||
|
stop_qos() {
|
||||||
|
for face in $(tc qdisc show 2>/dev/null | grep htb | awk '{print $5}'); do
|
||||||
|
$tc qdisc del dev $face root 2>/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
$tc qdisc del dev ${dev} root 2>/dev/null
|
||||||
|
$tc qdisc del dev ${dev}_ifb root 2>/dev/null
|
||||||
|
$tc qdisc del dev ${dev} ingress 2>/dev/null
|
||||||
|
$ip link del dev ${dev}_ifb 2>/dev/null 2>&1
|
||||||
|
|
||||||
|
if [ -n "$nftables_ver" ]; then
|
||||||
|
$nft delete table inet ${NAME} 2>/dev/null
|
||||||
|
$nft delete table bridge ${NAME} 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" > "$IDLIST" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
init_qosplus() {
|
||||||
|
|
||||||
|
insmod sch_htb 2>/dev/null
|
||||||
|
insmod act_mirred 2>/dev/null
|
||||||
|
insmod ifb 2>/dev/null
|
||||||
|
|
||||||
|
$ip link del dev ${dev}_ifb 2>/dev/null
|
||||||
|
$ip link add dev ${dev}_ifb name ${dev}_ifb type ifb
|
||||||
|
$ip link set dev ${dev}_ifb up
|
||||||
|
|
||||||
|
$tc qdisc del dev ${dev} root 2>/dev/null
|
||||||
|
$tc qdisc add dev ${dev} root handle 1:0 htb default 1
|
||||||
|
$tc class add dev ${dev} parent 1:0 classid 1:1 htb rate 1000mbit ceil 1000mbit
|
||||||
|
|
||||||
|
$tc qdisc del dev ${dev}_ifb root 2>/dev/null
|
||||||
|
$tc qdisc add dev ${dev}_ifb root handle 1:0 htb default 1
|
||||||
|
$tc class add dev ${dev}_ifb parent 1:0 classid 1:1 htb rate 1000mbit ceil 1000mbit
|
||||||
|
lanipaddr=$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F '.' '{print $1"."$2"."$3".0/24"}')
|
||||||
|
if [ -n "$lanipaddr" ]; then
|
||||||
|
$tc filter add dev $dev parent 1:0 protocol ip prio 1 u32 match ip src "$lanipaddr" match ip dst "$lanipaddr" flowid 1:1
|
||||||
|
$tc filter add dev ${dev}_ifb parent 1:0 protocol ip prio 1 u32 match ip src "$lanipaddr" match ip dst "$lanipaddr" flowid 1:1
|
||||||
|
fi
|
||||||
|
$tc qdisc del dev ${dev} ingress 2>/dev/null
|
||||||
|
$tc qdisc add dev ${dev} ingress
|
||||||
|
$tc filter add dev ${dev} parent ffff: protocol all prio 2 u32 match u32 0 0 flowid 1:1 action mirred egress redirect dev ${dev}_ifb
|
||||||
|
|
||||||
|
if [ -n "$nftables_ver" ]; then
|
||||||
|
$nft delete table inet ${NAME} 2>/dev/null
|
||||||
|
$nft delete table bridge ${NAME} 2>/dev/null
|
||||||
|
|
||||||
|
$nft add table inet ${NAME}
|
||||||
|
|
||||||
|
$nft add chain inet ${NAME} mark_forward { type filter hook forward priority -200\; policy accept\; }
|
||||||
|
$nft add chain inet ${NAME} mark_output { type filter hook output priority -200\; policy accept\; }
|
||||||
|
$nft add chain inet ${NAME} mark_postrouting { type filter hook postrouting priority -200\; policy accept\; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
del_id() {
|
||||||
|
local list_id=$1
|
||||||
|
id=$((list_id * 10 + 1000))
|
||||||
|
[ "${DEBUG:-0}" -eq 0 ] || echo "D: del_id $@ --$id --$mac"
|
||||||
|
$tc filter del dev ${dev} parent 1: handle 800::$(printf "%x" $id) 2>/dev/null
|
||||||
|
$tc class del dev ${dev} parent 1:1 classid 1:$id 2>/dev/null
|
||||||
|
$tc filter del dev ${dev}_ifb parent 1: handle 800::$(printf "%x" $id) 2>/dev/null
|
||||||
|
$tc class del dev ${dev}_ifb parent 1:1 classid 1:$id 2>/dev/null
|
||||||
|
mac=$(uci -q get $NAME.@device[$list_id].mac 2>/dev/null)
|
||||||
|
if [ -n "$nftables_ver" ] && [ -n "$mac" ]; then
|
||||||
|
if is_macaddr "$mac"; then
|
||||||
|
$nft delete rule inet ${NAME} mark_forward ether saddr $mac counter 2>/dev/null
|
||||||
|
$nft delete rule inet ${NAME} mark_output ether saddr $mac counter 2>/dev/null
|
||||||
|
$nft delete rule inet ${NAME} mark_postrouting ether saddr $mac counter 2>/dev/null
|
||||||
|
else
|
||||||
|
Z=$(echo $mac | awk -F '[/]' '{print $2}')
|
||||||
|
[ -n "$Z" ] && mac=$(echo $mac | awk -F '[/]' '{print $1}') || Z=32
|
||||||
|
$nft delete rule inet ${NAME} mark_forward ip saddr $mac/$Z counter 2>/dev/null
|
||||||
|
$nft delete rule inet ${NAME} mark_output ip saddr $mac/$Z counter 2>/dev/null
|
||||||
|
$nft delete rule inet ${NAME} mark_postrouting ip saddr $mac/$Z counter 2>/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
add_mac() {
|
||||||
|
local list_id=$1
|
||||||
|
id=$((list_id * 10 + 1000))
|
||||||
|
M0=$(echo $mac | cut -d : -f 1)$(echo $mac | cut -d : -f 2)
|
||||||
|
M1=$(echo $mac | cut -d : -f 3)$(echo $mac | cut -d : -f 4)
|
||||||
|
M2=$(echo $mac | cut -d : -f 5)$(echo $mac | cut -d : -f 6)
|
||||||
|
[ "${DEBUG:-0}" -eq 0 ] || echo "D: add_mac $@ --id:$id --mac:$mac M012--$M0-$M1-$M2"
|
||||||
|
|
||||||
|
if is_gt_zero "$UL"; then
|
||||||
|
if [ -n "$nftables_ver" ]; then
|
||||||
|
$nft add rule inet ${NAME} mark_forward ether saddr $mac counter meta mark set $id
|
||||||
|
$nft add rule inet ${NAME} mark_output ether saddr $mac counter meta mark set $id
|
||||||
|
$nft add rule inet ${NAME} mark_postrouting ether saddr $mac counter meta mark set $id
|
||||||
|
fi
|
||||||
|
$tc class add dev ${dev}_ifb parent 1:1 classid 1:$id htb rate "${UL}kbit" ceil "${UL}kbit" prio $id burst 15k cburst 15k
|
||||||
|
$tc qdisc add dev ${dev}_ifb parent 1:$id handle ${id}: sfq perturb 10
|
||||||
|
|
||||||
|
$tc filter add dev ${dev}_ifb parent 1: protocol ip prio $id handle $id fw flowid 1:$id
|
||||||
|
|
||||||
|
$tc filter add dev ${dev}_ifb parent 1: protocol ip prio $((id + 100)) u32 \
|
||||||
|
match u16 0x0800 0xFFFF at -2 \
|
||||||
|
match u16 0x${M2} 0xFFFF at -4 \
|
||||||
|
match u32 0x${M0}${M1} 0xFFFFFFFF at -8 \
|
||||||
|
flowid 1:$id
|
||||||
|
fi
|
||||||
|
if is_gt_zero "$DL"; then
|
||||||
|
$tc class add dev ${dev} parent 1:1 classid 1:$id htb rate "${DL}kbit" ceil "${DL}kbit" prio $id burst 15k cburst 15k
|
||||||
|
$tc qdisc add dev ${dev} parent 1:$id handle ${id}: sfq perturb 10
|
||||||
|
|
||||||
|
$tc filter add dev ${dev} parent 1: protocol ip prio $id u32 \
|
||||||
|
match u16 0x0800 0xFFFF at -2 \
|
||||||
|
match u32 0x${M1}${M2} 0xFFFFFFFF at -12 \
|
||||||
|
match u16 0x${M0} 0xFFFF at -14 \
|
||||||
|
flowid 1:$id
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
add_ip() {
|
||||||
|
local list_id=$1
|
||||||
|
id=$((list_id * 10 + 1000))
|
||||||
|
[ "${DEBUG:-0}" -eq 0 ] || echo "D: add_ip $@ --$id --$mac"
|
||||||
|
|
||||||
|
Z=$(echo $mac | awk -F '[/]' '{print $2}')
|
||||||
|
[ -n "$Z" ] && mac=$(echo $mac | awk -F '[/]' '{print $1}') || Z=32
|
||||||
|
if is_gt_zero "$UL"; then
|
||||||
|
if [ -n "$nftables_ver" ]; then
|
||||||
|
$nft add rule inet ${NAME} mark_forward ip saddr $mac/$Z counter meta mark set $id
|
||||||
|
$nft add rule inet ${NAME} mark_output ip saddr $mac/$Z counter meta mark set $id
|
||||||
|
$nft add rule inet ${NAME} mark_postrouting ip saddr $mac/$Z counter meta mark set $id
|
||||||
|
fi
|
||||||
|
$tc class add dev ${dev}_ifb parent 1:1 classid 1:$id htb rate "${UL}kbit" ceil "${UL}kbit" prio $id burst 15k cburst 15k
|
||||||
|
$tc qdisc add dev ${dev}_ifb parent 1:$id handle ${id}: sfq perturb 10
|
||||||
|
$tc filter add dev ${dev}_ifb parent 1: protocol ip prio $id handle $id fw flowid 1:$id
|
||||||
|
$tc filter add dev ${dev}_ifb parent 1:0 prio $((id + 100)) protocol ip u32 \
|
||||||
|
match ip src "$mac"/"$Z" \
|
||||||
|
flowid 1:$id
|
||||||
|
fi
|
||||||
|
if is_gt_zero "$DL"; then
|
||||||
|
$tc class add dev ${dev} parent 1:1 classid 1:$id htb rate "${DL}kbit" ceil "${DL}kbit" prio $id burst 15k cburst 15k
|
||||||
|
$tc qdisc add dev ${dev} parent 1:$id handle ${id}: sfq perturb 10
|
||||||
|
$tc filter add dev ${dev} parent 1:0 prio $id protocol ip u32 \
|
||||||
|
match ip dst "$mac"/"$Z" \
|
||||||
|
flowid 1:$id
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_time() {
|
||||||
|
local start=$1
|
||||||
|
local end=$2
|
||||||
|
local current=$(date +%H%M)
|
||||||
|
local start_min=$((10#${start:0:2}*60 + 10#${start:3:2}))
|
||||||
|
local end_min=$((10#${end:0:2}*60 + 10#${end:3:2}))
|
||||||
|
local current_min=$((10#${current:0:2}*60 + 10#${current:2:2}))
|
||||||
|
|
||||||
|
if [[ $start_min -lt $end_min ]]; then
|
||||||
|
[[ $current_min -ge $start_min && $current_min -lt $end_min ]]
|
||||||
|
else
|
||||||
|
[[ $current_min -ge $start_min || $current_min -lt $end_min ]]
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_list() {
|
||||||
|
local i=$1
|
||||||
|
local start_time=$(uci -q get $NAME.@device[$i].timestart 2>/dev/null)
|
||||||
|
local end_time=$(uci -q get $NAME.@device[$i].timeend 2>/dev/null)
|
||||||
|
local wweek=$(uci -q get $NAME.@device[$i].week 2>/dev/null)
|
||||||
|
local current_weekday=$(date +%u)
|
||||||
|
[ -z "$start_time" ] && [ -z "$end_time" ] && [ -z "$wweek" ] && return 0
|
||||||
|
|
||||||
|
if [ -n "$wweek" ] && [ "$wweek" != "0" ]; then
|
||||||
|
local day_match=0
|
||||||
|
for day in $(echo $wweek | tr ',' ' '); do
|
||||||
|
[ "$day" -eq "$current_weekday" ] && day_match=1 && break
|
||||||
|
done
|
||||||
|
[ "$day_match" -eq 0 ] && return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$start_time" ] && [ -n "$end_time" ]; then
|
||||||
|
check_time "$start_time" "$end_time" || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$crrun" in
|
||||||
|
"stop")
|
||||||
|
stop_qos
|
||||||
|
;;
|
||||||
|
"start")
|
||||||
|
stop_qos > /dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
init_qosplus
|
||||||
|
idlist=$(uci show $NAME 2>/dev/null | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' | sort -nu | sed -e 's/^/!/g' -e 's/$/!/g' > $IDLIST; cat $IDLIST 2>/dev/null | sed -e 's/!//g')
|
||||||
|
|
||||||
|
if [ ! -s "$IDLIST" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
device_count=0
|
||||||
|
for list in $(echo $idlist | sed -e 's/!//g'); do
|
||||||
|
if check_list $list; then
|
||||||
|
mac=$(uci -q get $NAME.@device[$list].mac 2>/dev/null)
|
||||||
|
download_mb=$(uci -q get $NAME.@device[$list].download 2>/dev/null)
|
||||||
|
upload_mb=$(uci -q get $NAME.@device[$list].upload 2>/dev/null)
|
||||||
|
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
|
||||||
|
|
||||||
|
# 转换为kbit
|
||||||
|
DL=$(to_kbit "$download_mb")
|
||||||
|
UL=$(to_kbit "$upload_mb")
|
||||||
|
|
||||||
|
if [ -n "$mac" ]; then
|
||||||
|
if is_macaddr "$mac"; then
|
||||||
|
add_mac $list >>$LOG
|
||||||
|
echo "✅ MAC限速: $comment ($mac) - 下载:${download_mb}MB/s(${DL}kbit/s) 上传:${upload_mb}MB/s(${UL}kbit/s)" >>$LOG
|
||||||
|
else
|
||||||
|
add_ip $list >>$LOG
|
||||||
|
echo "✅ IP限速: $comment ($mac) - 下载:${download_mb}MB/s(${DL}kbit/s) 上传:${upload_mb}MB/s(${UL}kbit/s)" >>$LOG
|
||||||
|
fi
|
||||||
|
device_count=$((device_count + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
[ -f "$IDLIST" ] && [ $(grep "!${list}!" "$IDLIST" 2>/dev/null | wc -l) -gt 0 ] && {
|
||||||
|
del_id $list >>$LOG
|
||||||
|
sed -i "/!$list!/d" "$IDLIST" >/dev/null 2>&1
|
||||||
|
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "EQOSPLUS限速服务启动完成,共配置 $device_count 个设备" >>$LOG
|
||||||
|
echo ""
|
||||||
|
echo "当前限速状态:" >>$LOG
|
||||||
|
tc -s class show dev $dev | grep "rate" | head -5 >>$LOG
|
||||||
|
tc -s class show dev ${dev}_ifb | grep "rate" | head -5 >>$LOG
|
||||||
|
|
||||||
|
;;
|
||||||
|
"add")
|
||||||
|
echo "➕ 添加限速规则..."
|
||||||
|
for list in $(echo $crid | sed -e 's/!//g' | sed 's/,/ /g'); do
|
||||||
|
mac=$(uci -q get $NAME.@device[$list].mac 2>/dev/null)
|
||||||
|
download_mb=$(uci -q get $NAME.@device[$list].download 2>/dev/null)
|
||||||
|
upload_mb=$(uci -q get $NAME.@device[$list].upload 2>/dev/null)
|
||||||
|
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
|
||||||
|
DL=$(to_kbit "$download_mb")
|
||||||
|
UL=$(to_kbit "$upload_mb")
|
||||||
|
|
||||||
|
if is_macaddr $mac; then
|
||||||
|
add_mac $list
|
||||||
|
echo "✅ 添加MAC限速: $comment ($mac)" >>$LOG
|
||||||
|
else
|
||||||
|
add_ip $list
|
||||||
|
echo "✅ 添加IP限速: $comment ($mac)" >>$LOG
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
"del")
|
||||||
|
for list in $(echo $crid | sed -e 's/!//g' | sed 's/,/ /g'); do
|
||||||
|
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
|
||||||
|
mac=$(uci -q get $NAME.@device[$list].mac 2>/dev/null)
|
||||||
|
del_id $list
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
"status")
|
||||||
|
echo "EQOSPLUS限速服务状态"
|
||||||
|
echo "### 网络接口: $dev ###"
|
||||||
|
echo "# 下载队列统计 #"
|
||||||
|
tc -s qdisc show dev $dev 2>/dev/null || echo "无下载队列"
|
||||||
|
echo "# 下载类统计 #"
|
||||||
|
tc -s class show dev $dev 2>/dev/null | grep -E "class htb|rate [0-9]" || echo "无下载类"
|
||||||
|
echo "# 下载过滤器 #"
|
||||||
|
tc -s filter show dev $dev parent 1: 2>/dev/null | head -10 || echo "无下载过滤器"
|
||||||
|
|
||||||
|
echo "### 虚拟接口: ${dev}_ifb ###"
|
||||||
|
echo "# 上传队列统计 #"
|
||||||
|
tc -s qdisc show dev ${dev}_ifb 2>/dev/null || echo "无上传队列"
|
||||||
|
echo "# 上传类统计 #"
|
||||||
|
tc -s class show dev ${dev}_ifb 2>/dev/null | grep -E "class htb|rate [0-9]" || echo "无上传类"
|
||||||
|
echo "# 上传过滤器 #"
|
||||||
|
tc -s filter show dev ${dev}_ifb parent 1: 2>/dev/null | head -10 || echo "无上传过滤器"
|
||||||
|
|
||||||
|
if [ -n "$nftables_ver" ]; then
|
||||||
|
echo "### NFTables规则 ###"
|
||||||
|
nft list table inet ${NAME} 2>/dev/null || echo "没有NFTables规则"
|
||||||
|
else
|
||||||
|
echo "NFTables不可用"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 显示流量统计
|
||||||
|
echo "### 流量统计 ###"
|
||||||
|
echo "下载流量:"
|
||||||
|
tc -s class show dev $dev 2>/dev/null | grep "Sent" | head -5
|
||||||
|
echo "上传流量:"
|
||||||
|
tc -s class show dev ${dev}_ifb 2>/dev/null | grep "Sent" | head -5
|
||||||
|
;;
|
||||||
|
|
||||||
|
esac
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Copyright (C) 2006 OpenWrt.org
|
||||||
|
# Copyright 2022-2025 sirpdboy <herboy2008@gmail.com>
|
||||||
|
NAME=eqosplus
|
||||||
|
IDLIST="/var/$NAME.idlist"
|
||||||
|
TMPID="/var/$NAME.tmpid"
|
||||||
|
|
||||||
|
idlist=`uci show $NAME | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' `
|
||||||
|
|
||||||
|
check_list() {
|
||||||
|
i=$1
|
||||||
|
checki='0'
|
||||||
|
start_time=$(uci -q get $NAME.@device[$i].timestart 2>/dev/null)
|
||||||
|
end_time=$(uci -q get $NAME.@device[$i].timeend 2>/dev/null)
|
||||||
|
wweek=`uci -q get $NAME.@device[$i].week `
|
||||||
|
current_time=$(date +%H:%M)
|
||||||
|
current_weekday=$(date +%u)
|
||||||
|
for ww in `echo $wweek | sed 's/,/ /g' `; do
|
||||||
|
if [ $current_weekday = $ww ] || [ 'x0' = x$ww ] ;then
|
||||||
|
[ "$start_time" = "$end_time" ] && checki='1' || {
|
||||||
|
if [ "$start_time" \< "$end_time" ]; then
|
||||||
|
if [ "$current_time" \> "$start_time" ] && [ "$current_time" \< "$end_time" ] ; then
|
||||||
|
checki='1'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "$start_time" \> "$end_time" ] ; then
|
||||||
|
if [ "$current_time" \< "$start_time" ] && [ "$current_time" \> "$end_time" ] ; then
|
||||||
|
checki='1'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idlistusr(){
|
||||||
|
checki='0'
|
||||||
|
[ -s $IDLIST ] || touch $IDLIST
|
||||||
|
for list in $idlist ;do
|
||||||
|
check_list $list
|
||||||
|
if [ $checki == '1' ] ; then
|
||||||
|
[ `cat $IDLIST 2>/dev/null | grep "!${list}!" | wc -l ` -gt 0 ] || {
|
||||||
|
eqosplus add $list
|
||||||
|
echo "!${list}!" >> $IDLIST ; cat $IDLIST | sort | uniq > $TMPID ;cat $TMPID >$IDLIST ;rm -rf $TMPID
|
||||||
|
}
|
||||||
|
else
|
||||||
|
[ `cat $IDLIST 2>/dev/null | grep "!${list}!" | wc -l ` -gt 0 ] && {
|
||||||
|
eqosplus del $list
|
||||||
|
sed -i "/!$list!/d" $IDLIST >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
while :;do
|
||||||
|
sleep 30
|
||||||
|
idlistusr
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"luci-app-eqosplus": {
|
||||||
|
"description": "Grant UCI access for luci-app-eqosplus",
|
||||||
|
"read": {
|
||||||
|
"uci": [ "eqosplus" ]
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"uci": [ "eqosplus" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 585 KiB |
@@ -0,0 +1,773 @@
|
|||||||
|
/*
|
||||||
|
* Click-to-sort headers and a per-group filter bar for the node list.
|
||||||
|
*
|
||||||
|
* Sort reorders rows inside one group table and calls set_order(), the same
|
||||||
|
* path as drag-and-drop and To Top. Nothing is written until Save & Apply.
|
||||||
|
*
|
||||||
|
* A filter only toggles the pw-row-hidden class. Rows stay in the form, so
|
||||||
|
* Save & Apply still posts every node and the full group.order value.
|
||||||
|
*
|
||||||
|
* Translated strings are read from #pw-nodesort-i18n (the node list template).
|
||||||
|
* The template calls init() once, then enhanceAll() after loadNodeList()
|
||||||
|
* has built the group tables.
|
||||||
|
*
|
||||||
|
* auto_detection_time is a top-level let in the template, not a window
|
||||||
|
* property. It is read by name so rows are not moved while automatic Ping
|
||||||
|
* or TCPing is still writing results by row position.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var COLUMNS = {
|
||||||
|
'pw-remark': 'name',
|
||||||
|
'pw-ping': 'ping',
|
||||||
|
'pw-tcping': 'tcping',
|
||||||
|
'pw-urltest': 'urltest'
|
||||||
|
};
|
||||||
|
/* sub controls drawn inside the Remarks header: [sort key, label key or symbol, title key] */
|
||||||
|
var NAME_EXTRA = [
|
||||||
|
['type', 'type', 'typeHint'],
|
||||||
|
['addr', 'addr', 'addrHint'],
|
||||||
|
['orig', '\u21BA', 'origHint']
|
||||||
|
];
|
||||||
|
|
||||||
|
var state = {
|
||||||
|
app: 'passwall',
|
||||||
|
root: null,
|
||||||
|
text: {},
|
||||||
|
bound: false
|
||||||
|
};
|
||||||
|
var searchTimer = null;
|
||||||
|
|
||||||
|
function T(key) {
|
||||||
|
var value = state.text[key];
|
||||||
|
return (value == null || value === '') ? key : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readText(box) {
|
||||||
|
var text = {};
|
||||||
|
var spans = box.querySelectorAll('[data-k]');
|
||||||
|
for (var i = 0; i < spans.length; i++)
|
||||||
|
text[spans[i].getAttribute('data-k')] = spans[i].textContent;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
var collator = null;
|
||||||
|
function prepareCollator() {
|
||||||
|
var lang = document.documentElement.lang || (navigator.language || '');
|
||||||
|
try {
|
||||||
|
collator = (window.Intl && Intl.Collator) ? new Intl.Collator(lang || undefined, { numeric: true, sensitivity: 'base' }) : null;
|
||||||
|
} catch (e) {
|
||||||
|
collator = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpText(a, b) {
|
||||||
|
if (collator)
|
||||||
|
return collator.compare(a, b);
|
||||||
|
return a < b ? -1 : (a > b ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "123 ms", "12.5 ms", "1.2 s" -> milliseconds. Timeout, Error, ---, Test stay null. */
|
||||||
|
function parseLatency(text) {
|
||||||
|
var t = String(text || '').replace(/\s+/g, ' ').trim();
|
||||||
|
var m = /^(\d+(?:[.,]\d+)?)\s*(ms|s)?$/i.exec(t);
|
||||||
|
if (!m)
|
||||||
|
return null;
|
||||||
|
var v = parseFloat(m[1].replace(',', '.'));
|
||||||
|
if (isNaN(v))
|
||||||
|
return null;
|
||||||
|
if (m[2] && /^s$/i.test(m[2]))
|
||||||
|
v *= 1000;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hiddenVal(id, opt) {
|
||||||
|
var el = document.getElementById('cbid.' + state.app + '.' + id + '.' + opt);
|
||||||
|
return el && el.value != null ? String(el.value).trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cellsOf(tr) {
|
||||||
|
var out = [];
|
||||||
|
for (var i = 0; i < tr.children.length; i++) {
|
||||||
|
var tag = tr.children[i].tagName;
|
||||||
|
if (tag === 'TD' || tag === 'TH')
|
||||||
|
out.push(tr.children[i]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wordSplit;
|
||||||
|
try {
|
||||||
|
wordSplit = new RegExp('[^\\p{L}\\p{N}]+', 'u');
|
||||||
|
} catch (e) {
|
||||||
|
wordSplit = /[\s.,;:!?|\/\\()\[\]{}<>"'`~@#$%^&*=+_-]+/;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wordsOf(text) {
|
||||||
|
var seen = {};
|
||||||
|
var out = [];
|
||||||
|
var parts = String(text || '').split(wordSplit);
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
var w = parts[i];
|
||||||
|
if (w.length < 2 || /^\d+$/.test(w))
|
||||||
|
continue;
|
||||||
|
var k = w.toLowerCase();
|
||||||
|
if (!seen[k]) {
|
||||||
|
seen[k] = 1;
|
||||||
|
out.push(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function infoOf(table) {
|
||||||
|
var prefix = 'cbi-' + state.app + '-nodes-';
|
||||||
|
var id = table && table.id ? table.id : '';
|
||||||
|
if (id.indexOf(prefix) !== 0 || id.slice(-6) !== '-table')
|
||||||
|
return null;
|
||||||
|
return { group: id.slice(prefix.length, -6) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowId(tr) {
|
||||||
|
var prefix = 'cbi-' + state.app + '-';
|
||||||
|
return tr.id.indexOf(prefix) === 0 ? tr.id.substring(prefix.length) : tr.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bodyOf(table) {
|
||||||
|
return table.querySelector('tbody') || table;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataRows(table) {
|
||||||
|
var body = bodyOf(table);
|
||||||
|
var out = [];
|
||||||
|
for (var i = 0; i < body.children.length; i++) {
|
||||||
|
var tr = body.children[i];
|
||||||
|
if (tr.tagName === 'TR' && tr.classList.contains('cbi-section-table-row') && !tr.classList.contains('placeholder'))
|
||||||
|
out.push(tr);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function colIndexMap(table) {
|
||||||
|
var rows = dataRows(table);
|
||||||
|
var map = {};
|
||||||
|
if (!rows.length)
|
||||||
|
return map;
|
||||||
|
var cells = cellsOf(rows[0]);
|
||||||
|
for (var i = 0; i < cells.length; i++) {
|
||||||
|
for (var cls in COLUMNS) {
|
||||||
|
if (cells[i].classList.contains(cls))
|
||||||
|
map[COLUMNS[cls]] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeOf(tr, cols) {
|
||||||
|
var cell = cellsOf(tr)[cols.name];
|
||||||
|
var text = cell ? cell.textContent.replace(/\s+/g, ' ').trim() : '';
|
||||||
|
var i = text.indexOf('\uFF1A');
|
||||||
|
if (i < 0)
|
||||||
|
i = text.indexOf(':');
|
||||||
|
return (i < 0 ? '' : text.slice(0, i)).replace(/\s+Realm$/, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortKey(tr, key, cols) {
|
||||||
|
var id = rowId(tr);
|
||||||
|
var cell;
|
||||||
|
if (key === 'name')
|
||||||
|
return hiddenVal(id, 'remarks') || ((cellsOf(tr)[cols.name] || {}).textContent || '').trim();
|
||||||
|
if (key === 'type')
|
||||||
|
return typeOf(tr, cols) + '\u0000' + hiddenVal(id, 'remarks');
|
||||||
|
if (key === 'addr') {
|
||||||
|
var address = hiddenVal(id, 'address');
|
||||||
|
if (!address)
|
||||||
|
return null;
|
||||||
|
return address + '\u0000' + ('00000' + hiddenVal(id, 'port')).slice(-5);
|
||||||
|
}
|
||||||
|
cell = cellsOf(tr)[cols[key]];
|
||||||
|
return cell ? parseLatency(cell.textContent) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectionMode() {
|
||||||
|
try {
|
||||||
|
if (typeof auto_detection_time === 'undefined')
|
||||||
|
return null;
|
||||||
|
return auto_detection_time;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* { inflight: requests still running, left: cells still waiting for a result } */
|
||||||
|
function pingBusy() {
|
||||||
|
var mode = detectionMode();
|
||||||
|
var inflight = 0;
|
||||||
|
var left = 0;
|
||||||
|
if (mode !== 'icmp' && mode !== 'tcping')
|
||||||
|
return { inflight: 0, left: 0 };
|
||||||
|
var list = window.ajax && window.ajax.list;
|
||||||
|
if (list && list.length) {
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
if (list[i].readyState > 0 && list[i].readyState < 4)
|
||||||
|
inflight++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!state.root)
|
||||||
|
return { inflight: inflight, left: 0 };
|
||||||
|
var spans = state.root.getElementsByClassName(mode === 'tcping' ? 'tcping_value' : 'ping_value');
|
||||||
|
for (var s = 0; s < spans.length; s++) {
|
||||||
|
var id = spans[s].getAttribute('cbiid');
|
||||||
|
if (!id || spans[s].textContent.trim() !== '---')
|
||||||
|
continue;
|
||||||
|
var full = null;
|
||||||
|
if (typeof window.get_address_full === 'function')
|
||||||
|
full = window.get_address_full(id);
|
||||||
|
if (!full)
|
||||||
|
full = { address: hiddenVal(id, 'address'), port: hiddenVal(id, 'port') };
|
||||||
|
if (full.address && (mode === 'icmp' || full.port))
|
||||||
|
left++;
|
||||||
|
}
|
||||||
|
return { inflight: inflight, left: left };
|
||||||
|
}
|
||||||
|
|
||||||
|
function note(table, text) {
|
||||||
|
var id = 'pw-note-' + table.id;
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (!text) {
|
||||||
|
if (el && el.parentNode)
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement('div');
|
||||||
|
el.id = id;
|
||||||
|
el.className = 'pw-sort-note';
|
||||||
|
table.parentNode.insertBefore(el, table);
|
||||||
|
}
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setIndicator(table, th, sub, key, dir) {
|
||||||
|
var arrows = table.querySelectorAll('.pw-sort-arrow');
|
||||||
|
var subs = table.querySelectorAll('.pw-sort-sub');
|
||||||
|
var headers = table.querySelectorAll('th[data-pw-sort]');
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < arrows.length; i++)
|
||||||
|
arrows[i].textContent = '';
|
||||||
|
for (i = 0; i < subs.length; i++)
|
||||||
|
subs[i].classList.remove('pw-sort-active');
|
||||||
|
for (i = 0; i < headers.length; i++) {
|
||||||
|
if (headers[i] !== th) {
|
||||||
|
delete headers[i].dataset.pwDir;
|
||||||
|
delete headers[i].dataset.pwKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var arrow = th.querySelector('.pw-sort-arrow');
|
||||||
|
if (arrow && key !== 'orig' && !sub)
|
||||||
|
arrow.textContent = dir === 'desc' ? '\u25BC' : '\u25B2';
|
||||||
|
if (sub && key !== 'orig')
|
||||||
|
sub.classList.add('pw-sort-active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOrder(table, info, rows) {
|
||||||
|
var body = bodyOf(table);
|
||||||
|
var frag = document.createDocumentFragment();
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
rows[i].classList.remove('cbi-rowstyle-1', 'cbi-rowstyle-2');
|
||||||
|
rows[i].classList.add('cbi-rowstyle-' + (i % 2 + 1));
|
||||||
|
frag.appendChild(rows[i]);
|
||||||
|
}
|
||||||
|
body.appendChild(frag);
|
||||||
|
if (typeof window.set_order === 'function')
|
||||||
|
window.set_order(info.group);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSort(table, th, sub, key) {
|
||||||
|
var info = infoOf(table);
|
||||||
|
var rows = dataRows(table);
|
||||||
|
var dir = 'asc';
|
||||||
|
if (!info || rows.length < 2)
|
||||||
|
return;
|
||||||
|
if (key === 'orig') {
|
||||||
|
var pos = {};
|
||||||
|
var orig = table.__pwOrig || [];
|
||||||
|
for (var n = 0; n < orig.length; n++)
|
||||||
|
pos[orig[n]] = n;
|
||||||
|
rows.sort(function (a, b) {
|
||||||
|
var pa = pos[rowId(a)];
|
||||||
|
var pb = pos[rowId(b)];
|
||||||
|
return (pa == null ? 1e9 : pa) - (pb == null ? 1e9 : pb);
|
||||||
|
});
|
||||||
|
delete th.dataset.pwDir;
|
||||||
|
delete th.dataset.pwKey;
|
||||||
|
} else {
|
||||||
|
dir = (th.dataset.pwKey === key && th.dataset.pwDir === 'asc') ? 'desc' : 'asc';
|
||||||
|
th.dataset.pwKey = key;
|
||||||
|
th.dataset.pwDir = dir;
|
||||||
|
var cols = colIndexMap(table);
|
||||||
|
var sign = dir === 'desc' ? -1 : 1;
|
||||||
|
var list = [];
|
||||||
|
for (var i = 0; i < rows.length; i++)
|
||||||
|
list.push({ tr: rows[i], i: i, k: sortKey(rows[i], key, cols) });
|
||||||
|
list.sort(function (a, b) {
|
||||||
|
var emptyA = (a.k === null || a.k === '');
|
||||||
|
var emptyB = (b.k === null || b.k === '');
|
||||||
|
if (emptyA || emptyB)
|
||||||
|
return emptyA === emptyB ? a.i - b.i : (emptyA ? 1 : -1);
|
||||||
|
var cmp = (typeof a.k === 'number' && typeof b.k === 'number') ? a.k - b.k : cmpText(String(a.k), String(b.k));
|
||||||
|
return cmp !== 0 ? sign * cmp : a.i - b.i;
|
||||||
|
});
|
||||||
|
rows = [];
|
||||||
|
for (var j = 0; j < list.length; j++)
|
||||||
|
rows.push(list[j].tr);
|
||||||
|
}
|
||||||
|
applyOrder(table, info, rows);
|
||||||
|
refreshFilter(table);
|
||||||
|
setIndicator(table, th, sub, key, dir);
|
||||||
|
note(table, key === 'orig' ? T('restored') : T('changed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestSort(table, th, sub, key) {
|
||||||
|
if (table.__pwWait) {
|
||||||
|
clearInterval(table.__pwWait);
|
||||||
|
table.__pwWait = null;
|
||||||
|
}
|
||||||
|
var st = pingBusy();
|
||||||
|
if (!st.inflight && !st.left) {
|
||||||
|
doSort(table, th, sub, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var started = Date.now();
|
||||||
|
var idle = 0;
|
||||||
|
note(table, T('wait').replace('%d', st.left || st.inflight));
|
||||||
|
table.__pwWait = setInterval(function () {
|
||||||
|
st = pingBusy();
|
||||||
|
idle = st.inflight ? 0 : idle + 1;
|
||||||
|
if ((!st.inflight && !st.left) || idle >= 6) {
|
||||||
|
clearInterval(table.__pwWait);
|
||||||
|
table.__pwWait = null;
|
||||||
|
if (document.body.contains(table))
|
||||||
|
doSort(table, th, sub, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Date.now() - started > 120000) {
|
||||||
|
clearInterval(table.__pwWait);
|
||||||
|
table.__pwWait = null;
|
||||||
|
note(table, T('waitFail'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
note(table, T('wait').replace('%d', st.left || st.inflight));
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhance(table) {
|
||||||
|
var info = infoOf(table);
|
||||||
|
if (!info || table.__pwReady)
|
||||||
|
return;
|
||||||
|
var titles = table.querySelector('tr.cbi-section-table-titles');
|
||||||
|
var rows = dataRows(table);
|
||||||
|
if (!titles || !rows.length)
|
||||||
|
return;
|
||||||
|
var cols = colIndexMap(table);
|
||||||
|
var headers = cellsOf(titles);
|
||||||
|
table.__pwReady = true;
|
||||||
|
table.__pwOrig = [];
|
||||||
|
for (var r = 0; r < rows.length; r++)
|
||||||
|
table.__pwOrig.push(rowId(rows[r]));
|
||||||
|
for (var key in cols) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(cols, key))
|
||||||
|
continue;
|
||||||
|
var th = headers[cols[key]];
|
||||||
|
if (!th || th.classList.contains('cbi-section-actions'))
|
||||||
|
continue;
|
||||||
|
th.setAttribute('data-pw-sort', key);
|
||||||
|
th.title = T('hint');
|
||||||
|
var arrow = document.createElement('span');
|
||||||
|
arrow.className = 'pw-sort-arrow';
|
||||||
|
th.appendChild(arrow);
|
||||||
|
if (key !== 'name')
|
||||||
|
continue;
|
||||||
|
for (var s = 0; s < NAME_EXTRA.length; s++) {
|
||||||
|
var spec = NAME_EXTRA[s];
|
||||||
|
var sp = document.createElement('span');
|
||||||
|
sp.className = 'pw-sort-sub';
|
||||||
|
sp.setAttribute('data-pw-sort', spec[0]);
|
||||||
|
sp.textContent = T(spec[1]);
|
||||||
|
sp.title = T(spec[2]);
|
||||||
|
th.appendChild(sp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowVisible(el) {
|
||||||
|
var row = el && el.closest ? el.closest('tr') : null;
|
||||||
|
return !row || !row.classList.contains('pw-row-hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterDefs(rows, cols) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
label: T('type'),
|
||||||
|
multi: false,
|
||||||
|
get: function (tr) { return typeOf(tr, cols); }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'word',
|
||||||
|
label: T('fWord'),
|
||||||
|
multi: true,
|
||||||
|
minCount: 2,
|
||||||
|
max: 40,
|
||||||
|
get: function (tr) { return wordsOf(hiddenVal(rowId(tr), 'remarks')); }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchText(tr, cols) {
|
||||||
|
var id = rowId(tr);
|
||||||
|
return [
|
||||||
|
typeOf(tr, cols),
|
||||||
|
hiddenVal(id, 'remarks'),
|
||||||
|
hiddenVal(id, 'address') + ':' + hiddenVal(id, 'port')
|
||||||
|
].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function collect(table) {
|
||||||
|
var rows = dataRows(table);
|
||||||
|
var cols = colIndexMap(table);
|
||||||
|
var defs = rows.length >= 2 ? filterDefs(rows, cols) : [];
|
||||||
|
var shown = [];
|
||||||
|
for (var d = 0; d < defs.length; d++) {
|
||||||
|
var def = defs[d];
|
||||||
|
var counts = {};
|
||||||
|
var labels = {};
|
||||||
|
var vals = [];
|
||||||
|
for (var r = 0; r < rows.length; r++) {
|
||||||
|
var raw = def.get(rows[r]);
|
||||||
|
var list = def.multi ? (raw || []) : [raw == null ? '' : String(raw)];
|
||||||
|
var keys = [];
|
||||||
|
for (var n = 0; n < list.length; n++) {
|
||||||
|
var item = list[n];
|
||||||
|
var k = def.multi ? String(item).toLowerCase() : String(item);
|
||||||
|
counts[k] = (counts[k] || 0) + 1;
|
||||||
|
if (!labels[k])
|
||||||
|
labels[k] = {};
|
||||||
|
labels[k][item] = (labels[k][item] || 0) + 1;
|
||||||
|
keys.push(k);
|
||||||
|
}
|
||||||
|
vals.push(keys);
|
||||||
|
}
|
||||||
|
var opts = [];
|
||||||
|
for (var k in counts) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(counts, k))
|
||||||
|
continue;
|
||||||
|
if (def.multi && (counts[k] < (def.minCount || 1) || counts[k] >= rows.length))
|
||||||
|
continue;
|
||||||
|
var best = '';
|
||||||
|
var bestN = -1;
|
||||||
|
for (var label in labels[k]) {
|
||||||
|
if (labels[k][label] > bestN) {
|
||||||
|
bestN = labels[k][label];
|
||||||
|
best = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opts.push({ v: k, label: best === '' ? T('empty') : best, n: counts[k] });
|
||||||
|
}
|
||||||
|
opts.sort(function (a, b) {
|
||||||
|
if (def.multi && a.n !== b.n)
|
||||||
|
return b.n - a.n;
|
||||||
|
return cmpText(a.label, b.label);
|
||||||
|
});
|
||||||
|
if (def.max && opts.length > def.max)
|
||||||
|
opts = opts.slice(0, def.max);
|
||||||
|
var enough = def.multi ? opts.length >= 1 : opts.length >= 2;
|
||||||
|
if (!enough)
|
||||||
|
continue;
|
||||||
|
def.vals = vals;
|
||||||
|
def.opts = opts;
|
||||||
|
shown.push(def);
|
||||||
|
}
|
||||||
|
var search = rows.length >= 2;
|
||||||
|
var sig = String(rows.length) + (search ? 's' : '');
|
||||||
|
for (var i = 0; i < shown.length; i++) {
|
||||||
|
sig += '|' + shown[i].key + '=';
|
||||||
|
for (var o = 0; o < shown[i].opts.length; o++)
|
||||||
|
sig += shown[i].opts[o].v + ':' + shown[i].opts[o].n + ',';
|
||||||
|
}
|
||||||
|
return { rows: rows, cols: cols, defs: shown, search: search, sig: sig };
|
||||||
|
}
|
||||||
|
|
||||||
|
function barId(table) {
|
||||||
|
return 'pw-filter-' + table.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBar(table, collected) {
|
||||||
|
var st = table.__pwF || (table.__pwF = { sel: {}, q: '' });
|
||||||
|
var id = barId(table);
|
||||||
|
var bar = document.getElementById(id);
|
||||||
|
if (!bar) {
|
||||||
|
bar = document.createElement('div');
|
||||||
|
bar.id = id;
|
||||||
|
bar.className = 'pw-filter';
|
||||||
|
bar.title = T('filterHint');
|
||||||
|
table.parentNode.insertBefore(bar, table);
|
||||||
|
bar.__pwTable = table;
|
||||||
|
}
|
||||||
|
while (bar.firstChild)
|
||||||
|
bar.removeChild(bar.firstChild);
|
||||||
|
var head = document.createElement('span');
|
||||||
|
head.textContent = T('filter');
|
||||||
|
bar.appendChild(head);
|
||||||
|
var keep = {};
|
||||||
|
for (var d = 0; d < collected.defs.length; d++) {
|
||||||
|
var def = collected.defs[d];
|
||||||
|
var lab = document.createElement('label');
|
||||||
|
var sel = document.createElement('select');
|
||||||
|
lab.appendChild(document.createTextNode(def.label));
|
||||||
|
sel.className = 'cbi-input-select';
|
||||||
|
sel.setAttribute('data-pw-filter', def.key);
|
||||||
|
var all = document.createElement('option');
|
||||||
|
all.value = '';
|
||||||
|
all.textContent = T('all') + ' (' + collected.rows.length + ')';
|
||||||
|
sel.appendChild(all);
|
||||||
|
for (var o = 0; o < def.opts.length; o++) {
|
||||||
|
var op = document.createElement('option');
|
||||||
|
op.value = 'v:' + def.opts[o].v;
|
||||||
|
op.textContent = def.opts[o].label + ' (' + def.opts[o].n + ')';
|
||||||
|
sel.appendChild(op);
|
||||||
|
}
|
||||||
|
var cur = st.sel[def.key] || '';
|
||||||
|
var known = false;
|
||||||
|
if (cur) {
|
||||||
|
for (var c = 0; c < def.opts.length; c++) {
|
||||||
|
if (('v:' + def.opts[c].v) === cur) {
|
||||||
|
known = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!known)
|
||||||
|
cur = '';
|
||||||
|
sel.value = cur;
|
||||||
|
if (cur)
|
||||||
|
keep[def.key] = cur;
|
||||||
|
lab.appendChild(sel);
|
||||||
|
bar.appendChild(lab);
|
||||||
|
}
|
||||||
|
st.sel = keep;
|
||||||
|
if (collected.search) {
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.type = 'search';
|
||||||
|
input.className = 'cbi-input-text';
|
||||||
|
input.placeholder = T('search');
|
||||||
|
input.title = T('searchHint');
|
||||||
|
input.autocomplete = 'off';
|
||||||
|
input.setAttribute('data-pw-search', '1');
|
||||||
|
input.value = st.q;
|
||||||
|
bar.appendChild(input);
|
||||||
|
} else {
|
||||||
|
st.q = '';
|
||||||
|
}
|
||||||
|
var count = document.createElement('span');
|
||||||
|
count.className = 'pw-filter-count';
|
||||||
|
bar.appendChild(count);
|
||||||
|
var reset = document.createElement('button');
|
||||||
|
reset.type = 'button';
|
||||||
|
reset.className = 'btn cbi-button';
|
||||||
|
reset.setAttribute('data-pw-reset', '1');
|
||||||
|
reset.textContent = T('reset');
|
||||||
|
reset.title = T('resetHint');
|
||||||
|
bar.appendChild(reset);
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listHas(list, value) {
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
if (list[i] === value)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilter(table, collected, bar) {
|
||||||
|
var st = table.__pwF || { sel: {}, q: '' };
|
||||||
|
var q = String(st.q || '').toLowerCase().trim();
|
||||||
|
var active = !!q;
|
||||||
|
var shown = 0;
|
||||||
|
var d;
|
||||||
|
for (d = 0; d < collected.defs.length; d++) {
|
||||||
|
if (st.sel[collected.defs[d].key])
|
||||||
|
active = true;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < collected.rows.length; i++) {
|
||||||
|
var tr = collected.rows[i];
|
||||||
|
var ok = true;
|
||||||
|
for (d = 0; d < collected.defs.length; d++) {
|
||||||
|
var sel = st.sel[collected.defs[d].key];
|
||||||
|
if (ok && sel && !listHas(collected.defs[d].vals[i], sel.slice(2)))
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
if (ok && q && searchText(tr, collected.cols).toLowerCase().indexOf(q) < 0)
|
||||||
|
ok = false;
|
||||||
|
if (ok !== !tr.classList.contains('pw-row-hidden'))
|
||||||
|
tr.classList.toggle('pw-row-hidden', !ok);
|
||||||
|
if (ok) {
|
||||||
|
shown++;
|
||||||
|
var want = 'cbi-rowstyle-' + ((shown - 1) % 2 + 1);
|
||||||
|
var other = want === 'cbi-rowstyle-1' ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1';
|
||||||
|
if (!tr.classList.contains(want))
|
||||||
|
tr.classList.add(want);
|
||||||
|
if (tr.classList.contains(other))
|
||||||
|
tr.classList.remove(other);
|
||||||
|
} else {
|
||||||
|
var cb = tr.querySelector('input.nodes_select');
|
||||||
|
if (cb && cb.checked) {
|
||||||
|
cb.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof window.update_select_state === 'function')
|
||||||
|
window.update_select_state();
|
||||||
|
var count = bar.querySelector('.pw-filter-count');
|
||||||
|
var txt = T('shown').replace('%s', shown).replace('%t', collected.rows.length);
|
||||||
|
if (count && count.textContent !== txt)
|
||||||
|
count.textContent = txt;
|
||||||
|
if (bar.classList.contains('pw-filter-on') !== active)
|
||||||
|
bar.classList.toggle('pw-filter-on', active);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeBar(table) {
|
||||||
|
var bar = document.getElementById(barId(table));
|
||||||
|
if (bar && bar.parentNode)
|
||||||
|
bar.parentNode.removeChild(bar);
|
||||||
|
var rows = dataRows(table);
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
if (rows[i].classList.contains('pw-row-hidden'))
|
||||||
|
rows[i].classList.remove('pw-row-hidden');
|
||||||
|
}
|
||||||
|
table.__pwSig = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshFilter(table) {
|
||||||
|
if (!infoOf(table))
|
||||||
|
return;
|
||||||
|
var collected = collect(table);
|
||||||
|
if (!collected.defs.length && !collected.search) {
|
||||||
|
if (table.__pwSig)
|
||||||
|
removeBar(table);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var bar = document.getElementById(barId(table));
|
||||||
|
if (!bar || collected.sig !== table.__pwSig) {
|
||||||
|
bar = buildBar(table, collected);
|
||||||
|
table.__pwSig = collected.sig;
|
||||||
|
}
|
||||||
|
applyFilter(table, collected, bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
function barTable(el) {
|
||||||
|
var bar = el && el.closest ? el.closest('.pw-filter') : null;
|
||||||
|
return bar && bar.__pwTable ? bar.__pwTable : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventTarget(ev) {
|
||||||
|
var t = ev.target;
|
||||||
|
if (t && t.nodeType !== 1)
|
||||||
|
t = t.parentNode;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind(root) {
|
||||||
|
root.addEventListener('click', function (ev) {
|
||||||
|
var t = eventTarget(ev);
|
||||||
|
if (t && t.getAttribute && t.getAttribute('data-pw-reset')) {
|
||||||
|
var resetTable = barTable(t);
|
||||||
|
if (!resetTable)
|
||||||
|
return;
|
||||||
|
ev.preventDefault();
|
||||||
|
resetTable.__pwF = { sel: {}, q: '' };
|
||||||
|
resetTable.__pwSig = null;
|
||||||
|
refreshFilter(resetTable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var el = t && t.closest ? t.closest('[data-pw-sort]') : null;
|
||||||
|
if (!el || !root.contains(el))
|
||||||
|
return;
|
||||||
|
if (t.closest && t.closest('input, a, button, select'))
|
||||||
|
return;
|
||||||
|
var th = el.closest('th');
|
||||||
|
var table = el.closest('table');
|
||||||
|
if (!th || !table || !infoOf(table))
|
||||||
|
return;
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
requestSort(table, th, el === th ? null : el, el.getAttribute('data-pw-sort'));
|
||||||
|
});
|
||||||
|
root.addEventListener('change', function (ev) {
|
||||||
|
var t = eventTarget(ev);
|
||||||
|
if (!t || !t.getAttribute || !t.getAttribute('data-pw-filter'))
|
||||||
|
return;
|
||||||
|
var table = barTable(t);
|
||||||
|
if (!table)
|
||||||
|
return;
|
||||||
|
(table.__pwF = table.__pwF || { sel: {}, q: '' }).sel[t.getAttribute('data-pw-filter')] = t.value;
|
||||||
|
refreshFilter(table);
|
||||||
|
});
|
||||||
|
root.addEventListener('input', function (ev) {
|
||||||
|
var t = eventTarget(ev);
|
||||||
|
if (!t || !t.getAttribute || !t.getAttribute('data-pw-search'))
|
||||||
|
return;
|
||||||
|
var table = barTable(t);
|
||||||
|
if (!table)
|
||||||
|
return;
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
searchTimer = setTimeout(function () {
|
||||||
|
(table.__pwF = table.__pwF || { sel: {}, q: '' }).q = t.value;
|
||||||
|
refreshFilter(table);
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
root.addEventListener('keydown', function (ev) {
|
||||||
|
var t = eventTarget(ev);
|
||||||
|
if (ev.key === 'Enter' && t && t.getAttribute && t.getAttribute('data-pw-search'))
|
||||||
|
ev.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var box = document.getElementById('pw-nodesort-i18n');
|
||||||
|
var root = document.getElementById('node_list');
|
||||||
|
if (!box || !root)
|
||||||
|
return;
|
||||||
|
state.app = box.getAttribute('data-app') || 'passwall';
|
||||||
|
state.text = readText(box);
|
||||||
|
state.root = root;
|
||||||
|
prepareCollator();
|
||||||
|
if (!state.bound) {
|
||||||
|
bind(root);
|
||||||
|
state.bound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhanceAll() {
|
||||||
|
if (!state.root)
|
||||||
|
return;
|
||||||
|
var tables = state.root.querySelectorAll('table.cbi-section-table');
|
||||||
|
for (var i = 0; i < tables.length; i++) {
|
||||||
|
try {
|
||||||
|
enhance(tables[i]);
|
||||||
|
refreshFilter(tables[i]);
|
||||||
|
} catch (e) {
|
||||||
|
if (window.console)
|
||||||
|
console.warn('passwall node list sort:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pwNodeSort = {
|
||||||
|
init: init,
|
||||||
|
enhanceAll: enhanceAll,
|
||||||
|
rowVisible: rowVisible
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -579,11 +579,15 @@ function gen_config_server(node)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if node.protocol == "vmess" or node.protocol == "vless" then
|
if node.protocol == "vless" then
|
||||||
|
settings = {
|
||||||
|
users = users,
|
||||||
|
decryption = (node.decryption and node.decryption ~= "") and node.decryption or "none"
|
||||||
|
}
|
||||||
|
elseif node.protocol == "vmess" then
|
||||||
if users then
|
if users then
|
||||||
settings = {
|
settings = {
|
||||||
users = users,
|
users = users
|
||||||
decryption = (node.protocol == "vless") and ((node.decryption and node.decryption ~= "") and node.decryption or "none") or nil
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
elseif node.protocol == "socks" then
|
elseif node.protocol == "socks" then
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ if node then
|
|||||||
end
|
end
|
||||||
-%>
|
-%>
|
||||||
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js?v=1.15.7"></script>
|
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js?v=1.15.7"></script>
|
||||||
|
<script src="<%=resource%>/view/<%=appname%>/node_list_sort.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
table th, .table .th {
|
table th, .table .th {
|
||||||
@@ -385,6 +386,89 @@ table td, .table .td {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* node list: click-to-sort headers and the per-group filter bar */
|
||||||
|
th[data-pw-sort] {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-pw-sort]:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-sort-arrow {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 1em;
|
||||||
|
margin-left: 0.25em;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-sort-sub {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 85%;
|
||||||
|
opacity: 0.85;
|
||||||
|
margin-left: 0.6em;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-sort-sub.pw-sort-active {
|
||||||
|
opacity: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-sort-note {
|
||||||
|
margin: 0.3em 0;
|
||||||
|
padding: 0.3em 0.6em;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(251, 154, 5, 0.15);
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.pw-row-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-filter {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3em 0.8em;
|
||||||
|
margin: 0.4em 0;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-filter label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-filter select,
|
||||||
|
.pw-filter input[type="search"],
|
||||||
|
.pw-filter .cbi-button {
|
||||||
|
width: auto !important;
|
||||||
|
min-width: 6em;
|
||||||
|
max-width: 22em;
|
||||||
|
height: auto;
|
||||||
|
padding: 0.15em 0.3em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-filter .pw-filter-count {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-filter.pw-filter-on .pw-filter-count {
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
<% if is_js_luci then -%>
|
<% if is_js_luci then -%>
|
||||||
var cbi_t = [];
|
var cbi_t = [];
|
||||||
@@ -540,15 +624,29 @@ table td, .table .td {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function node_row_visible(el) {
|
||||||
|
if (window.pwNodeSort && typeof pwNodeSort.rowVisible === "function")
|
||||||
|
return pwNodeSort.rowVisible(el);
|
||||||
|
var row = el && el.closest ? el.closest("tr") : null;
|
||||||
|
return !row || !row.classList.contains("pw-row-hidden");
|
||||||
|
}
|
||||||
|
|
||||||
function set_select_all_state(sectionChecked) {
|
function set_select_all_state(sectionChecked) {
|
||||||
var visibleContainer = document.querySelector('#cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display: block"]');
|
var visibleContainer = document.querySelector('#cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display: block"]');
|
||||||
if (!visibleContainer) return;
|
if (!visibleContainer) return;
|
||||||
var nodes = visibleContainer.getElementsByClassName("nodes_select");
|
var nodes = visibleContainer.getElementsByClassName("nodes_select");
|
||||||
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
||||||
var selectAllBtn = document.getElementById("select_all_btn");
|
var selectAllBtn = document.getElementById("select_all_btn");
|
||||||
|
var visibleCount = 0;
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
if (!node_row_visible(nodes[i])) {
|
||||||
|
nodes[i].checked = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
nodes[i].checked = sectionChecked;
|
nodes[i].checked = sectionChecked;
|
||||||
|
visibleCount++;
|
||||||
}
|
}
|
||||||
|
sectionChecked = sectionChecked && visibleCount > 0;
|
||||||
if (selectAllChk) {
|
if (selectAllChk) {
|
||||||
selectAllChk.checked = sectionChecked;
|
selectAllChk.checked = sectionChecked;
|
||||||
selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
||||||
@@ -576,10 +674,13 @@ table td, .table .td {
|
|||||||
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
||||||
var selectAllBtn = document.getElementById("select_all_btn");
|
var selectAllBtn = document.getElementById("select_all_btn");
|
||||||
var checkedCount = 0;
|
var checkedCount = 0;
|
||||||
|
var visibleCount = 0;
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
if (!node_row_visible(nodes[i])) continue;
|
||||||
|
visibleCount++;
|
||||||
if (nodes[i].checked) checkedCount++;
|
if (nodes[i].checked) checkedCount++;
|
||||||
}
|
}
|
||||||
var allChecked = checkedCount === nodes.length;
|
var allChecked = visibleCount > 0 && checkedCount === visibleCount;
|
||||||
var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
||||||
var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)";
|
var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)";
|
||||||
|
|
||||||
@@ -602,7 +703,7 @@ table td, .table .td {
|
|||||||
var doms = visibleContainer.getElementsByClassName("nodes_select");
|
var doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||||
if (doms && doms.length > 0) {
|
if (doms && doms.length > 0) {
|
||||||
for (var i = 0 ; i < doms.length; i++) {
|
for (var i = 0 ; i < doms.length; i++) {
|
||||||
if (doms[i].checked) {
|
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||||
ids.push(doms[i].getAttribute("cbid"))
|
ids.push(doms[i].getAttribute("cbid"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1163,7 +1264,7 @@ table td, .table .td {
|
|||||||
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||||
if (doms && doms.length > 0) {
|
if (doms && doms.length > 0) {
|
||||||
for (let i = 0 ; i < doms.length; i++) {
|
for (let i = 0 ; i < doms.length; i++) {
|
||||||
if (doms[i].checked) {
|
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||||
ids.push(doms[i].getAttribute("cbid"))
|
ids.push(doms[i].getAttribute("cbid"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1195,7 +1296,7 @@ table td, .table .td {
|
|||||||
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||||
if (doms && doms.length > 0) {
|
if (doms && doms.length > 0) {
|
||||||
for (let i = 0 ; i < doms.length; i++) {
|
for (let i = 0 ; i < doms.length; i++) {
|
||||||
if (doms[i].checked) {
|
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||||
ids.push(doms[i].getAttribute("cbid"))
|
ids.push(doms[i].getAttribute("cbid"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1307,8 +1408,33 @@ table td, .table .td {
|
|||||||
<<%=section_tag%> class="cbi-section" id="node_list">
|
<<%=section_tag%> class="cbi-section" id="node_list">
|
||||||
</<%=section_tag%>>
|
</<%=section_tag%>>
|
||||||
|
|
||||||
|
<div id="pw-nodesort-i18n" data-app="<%=appname%>" style="display:none">
|
||||||
|
<span data-k="hint"><%:Click to sort; click again to reverse. Press Save & Apply to keep the order.%></span>
|
||||||
|
<span data-k="typeHint"><%:Sort by type or protocol, then by name%></span>
|
||||||
|
<span data-k="addrHint"><%:Sort by server address and port%></span>
|
||||||
|
<span data-k="origHint"><%:Restore the saved order%></span>
|
||||||
|
<span data-k="changed"><%:The order changed on this page only. Press Save & Apply to store it on the router.%></span>
|
||||||
|
<span data-k="restored"><%:Saved order restored.%></span>
|
||||||
|
<span data-k="wait"><%:Automatic ping is still running (%d left). Sorting will run when it finishes.%></span>
|
||||||
|
<span data-k="waitFail"><%:Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again.%></span>
|
||||||
|
<span data-k="filter"><%:Filter:%></span>
|
||||||
|
<span data-k="all"><%:All%></span>
|
||||||
|
<span data-k="empty"><%:(empty)%></span>
|
||||||
|
<span data-k="type"><%:Type%></span>
|
||||||
|
<span data-k="addr"><%:Address%></span>
|
||||||
|
<span data-k="fWord"><%:Word in remarks%></span>
|
||||||
|
<span data-k="search"><%:search...%></span>
|
||||||
|
<span data-k="searchHint"><%:Show only rows that contain this text%></span>
|
||||||
|
<span data-k="shown"><%:Shown %s of %t%></span>
|
||||||
|
<span data-k="reset"><%:Reset%></span>
|
||||||
|
<span data-k="resetHint"><%:Show all rows%></span>
|
||||||
|
<span data-k="filterHint"><%:The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows.%></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
//init logic
|
//init logic
|
||||||
|
if (window.pwNodeSort)
|
||||||
|
pwNodeSort.init();
|
||||||
|
|
||||||
function get_remarks_name(o) {
|
function get_remarks_name(o) {
|
||||||
let str = "";
|
let str = "";
|
||||||
@@ -1511,6 +1637,8 @@ table td, .table .td {
|
|||||||
get_now_use_node();
|
get_now_use_node();
|
||||||
|
|
||||||
pingAllNodes();
|
pingAllNodes();
|
||||||
|
if (window.pwNodeSort)
|
||||||
|
pwNodeSort.enhanceAll();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2006,3 +2006,57 @@ msgstr "WARP 设备私钥"
|
|||||||
|
|
||||||
msgid "WARP Endpoint Public Key"
|
msgid "WARP Endpoint Public Key"
|
||||||
msgstr "WARP 服务端公钥"
|
msgstr "WARP 服务端公钥"
|
||||||
|
|
||||||
|
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
|
||||||
|
msgstr "点击表头排序,再次点击切换方向。按保存并应用可保留顺序。"
|
||||||
|
|
||||||
|
msgid "Sort by type or protocol, then by name"
|
||||||
|
msgstr "先按类型或协议排序,再按名称排序"
|
||||||
|
|
||||||
|
msgid "Sort by server address and port"
|
||||||
|
msgstr "按服务器地址和端口排序"
|
||||||
|
|
||||||
|
msgid "Restore the saved order"
|
||||||
|
msgstr "恢复已保存的顺序"
|
||||||
|
|
||||||
|
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
|
||||||
|
msgstr "顺序仅在本页改变。按保存并应用才会写入路由器。"
|
||||||
|
|
||||||
|
msgid "Saved order restored."
|
||||||
|
msgstr "已恢复保存的顺序。"
|
||||||
|
|
||||||
|
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
|
||||||
|
msgstr "自动探测仍在进行(剩余 %d)。结束后会立即排序。"
|
||||||
|
|
||||||
|
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
|
||||||
|
msgstr "自动探测在 2 分钟内未结束,已取消排序。请刷新页面后重试。"
|
||||||
|
|
||||||
|
msgid "Filter:"
|
||||||
|
msgstr "筛选:"
|
||||||
|
|
||||||
|
msgid "(empty)"
|
||||||
|
msgstr "(空)"
|
||||||
|
|
||||||
|
msgid "Word in remarks"
|
||||||
|
msgstr "备注关键词"
|
||||||
|
|
||||||
|
msgid "Address"
|
||||||
|
msgstr "地址"
|
||||||
|
|
||||||
|
msgid "search..."
|
||||||
|
msgstr "搜索..."
|
||||||
|
|
||||||
|
msgid "Show only rows that contain this text"
|
||||||
|
msgstr "只显示包含此文本的行"
|
||||||
|
|
||||||
|
msgid "Shown %s of %t"
|
||||||
|
msgstr "显示 %s / %t"
|
||||||
|
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr "重置"
|
||||||
|
|
||||||
|
msgid "Show all rows"
|
||||||
|
msgstr "显示全部行"
|
||||||
|
|
||||||
|
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
|
||||||
|
msgstr "筛选只在本页隐藏行,不会改动路由器上的设置。全选会跳过隐藏行。"
|
||||||
@@ -0,0 +1,775 @@
|
|||||||
|
/*
|
||||||
|
* Click-to-sort headers and a per-group filter bar for the node list.
|
||||||
|
*
|
||||||
|
* Sort reorders rows inside one group table and calls set_order(), the same
|
||||||
|
* path as drag-and-drop and To Top. Nothing is written until Save & Apply.
|
||||||
|
*
|
||||||
|
* A filter only toggles the pw-row-hidden class. Rows stay in the form, so
|
||||||
|
* Save & Apply still posts every node and the full group.order value.
|
||||||
|
*
|
||||||
|
* Translated strings are read from #pw2-nodesort-i18n (the node list template).
|
||||||
|
* The template calls init() once, then enhanceAll() after loadNodeList()
|
||||||
|
* has built the group tables.
|
||||||
|
*
|
||||||
|
* auto_detection_time is a top-level let in the template, not a window
|
||||||
|
* property. It is read by name so rows are not moved while automatic Ping
|
||||||
|
* or TCPing is still writing results by row position.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var COLUMNS = {
|
||||||
|
'pw-remark': 'name',
|
||||||
|
'pw-ping': 'ping',
|
||||||
|
'pw-tcping': 'tcping',
|
||||||
|
'pw-urltest': 'urltest'
|
||||||
|
};
|
||||||
|
/* sub controls drawn inside the Remarks header: [sort key, label key or symbol, title key] */
|
||||||
|
var NAME_EXTRA = [
|
||||||
|
['type', 'type', 'typeHint'],
|
||||||
|
['addr', 'addr', 'addrHint'],
|
||||||
|
['orig', '\u21BA', 'origHint']
|
||||||
|
];
|
||||||
|
|
||||||
|
var state = {
|
||||||
|
app: 'passwall2',
|
||||||
|
root: null,
|
||||||
|
text: {},
|
||||||
|
bound: false
|
||||||
|
};
|
||||||
|
var searchTimer = null;
|
||||||
|
|
||||||
|
function T(key) {
|
||||||
|
var value = state.text[key];
|
||||||
|
return (value == null || value === '') ? key : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readText(box) {
|
||||||
|
var text = {};
|
||||||
|
var spans = box.querySelectorAll('[data-k]');
|
||||||
|
for (var i = 0; i < spans.length; i++)
|
||||||
|
text[spans[i].getAttribute('data-k')] = spans[i].textContent;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
var collator = null;
|
||||||
|
function prepareCollator() {
|
||||||
|
var lang = document.documentElement.lang || (navigator.language || '');
|
||||||
|
try {
|
||||||
|
collator = (window.Intl && Intl.Collator) ? new Intl.Collator(lang || undefined, { numeric: true, sensitivity: 'base' }) : null;
|
||||||
|
} catch (e) {
|
||||||
|
collator = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpText(a, b) {
|
||||||
|
if (collator)
|
||||||
|
return collator.compare(a, b);
|
||||||
|
return a < b ? -1 : (a > b ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "123 ms", "12.5 ms", "1.2 s" -> milliseconds. Timeout, Error, ---, Test stay null. */
|
||||||
|
function parseLatency(text) {
|
||||||
|
var t = String(text || '').replace(/\s+/g, ' ').trim();
|
||||||
|
var m = /^(\d+(?:[.,]\d+)?)\s*(ms|s)?$/i.exec(t);
|
||||||
|
if (!m)
|
||||||
|
return null;
|
||||||
|
var v = parseFloat(m[1].replace(',', '.'));
|
||||||
|
if (isNaN(v))
|
||||||
|
return null;
|
||||||
|
if (m[2] && /^s$/i.test(m[2]))
|
||||||
|
v *= 1000;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hiddenVal(id, opt) {
|
||||||
|
var el = document.getElementById('cbid.' + state.app + '.' + id + '.' + opt);
|
||||||
|
return el && el.value != null ? String(el.value).trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cellsOf(tr) {
|
||||||
|
var out = [];
|
||||||
|
for (var i = 0; i < tr.children.length; i++) {
|
||||||
|
var tag = tr.children[i].tagName;
|
||||||
|
if (tag === 'TD' || tag === 'TH')
|
||||||
|
out.push(tr.children[i]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wordSplit;
|
||||||
|
try {
|
||||||
|
wordSplit = new RegExp('[^\\p{L}\\p{N}]+', 'u');
|
||||||
|
} catch (e) {
|
||||||
|
wordSplit = /[\s.,;:!?|\/\\()\[\]{}<>"'`~@#$%^&*=+_-]+/;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wordsOf(text) {
|
||||||
|
var seen = {};
|
||||||
|
var out = [];
|
||||||
|
var parts = String(text || '').split(wordSplit);
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
var w = parts[i];
|
||||||
|
if (w.length < 2 || /^\d+$/.test(w))
|
||||||
|
continue;
|
||||||
|
var k = w.toLowerCase();
|
||||||
|
if (!seen[k]) {
|
||||||
|
seen[k] = 1;
|
||||||
|
out.push(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function infoOf(table) {
|
||||||
|
var prefix = 'cbi-' + state.app + '-nodes-';
|
||||||
|
var id = table && table.id ? table.id : '';
|
||||||
|
if (id.indexOf(prefix) !== 0 || id.slice(-6) !== '-table')
|
||||||
|
return null;
|
||||||
|
return { group: id.slice(prefix.length, -6) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowId(tr) {
|
||||||
|
var prefix = 'cbi-' + state.app + '-';
|
||||||
|
return tr.id.indexOf(prefix) === 0 ? tr.id.substring(prefix.length) : tr.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bodyOf(table) {
|
||||||
|
return table.querySelector('tbody') || table;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataRows(table) {
|
||||||
|
var body = bodyOf(table);
|
||||||
|
var out = [];
|
||||||
|
for (var i = 0; i < body.children.length; i++) {
|
||||||
|
var tr = body.children[i];
|
||||||
|
if (tr.tagName === 'TR' && tr.classList.contains('cbi-section-table-row') && !tr.classList.contains('placeholder'))
|
||||||
|
out.push(tr);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function colIndexMap(table) {
|
||||||
|
var rows = dataRows(table);
|
||||||
|
var map = {};
|
||||||
|
if (!rows.length)
|
||||||
|
return map;
|
||||||
|
var cells = cellsOf(rows[0]);
|
||||||
|
for (var i = 0; i < cells.length; i++) {
|
||||||
|
for (var cls in COLUMNS) {
|
||||||
|
if (cells[i].classList.contains(cls))
|
||||||
|
map[COLUMNS[cls]] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeOf(tr, cols) {
|
||||||
|
var cell = cellsOf(tr)[cols.name];
|
||||||
|
var text = cell ? cell.textContent.replace(/\s+/g, ' ').trim() : '';
|
||||||
|
var i = text.indexOf('\uFF1A');
|
||||||
|
if (i < 0)
|
||||||
|
i = text.indexOf(':');
|
||||||
|
return (i < 0 ? '' : text.slice(0, i)).replace(/\s+Realm$/, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortKey(tr, key, cols) {
|
||||||
|
var id = rowId(tr);
|
||||||
|
var cell;
|
||||||
|
if (key === 'name')
|
||||||
|
return hiddenVal(id, 'remarks') || ((cellsOf(tr)[cols.name] || {}).textContent || '').trim();
|
||||||
|
if (key === 'type')
|
||||||
|
return typeOf(tr, cols) + '\u0000' + hiddenVal(id, 'remarks');
|
||||||
|
if (key === 'addr') {
|
||||||
|
var address = hiddenVal(id, 'address');
|
||||||
|
if (!address)
|
||||||
|
return null;
|
||||||
|
return address + '\u0000' + ('00000' + hiddenVal(id, 'port')).slice(-5);
|
||||||
|
}
|
||||||
|
cell = cellsOf(tr)[cols[key]];
|
||||||
|
return cell ? parseLatency(cell.textContent) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectionMode() {
|
||||||
|
try {
|
||||||
|
if (typeof auto_detection_time === 'undefined')
|
||||||
|
return null;
|
||||||
|
return auto_detection_time;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* { inflight: requests still running, left: cells still waiting for a result } */
|
||||||
|
function pingBusy() {
|
||||||
|
var mode = detectionMode();
|
||||||
|
var inflight = 0;
|
||||||
|
var left = 0;
|
||||||
|
if (mode !== 'icmp' && mode !== 'tcping')
|
||||||
|
return { inflight: 0, left: 0 };
|
||||||
|
var list = window.ajax && window.ajax.list;
|
||||||
|
if (list && list.length) {
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
if (list[i].readyState > 0 && list[i].readyState < 4)
|
||||||
|
inflight++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!state.root)
|
||||||
|
return { inflight: inflight, left: 0 };
|
||||||
|
var spans = state.root.getElementsByClassName(mode === 'tcping' ? 'tcping_value' : 'ping_value');
|
||||||
|
for (var s = 0; s < spans.length; s++) {
|
||||||
|
var id = spans[s].getAttribute('cbiid');
|
||||||
|
if (!id || spans[s].textContent.trim() !== '---')
|
||||||
|
continue;
|
||||||
|
var full = null;
|
||||||
|
if (typeof window.get_address_full === 'function')
|
||||||
|
full = window.get_address_full(id);
|
||||||
|
if (!full)
|
||||||
|
full = { address: hiddenVal(id, 'address'), port: hiddenVal(id, 'port') };
|
||||||
|
if (full.address && (mode === 'icmp' || full.port))
|
||||||
|
left++;
|
||||||
|
}
|
||||||
|
return { inflight: inflight, left: left };
|
||||||
|
}
|
||||||
|
|
||||||
|
function note(table, text) {
|
||||||
|
var id = 'pw-note-' + table.id;
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (!text) {
|
||||||
|
if (el && el.parentNode)
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement('div');
|
||||||
|
el.id = id;
|
||||||
|
el.className = 'pw-sort-note';
|
||||||
|
table.parentNode.insertBefore(el, table);
|
||||||
|
}
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setIndicator(table, th, sub, key, dir) {
|
||||||
|
var arrows = table.querySelectorAll('.pw-sort-arrow');
|
||||||
|
var subs = table.querySelectorAll('.pw-sort-sub');
|
||||||
|
var headers = table.querySelectorAll('th[data-pw-sort]');
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < arrows.length; i++)
|
||||||
|
arrows[i].textContent = '';
|
||||||
|
for (i = 0; i < subs.length; i++)
|
||||||
|
subs[i].classList.remove('pw-sort-active');
|
||||||
|
for (i = 0; i < headers.length; i++) {
|
||||||
|
if (headers[i] !== th) {
|
||||||
|
delete headers[i].dataset.pwDir;
|
||||||
|
delete headers[i].dataset.pwKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var arrow = th.querySelector('.pw-sort-arrow');
|
||||||
|
if (arrow && key !== 'orig' && !sub)
|
||||||
|
arrow.textContent = dir === 'desc' ? '\u25BC' : '\u25B2';
|
||||||
|
if (sub && key !== 'orig')
|
||||||
|
sub.classList.add('pw-sort-active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOrder(table, info, rows) {
|
||||||
|
var body = bodyOf(table);
|
||||||
|
var frag = document.createDocumentFragment();
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
rows[i].classList.remove('cbi-rowstyle-1', 'cbi-rowstyle-2');
|
||||||
|
rows[i].classList.add('cbi-rowstyle-' + (i % 2 + 1));
|
||||||
|
frag.appendChild(rows[i]);
|
||||||
|
}
|
||||||
|
body.appendChild(frag);
|
||||||
|
if (typeof window.set_order === 'function')
|
||||||
|
window.set_order(info.group);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSort(table, th, sub, key) {
|
||||||
|
var info = infoOf(table);
|
||||||
|
var rows = dataRows(table);
|
||||||
|
var dir = 'asc';
|
||||||
|
if (!info || rows.length < 2)
|
||||||
|
return;
|
||||||
|
if (key === 'orig') {
|
||||||
|
var pos = {};
|
||||||
|
var orig = table.__pwOrig || [];
|
||||||
|
for (var n = 0; n < orig.length; n++)
|
||||||
|
pos[orig[n]] = n;
|
||||||
|
rows.sort(function (a, b) {
|
||||||
|
var pa = pos[rowId(a)];
|
||||||
|
var pb = pos[rowId(b)];
|
||||||
|
return (pa == null ? 1e9 : pa) - (pb == null ? 1e9 : pb);
|
||||||
|
});
|
||||||
|
delete th.dataset.pwDir;
|
||||||
|
delete th.dataset.pwKey;
|
||||||
|
} else {
|
||||||
|
dir = (th.dataset.pwKey === key && th.dataset.pwDir === 'asc') ? 'desc' : 'asc';
|
||||||
|
th.dataset.pwKey = key;
|
||||||
|
th.dataset.pwDir = dir;
|
||||||
|
var cols = colIndexMap(table);
|
||||||
|
var sign = dir === 'desc' ? -1 : 1;
|
||||||
|
var list = [];
|
||||||
|
for (var i = 0; i < rows.length; i++)
|
||||||
|
list.push({ tr: rows[i], i: i, k: sortKey(rows[i], key, cols) });
|
||||||
|
list.sort(function (a, b) {
|
||||||
|
var emptyA = (a.k === null || a.k === '');
|
||||||
|
var emptyB = (b.k === null || b.k === '');
|
||||||
|
if (emptyA || emptyB)
|
||||||
|
return emptyA === emptyB ? a.i - b.i : (emptyA ? 1 : -1);
|
||||||
|
var cmp = (typeof a.k === 'number' && typeof b.k === 'number') ? a.k - b.k : cmpText(String(a.k), String(b.k));
|
||||||
|
return cmp !== 0 ? sign * cmp : a.i - b.i;
|
||||||
|
});
|
||||||
|
rows = [];
|
||||||
|
for (var j = 0; j < list.length; j++)
|
||||||
|
rows.push(list[j].tr);
|
||||||
|
}
|
||||||
|
applyOrder(table, info, rows);
|
||||||
|
refreshFilter(table);
|
||||||
|
setIndicator(table, th, sub, key, dir);
|
||||||
|
note(table, key === 'orig' ? T('restored') : T('changed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestSort(table, th, sub, key) {
|
||||||
|
if (table.__pwWait) {
|
||||||
|
clearInterval(table.__pwWait);
|
||||||
|
table.__pwWait = null;
|
||||||
|
}
|
||||||
|
var st = pingBusy();
|
||||||
|
if (!st.inflight && !st.left) {
|
||||||
|
doSort(table, th, sub, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var started = Date.now();
|
||||||
|
var idle = 0;
|
||||||
|
note(table, T('wait').replace('%d', st.left || st.inflight));
|
||||||
|
table.__pwWait = setInterval(function () {
|
||||||
|
st = pingBusy();
|
||||||
|
idle = st.inflight ? 0 : idle + 1;
|
||||||
|
if ((!st.inflight && !st.left) || idle >= 6) {
|
||||||
|
clearInterval(table.__pwWait);
|
||||||
|
table.__pwWait = null;
|
||||||
|
if (document.body.contains(table))
|
||||||
|
doSort(table, th, sub, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Date.now() - started > 120000) {
|
||||||
|
clearInterval(table.__pwWait);
|
||||||
|
table.__pwWait = null;
|
||||||
|
note(table, T('waitFail'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
note(table, T('wait').replace('%d', st.left || st.inflight));
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhance(table) {
|
||||||
|
var info = infoOf(table);
|
||||||
|
if (!info || table.__pwReady)
|
||||||
|
return;
|
||||||
|
var titles = table.querySelector('tr.cbi-section-table-titles');
|
||||||
|
var rows = dataRows(table);
|
||||||
|
if (!titles || !rows.length)
|
||||||
|
return;
|
||||||
|
var cols = colIndexMap(table);
|
||||||
|
var headers = cellsOf(titles);
|
||||||
|
table.__pwReady = true;
|
||||||
|
table.__pwOrig = [];
|
||||||
|
for (var r = 0; r < rows.length; r++)
|
||||||
|
table.__pwOrig.push(rowId(rows[r]));
|
||||||
|
for (var key in cols) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(cols, key))
|
||||||
|
continue;
|
||||||
|
var th = headers[cols[key]];
|
||||||
|
if (!th || th.classList.contains('cbi-section-actions'))
|
||||||
|
continue;
|
||||||
|
th.setAttribute('data-pw-sort', key);
|
||||||
|
th.title = T('hint');
|
||||||
|
var arrow = document.createElement('span');
|
||||||
|
arrow.className = 'pw-sort-arrow';
|
||||||
|
th.appendChild(arrow);
|
||||||
|
if (key !== 'name')
|
||||||
|
continue;
|
||||||
|
for (var s = 0; s < NAME_EXTRA.length; s++) {
|
||||||
|
var spec = NAME_EXTRA[s];
|
||||||
|
var sp = document.createElement('span');
|
||||||
|
sp.className = 'pw-sort-sub';
|
||||||
|
sp.setAttribute('data-pw-sort', spec[0]);
|
||||||
|
sp.textContent = T(spec[1]);
|
||||||
|
sp.title = T(spec[2]);
|
||||||
|
th.appendChild(sp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowVisible(el) {
|
||||||
|
var row = el && el.closest ? el.closest('tr') : null;
|
||||||
|
return !row || !row.classList.contains('pw-row-hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterDefs(rows, cols) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
label: T('type'),
|
||||||
|
multi: false,
|
||||||
|
get: function (tr) { return typeOf(tr, cols); }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'word',
|
||||||
|
label: T('fWord'),
|
||||||
|
multi: true,
|
||||||
|
minCount: 2,
|
||||||
|
max: 40,
|
||||||
|
get: function (tr) { return wordsOf(hiddenVal(rowId(tr), 'remarks')); }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchText(tr, cols) {
|
||||||
|
var id = rowId(tr);
|
||||||
|
return [
|
||||||
|
typeOf(tr, cols),
|
||||||
|
hiddenVal(id, 'remarks'),
|
||||||
|
hiddenVal(id, 'address') + ':' + hiddenVal(id, 'port')
|
||||||
|
].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function collect(table) {
|
||||||
|
var rows = dataRows(table);
|
||||||
|
var cols = colIndexMap(table);
|
||||||
|
var defs = rows.length >= 2 ? filterDefs(rows, cols) : [];
|
||||||
|
var shown = [];
|
||||||
|
for (var d = 0; d < defs.length; d++) {
|
||||||
|
var def = defs[d];
|
||||||
|
var counts = {};
|
||||||
|
var labels = {};
|
||||||
|
var vals = [];
|
||||||
|
for (var r = 0; r < rows.length; r++) {
|
||||||
|
var raw = def.get(rows[r]);
|
||||||
|
var list = def.multi ? (raw || []) : [raw == null ? '' : String(raw)];
|
||||||
|
var keys = [];
|
||||||
|
for (var n = 0; n < list.length; n++) {
|
||||||
|
var item = list[n];
|
||||||
|
var k = def.multi ? String(item).toLowerCase() : String(item);
|
||||||
|
counts[k] = (counts[k] || 0) + 1;
|
||||||
|
if (!labels[k])
|
||||||
|
labels[k] = {};
|
||||||
|
labels[k][item] = (labels[k][item] || 0) + 1;
|
||||||
|
keys.push(k);
|
||||||
|
}
|
||||||
|
vals.push(keys);
|
||||||
|
}
|
||||||
|
var opts = [];
|
||||||
|
for (var k in counts) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(counts, k))
|
||||||
|
continue;
|
||||||
|
if (def.multi && (counts[k] < (def.minCount || 1) || counts[k] >= rows.length))
|
||||||
|
continue;
|
||||||
|
var best = '';
|
||||||
|
var bestN = -1;
|
||||||
|
for (var label in labels[k]) {
|
||||||
|
if (labels[k][label] > bestN) {
|
||||||
|
bestN = labels[k][label];
|
||||||
|
best = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opts.push({ v: k, label: best === '' ? T('empty') : best, n: counts[k] });
|
||||||
|
}
|
||||||
|
opts.sort(function (a, b) {
|
||||||
|
if (def.multi && a.n !== b.n)
|
||||||
|
return b.n - a.n;
|
||||||
|
return cmpText(a.label, b.label);
|
||||||
|
});
|
||||||
|
if (def.max && opts.length > def.max)
|
||||||
|
opts = opts.slice(0, def.max);
|
||||||
|
var enough = def.multi ? opts.length >= 1 : opts.length >= 2;
|
||||||
|
if (!enough)
|
||||||
|
continue;
|
||||||
|
def.vals = vals;
|
||||||
|
def.opts = opts;
|
||||||
|
shown.push(def);
|
||||||
|
}
|
||||||
|
var search = rows.length >= 2;
|
||||||
|
var sig = String(rows.length) + (search ? 's' : '');
|
||||||
|
for (var i = 0; i < shown.length; i++) {
|
||||||
|
sig += '|' + shown[i].key + '=';
|
||||||
|
for (var o = 0; o < shown[i].opts.length; o++)
|
||||||
|
sig += shown[i].opts[o].v + ':' + shown[i].opts[o].n + ',';
|
||||||
|
}
|
||||||
|
return { rows: rows, cols: cols, defs: shown, search: search, sig: sig };
|
||||||
|
}
|
||||||
|
|
||||||
|
function barId(table) {
|
||||||
|
return 'pw-filter-' + table.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBar(table, collected) {
|
||||||
|
var st = table.__pwF || (table.__pwF = { sel: {}, q: '' });
|
||||||
|
var id = barId(table);
|
||||||
|
var bar = document.getElementById(id);
|
||||||
|
if (!bar) {
|
||||||
|
bar = document.createElement('div');
|
||||||
|
bar.id = id;
|
||||||
|
bar.className = 'pw-filter';
|
||||||
|
bar.title = T('filterHint');
|
||||||
|
table.parentNode.insertBefore(bar, table);
|
||||||
|
bar.__pwTable = table;
|
||||||
|
}
|
||||||
|
while (bar.firstChild)
|
||||||
|
bar.removeChild(bar.firstChild);
|
||||||
|
var head = document.createElement('span');
|
||||||
|
head.textContent = T('filter');
|
||||||
|
bar.appendChild(head);
|
||||||
|
var keep = {};
|
||||||
|
for (var d = 0; d < collected.defs.length; d++) {
|
||||||
|
var def = collected.defs[d];
|
||||||
|
var lab = document.createElement('label');
|
||||||
|
var sel = document.createElement('select');
|
||||||
|
lab.appendChild(document.createTextNode(def.label));
|
||||||
|
sel.className = 'cbi-input-select';
|
||||||
|
sel.setAttribute('data-pw-filter', def.key);
|
||||||
|
var all = document.createElement('option');
|
||||||
|
all.value = '';
|
||||||
|
all.textContent = T('all') + ' (' + collected.rows.length + ')';
|
||||||
|
sel.appendChild(all);
|
||||||
|
for (var o = 0; o < def.opts.length; o++) {
|
||||||
|
var op = document.createElement('option');
|
||||||
|
op.value = 'v:' + def.opts[o].v;
|
||||||
|
op.textContent = def.opts[o].label + ' (' + def.opts[o].n + ')';
|
||||||
|
sel.appendChild(op);
|
||||||
|
}
|
||||||
|
var cur = st.sel[def.key] || '';
|
||||||
|
var known = false;
|
||||||
|
if (cur) {
|
||||||
|
for (var c = 0; c < def.opts.length; c++) {
|
||||||
|
if (('v:' + def.opts[c].v) === cur) {
|
||||||
|
known = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!known)
|
||||||
|
cur = '';
|
||||||
|
sel.value = cur;
|
||||||
|
if (cur)
|
||||||
|
keep[def.key] = cur;
|
||||||
|
lab.appendChild(sel);
|
||||||
|
bar.appendChild(lab);
|
||||||
|
}
|
||||||
|
st.sel = keep;
|
||||||
|
if (collected.search) {
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.type = 'search';
|
||||||
|
input.className = 'cbi-input-text';
|
||||||
|
input.placeholder = T('search');
|
||||||
|
input.title = T('searchHint');
|
||||||
|
input.autocomplete = 'off';
|
||||||
|
input.setAttribute('data-pw-search', '1');
|
||||||
|
input.value = st.q;
|
||||||
|
bar.appendChild(input);
|
||||||
|
} else {
|
||||||
|
st.q = '';
|
||||||
|
}
|
||||||
|
var count = document.createElement('span');
|
||||||
|
count.className = 'pw-filter-count';
|
||||||
|
bar.appendChild(count);
|
||||||
|
var reset = document.createElement('button');
|
||||||
|
reset.type = 'button';
|
||||||
|
reset.className = 'btn cbi-button';
|
||||||
|
reset.setAttribute('data-pw-reset', '1');
|
||||||
|
reset.textContent = T('reset');
|
||||||
|
reset.title = T('resetHint');
|
||||||
|
bar.appendChild(reset);
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listHas(list, value) {
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
if (list[i] === value)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilter(table, collected, bar) {
|
||||||
|
var st = table.__pwF || { sel: {}, q: '' };
|
||||||
|
var q = String(st.q || '').toLowerCase().trim();
|
||||||
|
var active = !!q;
|
||||||
|
var shown = 0;
|
||||||
|
var unchecked = false;
|
||||||
|
var d;
|
||||||
|
for (d = 0; d < collected.defs.length; d++) {
|
||||||
|
if (st.sel[collected.defs[d].key])
|
||||||
|
active = true;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < collected.rows.length; i++) {
|
||||||
|
var tr = collected.rows[i];
|
||||||
|
var ok = true;
|
||||||
|
for (d = 0; d < collected.defs.length; d++) {
|
||||||
|
var sel = st.sel[collected.defs[d].key];
|
||||||
|
if (ok && sel && !listHas(collected.defs[d].vals[i], sel.slice(2)))
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
if (ok && q && searchText(tr, collected.cols).toLowerCase().indexOf(q) < 0)
|
||||||
|
ok = false;
|
||||||
|
if (ok !== !tr.classList.contains('pw-row-hidden'))
|
||||||
|
tr.classList.toggle('pw-row-hidden', !ok);
|
||||||
|
if (ok) {
|
||||||
|
shown++;
|
||||||
|
var want = 'cbi-rowstyle-' + ((shown - 1) % 2 + 1);
|
||||||
|
var other = want === 'cbi-rowstyle-1' ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1';
|
||||||
|
if (!tr.classList.contains(want))
|
||||||
|
tr.classList.add(want);
|
||||||
|
if (tr.classList.contains(other))
|
||||||
|
tr.classList.remove(other);
|
||||||
|
} else {
|
||||||
|
var cb = tr.querySelector('input.nodes_select');
|
||||||
|
if (cb && cb.checked) {
|
||||||
|
cb.checked = false;
|
||||||
|
unchecked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unchecked && typeof window.update_select_state === 'function')
|
||||||
|
window.update_select_state();
|
||||||
|
var count = bar.querySelector('.pw-filter-count');
|
||||||
|
var txt = T('shown').replace('%s', shown).replace('%t', collected.rows.length);
|
||||||
|
if (count && count.textContent !== txt)
|
||||||
|
count.textContent = txt;
|
||||||
|
if (bar.classList.contains('pw-filter-on') !== active)
|
||||||
|
bar.classList.toggle('pw-filter-on', active);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeBar(table) {
|
||||||
|
var bar = document.getElementById(barId(table));
|
||||||
|
if (bar && bar.parentNode)
|
||||||
|
bar.parentNode.removeChild(bar);
|
||||||
|
var rows = dataRows(table);
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
if (rows[i].classList.contains('pw-row-hidden'))
|
||||||
|
rows[i].classList.remove('pw-row-hidden');
|
||||||
|
}
|
||||||
|
table.__pwSig = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshFilter(table) {
|
||||||
|
if (!infoOf(table))
|
||||||
|
return;
|
||||||
|
var collected = collect(table);
|
||||||
|
if (!collected.defs.length && !collected.search) {
|
||||||
|
if (table.__pwSig)
|
||||||
|
removeBar(table);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var bar = document.getElementById(barId(table));
|
||||||
|
if (!bar || collected.sig !== table.__pwSig) {
|
||||||
|
bar = buildBar(table, collected);
|
||||||
|
table.__pwSig = collected.sig;
|
||||||
|
}
|
||||||
|
applyFilter(table, collected, bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
function barTable(el) {
|
||||||
|
var bar = el && el.closest ? el.closest('.pw-filter') : null;
|
||||||
|
return bar && bar.__pwTable ? bar.__pwTable : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventTarget(ev) {
|
||||||
|
var t = ev.target;
|
||||||
|
if (t && t.nodeType !== 1)
|
||||||
|
t = t.parentNode;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind(root) {
|
||||||
|
root.addEventListener('click', function (ev) {
|
||||||
|
var t = eventTarget(ev);
|
||||||
|
if (t && t.getAttribute && t.getAttribute('data-pw-reset')) {
|
||||||
|
var resetTable = barTable(t);
|
||||||
|
if (!resetTable)
|
||||||
|
return;
|
||||||
|
ev.preventDefault();
|
||||||
|
resetTable.__pwF = { sel: {}, q: '' };
|
||||||
|
resetTable.__pwSig = null;
|
||||||
|
refreshFilter(resetTable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var el = t && t.closest ? t.closest('[data-pw-sort]') : null;
|
||||||
|
if (!el || !root.contains(el))
|
||||||
|
return;
|
||||||
|
if (t.closest && t.closest('input, a, button, select'))
|
||||||
|
return;
|
||||||
|
var th = el.closest('th');
|
||||||
|
var table = el.closest('table');
|
||||||
|
if (!th || !table || !infoOf(table))
|
||||||
|
return;
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
requestSort(table, th, el === th ? null : el, el.getAttribute('data-pw-sort'));
|
||||||
|
});
|
||||||
|
root.addEventListener('change', function (ev) {
|
||||||
|
var t = eventTarget(ev);
|
||||||
|
if (!t || !t.getAttribute || !t.getAttribute('data-pw-filter'))
|
||||||
|
return;
|
||||||
|
var table = barTable(t);
|
||||||
|
if (!table)
|
||||||
|
return;
|
||||||
|
(table.__pwF = table.__pwF || { sel: {}, q: '' }).sel[t.getAttribute('data-pw-filter')] = t.value;
|
||||||
|
refreshFilter(table);
|
||||||
|
});
|
||||||
|
root.addEventListener('input', function (ev) {
|
||||||
|
var t = eventTarget(ev);
|
||||||
|
if (!t || !t.getAttribute || !t.getAttribute('data-pw-search'))
|
||||||
|
return;
|
||||||
|
var table = barTable(t);
|
||||||
|
if (!table)
|
||||||
|
return;
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
searchTimer = setTimeout(function () {
|
||||||
|
(table.__pwF = table.__pwF || { sel: {}, q: '' }).q = t.value;
|
||||||
|
refreshFilter(table);
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
root.addEventListener('keydown', function (ev) {
|
||||||
|
var t = eventTarget(ev);
|
||||||
|
if (ev.key === 'Enter' && t && t.getAttribute && t.getAttribute('data-pw-search'))
|
||||||
|
ev.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var box = document.getElementById('pw2-nodesort-i18n');
|
||||||
|
var root = document.getElementById('node_list');
|
||||||
|
if (!box || !root)
|
||||||
|
return;
|
||||||
|
state.app = box.getAttribute('data-app') || 'passwall2';
|
||||||
|
state.text = readText(box);
|
||||||
|
state.root = root;
|
||||||
|
prepareCollator();
|
||||||
|
if (!state.bound) {
|
||||||
|
bind(root);
|
||||||
|
state.bound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhanceAll() {
|
||||||
|
if (!state.root)
|
||||||
|
return;
|
||||||
|
var tables = state.root.querySelectorAll('table.cbi-section-table');
|
||||||
|
for (var i = 0; i < tables.length; i++) {
|
||||||
|
try {
|
||||||
|
enhance(tables[i]);
|
||||||
|
refreshFilter(tables[i]);
|
||||||
|
} catch (e) {
|
||||||
|
if (window.console)
|
||||||
|
console.warn('passwall2 node list sort:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pw2NodeSort = {
|
||||||
|
init: init,
|
||||||
|
enhanceAll: enhanceAll,
|
||||||
|
rowVisible: rowVisible
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -586,11 +586,15 @@ function gen_config_server(node)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if node.protocol == "vmess" or node.protocol == "vless" then
|
if node.protocol == "vless" then
|
||||||
|
settings = {
|
||||||
|
users = users,
|
||||||
|
decryption = (node.decryption and node.decryption ~= "") and node.decryption or "none"
|
||||||
|
}
|
||||||
|
elseif node.protocol == "vmess" then
|
||||||
if users then
|
if users then
|
||||||
settings = {
|
settings = {
|
||||||
users = users,
|
users = users
|
||||||
decryption = (node.protocol == "vless") and ((node.decryption and node.decryption ~= "") and node.decryption or "none") or nil
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
elseif node.protocol == "socks" then
|
elseif node.protocol == "socks" then
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ if node then
|
|||||||
end
|
end
|
||||||
-%>
|
-%>
|
||||||
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
|
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
|
||||||
|
<script src="<%=resource%>/view/<%=appname%>/node_list_sort.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
table th, .table .th {
|
table th, .table .th {
|
||||||
@@ -383,6 +384,89 @@ table td, .table .td {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* node list: click-to-sort headers and the per-group filter bar */
|
||||||
|
th[data-pw-sort] {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-pw-sort]:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-sort-arrow {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 1em;
|
||||||
|
margin-left: 0.25em;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-sort-sub {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 85%;
|
||||||
|
opacity: 0.85;
|
||||||
|
margin-left: 0.6em;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-sort-sub.pw-sort-active {
|
||||||
|
opacity: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-sort-note {
|
||||||
|
margin: 0.3em 0;
|
||||||
|
padding: 0.3em 0.6em;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(251, 154, 5, 0.15);
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.pw-row-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-filter {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3em 0.8em;
|
||||||
|
margin: 0.4em 0;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-filter label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-filter select,
|
||||||
|
.pw-filter input[type="search"],
|
||||||
|
.pw-filter .cbi-button {
|
||||||
|
width: auto !important;
|
||||||
|
min-width: 6em;
|
||||||
|
max-width: 22em;
|
||||||
|
height: auto;
|
||||||
|
padding: 0.15em 0.3em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-filter .pw-filter-count {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-filter.pw-filter-on .pw-filter-count {
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
<% if api.is_js_luci() then -%>
|
<% if api.is_js_luci() then -%>
|
||||||
var cbi_t = [];
|
var cbi_t = [];
|
||||||
@@ -538,15 +622,29 @@ table td, .table .td {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function node_row_visible(el) {
|
||||||
|
if (window.pw2NodeSort && typeof pw2NodeSort.rowVisible === "function")
|
||||||
|
return pw2NodeSort.rowVisible(el);
|
||||||
|
var row = el && el.closest ? el.closest("tr") : null;
|
||||||
|
return !row || !row.classList.contains("pw-row-hidden");
|
||||||
|
}
|
||||||
|
|
||||||
function set_select_all_state(sectionChecked) {
|
function set_select_all_state(sectionChecked) {
|
||||||
var visibleContainer = document.querySelector('#cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display: block"]');
|
var visibleContainer = document.querySelector('#cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display: block"]');
|
||||||
if (!visibleContainer) return;
|
if (!visibleContainer) return;
|
||||||
var nodes = visibleContainer.getElementsByClassName("nodes_select");
|
var nodes = visibleContainer.getElementsByClassName("nodes_select");
|
||||||
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
||||||
var selectAllBtn = document.getElementById("select_all_btn");
|
var selectAllBtn = document.getElementById("select_all_btn");
|
||||||
|
var visibleCount = 0;
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
if (!node_row_visible(nodes[i])) {
|
||||||
|
nodes[i].checked = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
nodes[i].checked = sectionChecked;
|
nodes[i].checked = sectionChecked;
|
||||||
|
visibleCount++;
|
||||||
}
|
}
|
||||||
|
sectionChecked = sectionChecked && visibleCount > 0;
|
||||||
if (selectAllChk) {
|
if (selectAllChk) {
|
||||||
selectAllChk.checked = sectionChecked;
|
selectAllChk.checked = sectionChecked;
|
||||||
selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
||||||
@@ -574,10 +672,13 @@ table td, .table .td {
|
|||||||
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
||||||
var selectAllBtn = document.getElementById("select_all_btn");
|
var selectAllBtn = document.getElementById("select_all_btn");
|
||||||
var checkedCount = 0;
|
var checkedCount = 0;
|
||||||
|
var visibleCount = 0;
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
if (!node_row_visible(nodes[i])) continue;
|
||||||
|
visibleCount++;
|
||||||
if (nodes[i].checked) checkedCount++;
|
if (nodes[i].checked) checkedCount++;
|
||||||
}
|
}
|
||||||
var allChecked = checkedCount === nodes.length;
|
var allChecked = visibleCount > 0 && checkedCount === visibleCount;
|
||||||
var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
||||||
var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)";
|
var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)";
|
||||||
|
|
||||||
@@ -600,7 +701,7 @@ table td, .table .td {
|
|||||||
var doms = visibleContainer.getElementsByClassName("nodes_select");
|
var doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||||
if (doms && doms.length > 0) {
|
if (doms && doms.length > 0) {
|
||||||
for (var i = 0 ; i < doms.length; i++) {
|
for (var i = 0 ; i < doms.length; i++) {
|
||||||
if (doms[i].checked) {
|
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||||
ids.push(doms[i].getAttribute("cbid"))
|
ids.push(doms[i].getAttribute("cbid"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1158,7 +1259,7 @@ table td, .table .td {
|
|||||||
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||||
if (doms && doms.length > 0) {
|
if (doms && doms.length > 0) {
|
||||||
for (let i = 0 ; i < doms.length; i++) {
|
for (let i = 0 ; i < doms.length; i++) {
|
||||||
if (doms[i].checked) {
|
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||||
ids.push(doms[i].getAttribute("cbid"))
|
ids.push(doms[i].getAttribute("cbid"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1190,7 +1291,7 @@ table td, .table .td {
|
|||||||
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||||
if (doms && doms.length > 0) {
|
if (doms && doms.length > 0) {
|
||||||
for (let i = 0 ; i < doms.length; i++) {
|
for (let i = 0 ; i < doms.length; i++) {
|
||||||
if (doms[i].checked) {
|
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||||
ids.push(doms[i].getAttribute("cbid"))
|
ids.push(doms[i].getAttribute("cbid"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1298,8 +1399,33 @@ table td, .table .td {
|
|||||||
<fieldset class="cbi-section" id="node_list">
|
<fieldset class="cbi-section" id="node_list">
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<div id="pw2-nodesort-i18n" data-app="<%=appname%>" style="display:none">
|
||||||
|
<span data-k="hint"><%:Click to sort; click again to reverse. Press Save & Apply to keep the order.%></span>
|
||||||
|
<span data-k="typeHint"><%:Sort by type or protocol, then by name%></span>
|
||||||
|
<span data-k="addrHint"><%:Sort by server address and port%></span>
|
||||||
|
<span data-k="origHint"><%:Restore the saved order%></span>
|
||||||
|
<span data-k="changed"><%:The order changed on this page only. Press Save & Apply to store it on the router.%></span>
|
||||||
|
<span data-k="restored"><%:Saved order restored.%></span>
|
||||||
|
<span data-k="wait"><%:Automatic ping is still running (%d left). Sorting will run when it finishes.%></span>
|
||||||
|
<span data-k="waitFail"><%:Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again.%></span>
|
||||||
|
<span data-k="filter"><%:Filter:%></span>
|
||||||
|
<span data-k="all"><%:All%></span>
|
||||||
|
<span data-k="empty"><%:(empty)%></span>
|
||||||
|
<span data-k="type"><%:Type%></span>
|
||||||
|
<span data-k="addr"><%:Address%></span>
|
||||||
|
<span data-k="fWord"><%:Word in remarks%></span>
|
||||||
|
<span data-k="search"><%:search...%></span>
|
||||||
|
<span data-k="searchHint"><%:Show only rows that contain this text%></span>
|
||||||
|
<span data-k="shown"><%:Shown %s of %t%></span>
|
||||||
|
<span data-k="reset"><%:Reset%></span>
|
||||||
|
<span data-k="resetHint"><%:Show all rows%></span>
|
||||||
|
<span data-k="filterHint"><%:The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows.%></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
//init logic
|
//init logic
|
||||||
|
if (window.pw2NodeSort)
|
||||||
|
pw2NodeSort.init();
|
||||||
|
|
||||||
function get_remarks_name(o) {
|
function get_remarks_name(o) {
|
||||||
let str = "";
|
let str = "";
|
||||||
@@ -1499,8 +1625,10 @@ table td, .table .td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get_now_use_node();
|
get_now_use_node();
|
||||||
|
|
||||||
pingAllNodes();
|
pingAllNodes();
|
||||||
|
if (window.pw2NodeSort)
|
||||||
|
pw2NodeSort.enhanceAll();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2530,3 +2530,54 @@ msgstr "بهروزرسانی ناموفق بود"
|
|||||||
|
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "خطای ناشناخته"
|
msgstr "خطای ناشناخته"
|
||||||
|
|
||||||
|
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
|
||||||
|
msgstr "برای مرتبسازی کلیک کنید؛ کلیک دوباره جهت را عوض میکند. برای نگه داشتن ترتیب، ذخیره و اعمال را بزنید."
|
||||||
|
|
||||||
|
msgid "Sort by type or protocol, then by name"
|
||||||
|
msgstr "مرتبسازی بر اساس نوع یا پروتکل، سپس نام"
|
||||||
|
|
||||||
|
msgid "Sort by server address and port"
|
||||||
|
msgstr "مرتبسازی بر اساس آدرس سرور و درگاه"
|
||||||
|
|
||||||
|
msgid "Restore the saved order"
|
||||||
|
msgstr "بازگرداندن ترتیب ذخیرهشده"
|
||||||
|
|
||||||
|
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
|
||||||
|
msgstr "ترتیب فقط در این صفحه عوض شده است. برای نوشتن آن روی روتر، ذخیره و اعمال را بزنید."
|
||||||
|
|
||||||
|
msgid "Saved order restored."
|
||||||
|
msgstr "ترتیب ذخیرهشده بازگردانده شد."
|
||||||
|
|
||||||
|
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
|
||||||
|
msgstr "بررسی خودکار هنوز در حال اجراست (%d باقی مانده). مرتبسازی بلافاصله پس از پایان آن انجام میشود."
|
||||||
|
|
||||||
|
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
|
||||||
|
msgstr "بررسی خودکار ظرف 2 دقیقه تمام نشد. مرتبسازی لغو شد. صفحه را دوباره بارگذاری کنید و دوباره تلاش کنید."
|
||||||
|
|
||||||
|
msgid "Filter:"
|
||||||
|
msgstr "فیلتر:"
|
||||||
|
|
||||||
|
msgid "(empty)"
|
||||||
|
msgstr "(خالی)"
|
||||||
|
|
||||||
|
msgid "Word in remarks"
|
||||||
|
msgstr "کلمه در توضیحات"
|
||||||
|
|
||||||
|
msgid "search..."
|
||||||
|
msgstr "جستجو..."
|
||||||
|
|
||||||
|
msgid "Show only rows that contain this text"
|
||||||
|
msgstr "فقط ردیفهایی را نشان بده که این متن را دارند"
|
||||||
|
|
||||||
|
msgid "Shown %s of %t"
|
||||||
|
msgstr "نمایش %s از %t"
|
||||||
|
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr "بازنشانی"
|
||||||
|
|
||||||
|
msgid "Show all rows"
|
||||||
|
msgstr "نمایش همه ردیفها"
|
||||||
|
|
||||||
|
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
|
||||||
|
msgstr "فیلتر فقط ردیفها را در این صفحه پنهان میکند و چیزی را روی روتر تغییر نمیدهد. انتخاب همه، ردیفهای پنهان را رد میکند."
|
||||||
@@ -2531,3 +2531,54 @@ msgstr "Ошибка обновления"
|
|||||||
|
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "Неизвестная ошибка"
|
msgstr "Неизвестная ошибка"
|
||||||
|
|
||||||
|
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
|
||||||
|
msgstr "Нажмите для сортировки; повторное нажатие меняет направление. Чтобы сохранить порядок, нажмите «Сохранить и применить»."
|
||||||
|
|
||||||
|
msgid "Sort by type or protocol, then by name"
|
||||||
|
msgstr "Сортировать по типу или протоколу, затем по имени"
|
||||||
|
|
||||||
|
msgid "Sort by server address and port"
|
||||||
|
msgstr "Сортировать по адресу сервера и порту"
|
||||||
|
|
||||||
|
msgid "Restore the saved order"
|
||||||
|
msgstr "Вернуть сохранённый порядок"
|
||||||
|
|
||||||
|
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
|
||||||
|
msgstr "Порядок изменён только на этой странице. Чтобы записать его на роутер, нажмите «Сохранить и применить»."
|
||||||
|
|
||||||
|
msgid "Saved order restored."
|
||||||
|
msgstr "Сохранённый порядок восстановлен."
|
||||||
|
|
||||||
|
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
|
||||||
|
msgstr "Автопроверка ещё выполняется (осталось %d). Сортировка запустится сразу после её окончания."
|
||||||
|
|
||||||
|
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
|
||||||
|
msgstr "Автопроверка не завершилась за 2 минуты. Сортировка отменена. Обновите страницу и попробуйте снова."
|
||||||
|
|
||||||
|
msgid "Filter:"
|
||||||
|
msgstr "Фильтр:"
|
||||||
|
|
||||||
|
msgid "(empty)"
|
||||||
|
msgstr "(пусто)"
|
||||||
|
|
||||||
|
msgid "Word in remarks"
|
||||||
|
msgstr "Слово в описании"
|
||||||
|
|
||||||
|
msgid "search..."
|
||||||
|
msgstr "поиск..."
|
||||||
|
|
||||||
|
msgid "Show only rows that contain this text"
|
||||||
|
msgstr "Показать только строки, содержащие этот текст"
|
||||||
|
|
||||||
|
msgid "Shown %s of %t"
|
||||||
|
msgstr "Показано %s из %t"
|
||||||
|
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr "Сбросить"
|
||||||
|
|
||||||
|
msgid "Show all rows"
|
||||||
|
msgstr "Показать все строки"
|
||||||
|
|
||||||
|
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
|
||||||
|
msgstr "Фильтр только скрывает строки на этой странице и ничего не меняет на роутере. «Выделить все» пропускает скрытые строки."
|
||||||
@@ -2517,3 +2517,53 @@ msgstr "更新失败"
|
|||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "未知错误"
|
msgstr "未知错误"
|
||||||
|
|
||||||
|
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
|
||||||
|
msgstr "点击表头排序,再次点击切换方向。按保存并应用可保留顺序。"
|
||||||
|
|
||||||
|
msgid "Sort by type or protocol, then by name"
|
||||||
|
msgstr "先按类型或协议排序,再按名称排序"
|
||||||
|
|
||||||
|
msgid "Sort by server address and port"
|
||||||
|
msgstr "按服务器地址和端口排序"
|
||||||
|
|
||||||
|
msgid "Restore the saved order"
|
||||||
|
msgstr "恢复已保存的顺序"
|
||||||
|
|
||||||
|
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
|
||||||
|
msgstr "顺序仅在本页改变。按保存并应用才会写入路由器。"
|
||||||
|
|
||||||
|
msgid "Saved order restored."
|
||||||
|
msgstr "已恢复保存的顺序。"
|
||||||
|
|
||||||
|
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
|
||||||
|
msgstr "自动探测仍在进行(剩余 %d)。结束后会立即排序。"
|
||||||
|
|
||||||
|
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
|
||||||
|
msgstr "自动探测在 2 分钟内未结束,已取消排序。请刷新页面后重试。"
|
||||||
|
|
||||||
|
msgid "Filter:"
|
||||||
|
msgstr "筛选:"
|
||||||
|
|
||||||
|
msgid "(empty)"
|
||||||
|
msgstr "(空)"
|
||||||
|
|
||||||
|
msgid "Word in remarks"
|
||||||
|
msgstr "备注关键词"
|
||||||
|
|
||||||
|
msgid "search..."
|
||||||
|
msgstr "搜索..."
|
||||||
|
|
||||||
|
msgid "Show only rows that contain this text"
|
||||||
|
msgstr "只显示包含此文本的行"
|
||||||
|
|
||||||
|
msgid "Shown %s of %t"
|
||||||
|
msgstr "显示 %s / %t"
|
||||||
|
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr "重置"
|
||||||
|
|
||||||
|
msgid "Show all rows"
|
||||||
|
msgstr "显示全部行"
|
||||||
|
|
||||||
|
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
|
||||||
|
msgstr "筛选只在本页隐藏行,不会改动路由器上的设置。全选会跳过隐藏行。"
|
||||||
@@ -2522,3 +2522,54 @@ msgstr "更新失敗"
|
|||||||
|
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "未知錯誤"
|
msgstr "未知錯誤"
|
||||||
|
|
||||||
|
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
|
||||||
|
msgstr "點擊表頭排序,再次點擊切換方向。按儲存並套用可保留順序。"
|
||||||
|
|
||||||
|
msgid "Sort by type or protocol, then by name"
|
||||||
|
msgstr "先按類型或協定排序,再按名稱排序"
|
||||||
|
|
||||||
|
msgid "Sort by server address and port"
|
||||||
|
msgstr "按伺服器位址和埠排序"
|
||||||
|
|
||||||
|
msgid "Restore the saved order"
|
||||||
|
msgstr "還原已儲存的順序"
|
||||||
|
|
||||||
|
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
|
||||||
|
msgstr "順序只在本頁改變。按儲存並套用才會寫入路由器。"
|
||||||
|
|
||||||
|
msgid "Saved order restored."
|
||||||
|
msgstr "已還原儲存的順序。"
|
||||||
|
|
||||||
|
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
|
||||||
|
msgstr "自動探測仍在進行(剩餘 %d)。結束後會立即排序。"
|
||||||
|
|
||||||
|
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
|
||||||
|
msgstr "自動探測在 2 分鐘內未結束,已取消排序。請重新整理頁面後再試。"
|
||||||
|
|
||||||
|
msgid "Filter:"
|
||||||
|
msgstr "篩選:"
|
||||||
|
|
||||||
|
msgid "(empty)"
|
||||||
|
msgstr "(空)"
|
||||||
|
|
||||||
|
msgid "Word in remarks"
|
||||||
|
msgstr "備註關鍵詞"
|
||||||
|
|
||||||
|
msgid "search..."
|
||||||
|
msgstr "搜尋..."
|
||||||
|
|
||||||
|
msgid "Show only rows that contain this text"
|
||||||
|
msgstr "只顯示包含此文字的列"
|
||||||
|
|
||||||
|
msgid "Shown %s of %t"
|
||||||
|
msgstr "顯示 %s / %t"
|
||||||
|
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr "重設"
|
||||||
|
|
||||||
|
msgid "Show all rows"
|
||||||
|
msgstr "顯示全部列"
|
||||||
|
|
||||||
|
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
|
||||||
|
msgstr "篩選只在本頁隱藏列,不會改動路由器上的設定。全選會跳過隱藏列。"
|
||||||
+2
-2
@@ -5,12 +5,12 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=sing-box
|
PKG_NAME:=sing-box
|
||||||
PKG_VERSION:=1.14.3
|
PKG_VERSION:=1.14.4
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
|
||||||
PKG_HASH:=9a8e3712a5f611ad95f1d16874bbdcdffbd3b8eed16dae2d71ce7042a33c3386
|
PKG_HASH:=0958ccd23b1ea9a141d715d15478b1e040fd04bcca19d8ff5529636b1b753e18
|
||||||
|
|
||||||
PKG_LICENSE:=GPL-3.0-or-later
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
PKG_LICENSE_FILES:=LICENSE
|
PKG_LICENSE_FILES:=LICENSE
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ define Download/geoip
|
|||||||
HASH:=4ff4b2ec242a9e87b296ba27ddb85f8311e8d3e071eaebac5be9231c60e408dd
|
HASH:=4ff4b2ec242a9e87b296ba27ddb85f8311e8d3e071eaebac5be9231c60e408dd
|
||||||
endef
|
endef
|
||||||
|
|
||||||
GEOSITE_VER:=20261023064708
|
GEOSITE_VER:=20261024005255
|
||||||
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
|
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
|
||||||
define Download/geosite
|
define Download/geosite
|
||||||
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
|
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
|
||||||
URL_FILE:=dlc.dat
|
URL_FILE:=dlc.dat
|
||||||
FILE:=$(GEOSITE_FILE)
|
FILE:=$(GEOSITE_FILE)
|
||||||
HASH:=76b0704d7f2630e9b06d83595e9ffa363c946a1a3e19c40c0f43aa9f5d77f399
|
HASH:=356b6accce7b724490ddffee25e69edc9de8a1e7fe4e799871cf5c417fdf31d7
|
||||||
endef
|
endef
|
||||||
|
|
||||||
GEOSITE_IRAN_VER:=202610230802
|
GEOSITE_IRAN_VER:=202610230802
|
||||||
|
|||||||
Reference in new issue
Block a user