diff --git a/clashoo/Makefile b/clashoo/Makefile index 135a2131..488bb6d4 100644 --- a/clashoo/Makefile +++ b/clashoo/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=clashoo -PKG_SHORT_SHA:=3f27ef3 -PKG_COMMIT_DATE:=2026.10.21 +PKG_SHORT_SHA:=91e85ee +PKG_COMMIT_DATE:=2026.10.24 PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA) 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_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_VERSION:=3f27ef39f8c96bbfbc8b0b9bb10f5c9ef60149d8 -PKG_HASH:=b8534ada1cbd486386e1db95c5720f752ec682fb90cf3b3f963e472976b11e63 +PKG_SOURCE_VERSION:=91e85eebf0b654c73424f0a215b3c56f6b39614f +PKG_HASH:=efa0d9d66a589f06f402f50e0dea349ab9923d36d181303769f0575c0779e562 PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA) PKG_LICENSE:=GPL3.0+ diff --git a/luci-app-eqosplus/.github/workflows/main.yml b/luci-app-eqosplus/.github/workflows/main.yml new file mode 100644 index 00000000..3258ec29 --- /dev/null +++ b/luci-app-eqosplus/.github/workflows/main.yml @@ -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" diff --git a/luci-app-eqosplus/LICENSE b/luci-app-eqosplus/LICENSE new file mode 100644 index 00000000..574c7ac3 --- /dev/null +++ b/luci-app-eqosplus/LICENSE @@ -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. diff --git a/luci-app-eqosplus/Makefile b/luci-app-eqosplus/Makefile new file mode 100644 index 00000000..91e501b7 --- /dev/null +++ b/luci-app-eqosplus/Makefile @@ -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 + +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 + diff --git a/luci-app-eqosplus/README.md b/luci-app-eqosplus/README.md new file mode 100644 index 00000000..d2707f57 --- /dev/null +++ b/luci-app-eqosplus/README.md @@ -0,0 +1,91 @@ +![hello](https://views.whatilearened.today/views/github/sirpdboy/deplives.svg) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ) + +

+
luci-app-eqosplus
+

+ +

+OpenWrt +Chrome +Safari +Firefox + GitHub release +Downloads +

+ +[中文] | [English](README.md) + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg) + +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 + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明2.jpg) + + +## interface + +![screenshots](./doc/eqosplus0.png) + +![screenshots](./doc/eqosplus1.png) + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/eqosplus.png) + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/eqosplus2.png) + + +# 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 + +| 图飞了 | 图飞了 | +| :-----------------: | :-------------: | +|![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) | + + + no + + +![hello](https://visitor-badge-deno.deno.dev/sirpdboy.sirpdboy.svg) [![](https://img.shields.io/badge/TGGroup-ClickJoin-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ) diff --git a/luci-app-eqosplus/README_CN.md b/luci-app-eqosplus/README_CN.md new file mode 100644 index 00000000..8da16627 --- /dev/null +++ b/luci-app-eqosplus/README_CN.md @@ -0,0 +1,109 @@ +![hello](https://views.whatilearened.today/views/github/sirpdboy/deplives.svg) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ) + +

+
luci-app-eqosplus
+

+ +

+ + + + +

+ +[中文] | [English](README.md) + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg) + +请 **认真阅读完毕** 本页面,本页面包含注意事项和如何使用。 + +## 功能说明: + +### 定时限速1.2.2版 +#### 2023.7.19 定时限速1.2.2:增加更多日期:工作日和休息日,自定义日期1,2,3中间用逗号分隔;加入MAC地址限速,从此不用担心IPV6和IPV4的限速问题。 + +### 定时限速1.0版 +#### 2022.12.24 定时限速在eqos的加强版,加入定时限制等功能。 + +## 编译使用方法 [![](https://img.shields.io/badge/-编译使用方法-F5F5F5.svg)](#编译使用方法-) + +将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://img.shields.io/badge/-说明-F5F5F5.svg)](#说明-) + +源码来源:https://github.com/sirpdboy/luci-app-eqosplus + + +- 你可以随意使用其中的源码,但请注明出处。 + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明2.jpg) + +## 界面 + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/eqosplus.png) + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/eqosplus2.png) + +# 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 + +## 捐助 + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明3.jpg) + +| 图飞了 | 图飞了 | +| :-----------------: | :-------------: | +|![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) | + + + 图飞了 + + +![](https://visitor-badge-deno.deno.dev/sirpdboy.sirpdboy.svg) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ) + diff --git a/luci-app-eqosplus/doc/eqosplus0.png b/luci-app-eqosplus/doc/eqosplus0.png new file mode 100644 index 00000000..978f4abd Binary files /dev/null and b/luci-app-eqosplus/doc/eqosplus0.png differ diff --git a/luci-app-eqosplus/doc/eqosplus1.png b/luci-app-eqosplus/doc/eqosplus1.png new file mode 100644 index 00000000..c603fcda Binary files /dev/null and b/luci-app-eqosplus/doc/eqosplus1.png differ diff --git a/luci-app-eqosplus/luasrc/controller/eqosplus.lua b/luci-app-eqosplus/luasrc/controller/eqosplus.lua new file mode 100644 index 00000000..cbe01b70 --- /dev/null +++ b/luci-app-eqosplus/luasrc/controller/eqosplus.lua @@ -0,0 +1,19 @@ +module("luci.controller.eqosplus", package.seeall) +-- Copyright 2022-2023 sirpdboy +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 diff --git a/luci-app-eqosplus/luasrc/model/cbi/eqosplus.lua b/luci-app-eqosplus/luasrc/model/cbi/eqosplus.lua new file mode 100644 index 00000000..32c3433c --- /dev/null +++ b/luci-app-eqosplus/luasrc/model/cbi/eqosplus.lua @@ -0,0 +1,190 @@ +-- Copyright 2022-2023 sirpdboy +-- 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 diff --git a/luci-app-eqosplus/luasrc/view/eqosplus/eqosplus.htm b/luci-app-eqosplus/luasrc/view/eqosplus/eqosplus.htm new file mode 100644 index 00000000..4445143f --- /dev/null +++ b/luci-app-eqosplus/luasrc/view/eqosplus/eqosplus.htm @@ -0,0 +1,3 @@ +<%+cbi/valueheader%> +<%=pcdata(self:cfgvalue(section) or self.default or "")%> +<%+cbi/valuefooter%> \ No newline at end of file diff --git a/luci-app-eqosplus/luasrc/view/eqosplus/index.htm b/luci-app-eqosplus/luasrc/view/eqosplus/index.htm new file mode 100644 index 00000000..c4b8a072 --- /dev/null +++ b/luci-app-eqosplus/luasrc/view/eqosplus/index.htm @@ -0,0 +1,12 @@ +<% include("cbi/map") %> + \ No newline at end of file diff --git a/luci-app-eqosplus/po/templates/eqosplus.pot b/luci-app-eqosplus/po/templates/eqosplus.pot new file mode 100644 index 00000000..151cee8d --- /dev/null +++ b/luci-app-eqosplus/po/templates/eqosplus.pot @@ -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 "" + diff --git a/luci-app-eqosplus/po/zh-cn/eqosplus.po b/luci-app-eqosplus/po/zh-cn/eqosplus.po new file mode 100644 index 00000000..0fa56393 --- /dev/null +++ b/luci-app-eqosplus/po/zh-cn/eqosplus.po @@ -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限制用户上传/下载的网速。速度单位为 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 "自动设置" + diff --git a/luci-app-eqosplus/po/zh_Hans/eqosplus.po b/luci-app-eqosplus/po/zh_Hans/eqosplus.po new file mode 100644 index 00000000..840559fc --- /dev/null +++ b/luci-app-eqosplus/po/zh_Hans/eqosplus.po @@ -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 "自动设置" + diff --git a/luci-app-eqosplus/root/etc/config/eqosplus b/luci-app-eqosplus/root/etc/config/eqosplus new file mode 100644 index 00000000..722434ae --- /dev/null +++ b/luci-app-eqosplus/root/etc/config/eqosplus @@ -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' + diff --git a/luci-app-eqosplus/root/etc/hotplug.d/iface/10-eqosplus b/luci-app-eqosplus/root/etc/hotplug.d/iface/10-eqosplus new file mode 100644 index 00000000..91059ec8 --- /dev/null +++ b/luci-app-eqosplus/root/etc/hotplug.d/iface/10-eqosplus @@ -0,0 +1,5 @@ +#!/bin/sh +[ "$ACTION" = "ifup" ] && [ "$INTERFACE" = "wan" ] && { + sleep 5 + /etc/init.d/eqosplus restart +} \ No newline at end of file diff --git a/luci-app-eqosplus/root/etc/init.d/eqosplus b/luci-app-eqosplus/root/etc/init.d/eqosplus new file mode 100644 index 00000000..f74b5fdd --- /dev/null +++ b/luci-app-eqosplus/root/etc/init.d/eqosplus @@ -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 +} + diff --git a/luci-app-eqosplus/root/etc/uci-defaults/luci-eqosplus b/luci-app-eqosplus/root/etc/uci-defaults/luci-eqosplus new file mode 100644 index 00000000..f22cd0c6 --- /dev/null +++ b/luci-app-eqosplus/root/etc/uci-defaults/luci-eqosplus @@ -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 diff --git a/luci-app-eqosplus/root/usr/bin/eqosplus b/luci-app-eqosplus/root/usr/bin/eqosplus new file mode 100644 index 00000000..721fafa0 --- /dev/null +++ b/luci-app-eqosplus/root/usr/bin/eqosplus @@ -0,0 +1,379 @@ +#!/bin/bash + +# Copyright (C) 2006 OpenWrt.org +# Copyright 2022-2025 sirpdboy + +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 \ No newline at end of file diff --git a/luci-app-eqosplus/root/usr/bin/eqosplusctrl b/luci-app-eqosplus/root/usr/bin/eqosplusctrl new file mode 100644 index 00000000..9b5721d6 --- /dev/null +++ b/luci-app-eqosplus/root/usr/bin/eqosplusctrl @@ -0,0 +1,63 @@ +#!/bin/sh + +# Copyright (C) 2006 OpenWrt.org +# Copyright 2022-2025 sirpdboy +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 \ No newline at end of file diff --git a/luci-app-eqosplus/root/usr/share/rpcd/acl.d/luci-app-eqosplus.json b/luci-app-eqosplus/root/usr/share/rpcd/acl.d/luci-app-eqosplus.json new file mode 100644 index 00000000..b879f4de --- /dev/null +++ b/luci-app-eqosplus/root/usr/share/rpcd/acl.d/luci-app-eqosplus.json @@ -0,0 +1,11 @@ +{ + "luci-app-eqosplus": { + "description": "Grant UCI access for luci-app-eqosplus", + "read": { + "uci": [ "eqosplus" ] + }, + "write": { + "uci": [ "eqosplus" ] + } + } +} diff --git a/luci-app-eqosplus/界面.png b/luci-app-eqosplus/界面.png new file mode 100644 index 00000000..d346c698 Binary files /dev/null and b/luci-app-eqosplus/界面.png differ diff --git a/luci-app-passwall/htdocs/luci-static/resources/view/passwall/node_list_sort.js b/luci-app-passwall/htdocs/luci-static/resources/view/passwall/node_list_sort.js new file mode 100644 index 00000000..20a1e885 --- /dev/null +++ b/luci-app-passwall/htdocs/luci-static/resources/view/passwall/node_list_sort.js @@ -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 + }; +})(); diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index f463116c..40e69c3a 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -579,11 +579,15 @@ function gen_config_server(node) 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 settings = { - users = users, - decryption = (node.protocol == "vless") and ((node.decryption and node.decryption ~= "") and node.decryption or "none") or nil + users = users } end elseif node.protocol == "socks" then diff --git a/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm b/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm index c69a075b..731a35c8 100644 --- a/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm +++ b/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm @@ -24,6 +24,7 @@ if node then end -%> + + + + + +