diff --git a/airconnect/Makefile b/airconnect/Makefile index 53bccc948..3c6bb15a5 100644 --- a/airconnect/Makefile +++ b/airconnect/Makefile @@ -1,25 +1,24 @@ # -# Copyright (C) 2015 OpenWrt.org +# Copyright (C) 2015-2016 OpenWrt.org # -# This is free software, licensed under the GNU General Public License v2. -# See /LICENSE for more information. +# This is free software, licensed under the GNU General Public License v3. # include $(TOPDIR)/rules.mk PKG_NAME:=airconnect -PKG_VERSION:=1.0.13 -PKG_RELEASE:=1 +PKG_VERSION:=1.7.0 +PKG_RELEASE=1 -PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz -PKG_SOURCE_VERSION:=9fc8c184da22d6b34a5b093f6ec8cf6ecaf22dbf -PKG_SOURCE_URL_FILE:=$(PKG_SOURCE_VERSION).tar.gz -PKG_SOURCE_URL:=https://github.com/philippe44/AirConnect/archive/ -PKG_HASH:=0da27af9a1d49cd83f8381453d5e1b9c6551ad1ce45bbb7b820e5499796c5440 +PKG_SOURCE:=AirConnect-$(PKG_VERSION).zip +PKG_SOURCE_URL:=https://github.com/philippe44/AirConnect/releases/download/$(PKG_VERSION)/ +PKG_HASH:=757f18963a2d5c178a48c3f7e1657b83003f0673e84aba01ddff2c61dfbc8b32 -PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/AirConnect-$(PKG_SOURCE_VERSION) +PKG_BUILD_DIR:=$(BUILD_DIR)/airconnect-$(PKG_VERSION) -PKG_MAINTAINER:=jjm2473 +PKG_LICENSE:=MIT +PKG_LICENSE_FILE:=LICENSE +PKG_MAINTAINER:=sbwml include $(INCLUDE_DIR)/package.mk @@ -31,14 +30,15 @@ define Package/$(PKG_NAME) endef define Package/$(PKG_NAME)/description - Use AirPlay to stream to UPnP/Sonos & Chromecast devices + Use AirPlay to stream to UPnP/Sonos & Chromecast devices endef define Package/$(PKG_NAME)/conffiles /etc/config/airconnect endef -define Build/Configure +define Build/Prepare + unzip -q -d $(PKG_BUILD_DIR) $(DL_DIR)/AirConnect-$(PKG_VERSION).zip endef define Build/Compile @@ -46,10 +46,10 @@ endef define Package/$(PKG_NAME)/install $(INSTALL_DIR) $(1)/usr/bin $(1)/etc/config $(1)/etc/init.d - $(INSTALL_BIN) $(PKG_BUILD_DIR)/bin/aircast-linux-$(ARCH)-static $(1)/usr/bin/aircast - $(INSTALL_BIN) $(PKG_BUILD_DIR)/bin/airupnp-linux-$(ARCH)-static $(1)/usr/bin/airupnp - $(INSTALL_CONF) ./files/airconnect.config $(1)/etc/config/airconnect $(INSTALL_BIN) ./files/airconnect.init $(1)/etc/init.d/airconnect + $(INSTALL_CONF) ./files/airconnect.config $(1)/etc/config/airconnect + $(INSTALL_BIN) $(PKG_BUILD_DIR)/aircast-linux-$(ARCH)-static $(1)/usr/bin/aircast + $(INSTALL_BIN) $(PKG_BUILD_DIR)/airupnp-linux-$(ARCH)-static $(1)/usr/bin/airupnp endef -$(eval $(call BuildPackage,airconnect)) +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/airconnect/files/airconnect.config b/airconnect/files/airconnect.config index 310c0017f..8a297672f 100644 --- a/airconnect/files/airconnect.config +++ b/airconnect/files/airconnect.config @@ -1,5 +1,7 @@ -config main + +config airconnect 'config' option 'enabled' '0' - option 'interface' 'lan' - option 'aircast' '1' - option 'airupnp' '1' + option 'interface' 'br-lan' + option 'aircast' '1' + option 'airupnp' '1' + diff --git a/airconnect/files/airconnect.init b/airconnect/files/airconnect.init old mode 100755 new mode 100644 index ee30d2224..25d699a9c --- a/airconnect/files/airconnect.init +++ b/airconnect/files/airconnect.init @@ -1,46 +1,55 @@ #!/bin/sh /etc/rc.common START=99 -STOP=01 USE_PROCD=1 +PROG_AIRCAST=/usr/bin/aircast +PROG_AIRUPNP=/usr/bin/airupnp -service_triggers() { - procd_add_reload_trigger "airconnect" -} - -main_config() { - config_get AIRUPNP_IFACE "$1" interface "lan" - config_get_bool ENABLE_MAIN "$1" enabled 0 - config_get_bool ENABLE_AIRCAST "$1" aircast 0 - config_get_bool ENABLE_AIRUPNP "$1" airupnp 0 +get_config() { + config_get enabled $1 enabled "0" + config_get interface $1 interface "br-lan" + config_get aircast $1 aircast "1" + config_get airupnp $1 airupnp "1" } start_service() { - config_load airconnect - config_foreach main_config main - [ "$ENABLE_MAIN" = 0 ] && return 0 - [ "$ENABLE_AIRCAST" = 0 -a "$ENABLE_AIRUPNP" = 0 ] && return 0 + config_load "airconnect" + config_foreach get_config "airconnect" + [ $enabled -eq 0 ] && return 0 - local interface=$( - . /lib/functions/network.sh + # UPnP/Sonos + if [ "$airupnp" -eq 1 ]; then + procd_open_instance airupnp + procd_set_param command $PROG_AIRUPNP + procd_append_param command -l 1000:2000 + procd_append_param command -Z + procd_append_param command -b $interface + procd_set_param stdout 0 + procd_set_param stderr 0 + procd_set_param respawn + procd_close_instance airupnp + fi - network_is_up "$AIRUPNP_IFACE" || exit 0 - network_get_device device "$AIRUPNP_IFACE" - printf "%s" "${device:-$AIRUPNP_IFACE}" - ) - [ -z "$interface" ] && interface=br-lan - local common_args="-Z -b $interface" + # Chromecast + if [ "$aircast" -eq 1 ]; then + procd_open_instance aircast + procd_set_param command $PROG_AIRCAST + procd_append_param command -l 1000:2000 + procd_append_param command -Z + procd_append_param command -b $interface + procd_set_param stdout 0 + procd_set_param stderr 0 + procd_set_param respawn + procd_close_instance aircast + fi +} - if [ "$ENABLE_AIRUPNP" = 1 ]; then - procd_open_instance - procd_set_param command /usr/bin/airupnp $common_args - procd_set_param respawn - procd_close_instance - fi - if [ "$ENABLE_AIRCAST" = 1 ]; then - procd_open_instance - procd_set_param command /usr/bin/aircast $common_args - procd_set_param respawn - procd_close_instance - fi -} \ No newline at end of file +service_triggers() { + procd_add_reload_trigger "airconnect" +} + +reload_service() { + stop + sleep 1 + start +} diff --git a/daed-next/Makefile b/daed-next/Makefile new file mode 100644 index 000000000..bf2a0a9b8 --- /dev/null +++ b/daed-next/Makefile @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Copyright (C) 2023 ImmortalWrt.org + +include $(TOPDIR)/rules.mk + +PKG_NAME:=daed-next +PKG_VERSION:=2023-12-02-102cb56 +CORE_VERSION:=core-$(shell date "+%Y-%m-%d") +PKG_RELEASE:=1 + +PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz +PKG_SOURCE_PROTO:=git +PKG_SOURCE_VERSION:=102cb562b6962b44c47f38092284d2ab2506a702 +PKG_SOURCE_URL:=https://github.com/daeuniverse/daed-revived-next.git +PKG_MIRROR_HASH:=skip + +PKG_BUILD_DIR=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/wing +PKG_BUILD_DEPENDS:=golang/host bpf-headers +PKG_BUILD_PARALLEL:=1 +PKG_USE_MIPS16:=0 +PKG_BUILD_FLAGS:=no-mips16 + +GO_PKG:=github.com/daeuniverse/dae-wing +GO_PKG_LDFLAGS:= \ + -X '$(GO_PKG)/db.AppDescription=$(PKG_NAME) is a integration solution of dae, API and UI.' +GO_PKG_LDFLAGS_X:= \ + $(GO_PKG)/db.AppName=$(PKG_NAME) \ + $(GO_PKG)/db.AppVersion=$(PKG_VERSION)-$(CORE_VERSION) +GO_PKG_GCFLAGS:=-l=4 + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/bpf.mk +include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk + +define Package/daed-next/Default + SECTION:=net + CATEGORY:=Network + SUBMENU:=Web Servers/Proxies + URL:=https://github.com/daeuniverse/daed-revived-next +endef + +define Package/daed-next + $(call Package/daed-next/Default) + TITLE:=daed-next is a backend of dae + # You need enable KERNEL_DEBUG_INFO_BTF and KERNEL_BPF_EVENTS + DEPENDS:=$(GO_ARCH_DEPENDS) $(BPF_DEPENDS) \ + +ca-bundle +kmod-sched-core +kmod-sched-bpf +kmod-xdp-sockets-diag +kmod-veth \ + +node +v2ray-geoip +v2ray-geosite +zoneinfo-asia +endef + +define Package/daed-next/description + daed-next is a backend of dae, provides a method to bundle arbitrary + frontend, dae and geodata into one binary. +endef + +define Package/daed-next/conffiles +/etc/daed-next/wing.db +/etc/config/daed-next +endef + +define Download/daed-next + URL:=https://github.com/QiuSimons/luci-app-daed-next/releases/download/daed-web-$(PKG_VERSION)/ + URL_FILE:=daed-web-$(PKG_VERSION).squashfs + FILE:=daed-web-$(PKG_VERSION).squashfs + HASH:=skip +endef + +define Build/Prepare + ( \ + $(TAR) --strip-components=1 -C $(PKG_BUILD_DIR)/../ -xzf $(DL_DIR)/$(PKG_NAME)-$(PKG_VERSION).tar.gz ; \ + rm -rf $(PKG_BUILD_DIR) && git clone https://github.com/daeuniverse/dae-wing $(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/wing ; \ + ) +endef + +DAE_CFLAGS:= \ + -O2 -Wall -Werror \ + -DMAX_MATCH_SET_LEN=64 \ + -I$(BPF_HEADERS_DIR)/tools/lib \ + -I$(BPF_HEADERS_DIR)/arch/$(BPF_KARCH)/include/asm/mach-generic + +ifneq ($(CONFIG_USE_MUSL),) + TARGET_CFLAGS += -D_LARGEFILE64_SOURCE +endif + +define Build/Compile + ( \ + pushd $(PKG_BUILD_DIR) ; \ + rm -rf dae-core && git clone https://github.com/daeuniverse/dae dae-core ; \ + rm -rf dae-core/control/kern/headers && git clone https://github.com/daeuniverse/dae_bpf_headers dae-core/control/kern/headers ; \ + go mod tidy ; \ + $(MAKE) deps ; \ + $(GO_GENERAL_BUILD_CONFIG_VARS) \ + $(GO_PKG_BUILD_CONFIG_VARS) \ + $(GO_PKG_BUILD_VARS) \ + go generate ./... ; \ + cd dae-core ; \ + go mod tidy ; \ + $(GO_GENERAL_BUILD_CONFIG_VARS) \ + $(GO_PKG_BUILD_CONFIG_VARS) \ + $(GO_PKG_BUILD_VARS) \ + BPF_CLANG="$(CLANG)" \ + BPF_STRIP_FLAG="-strip=$(LLVM_STRIP)" \ + BPF_CFLAGS="$(DAE_CFLAGS)" \ + BPF_TARGET="bpfel,bpfeb" \ + go generate control/control.go ; \ + popd ; \ + $(call GoPackage/Build/Compile) ; \ + ) +endef + +define Package/daed-next/install + $(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR)) + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/dae-wing $(1)/usr/bin/dae-wing + + $(INSTALL_DIR) $(1)/usr/share/daed-next + $(INSTALL_DATA) $(DL_DIR)/daed-web-$(PKG_VERSION).squashfs $(1)/usr/share/daed-next/daed-web.squashfs + + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) $(CURDIR)/files/daed-next.config $(1)/etc/config/daed-next + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) $(CURDIR)/files/daed-next.init $(1)/etc/init.d/daed-next +endef + +$(eval $(call Download,daed-next)) +$(eval $(call GoBinPackage,daed-next)) +$(eval $(call BuildPackage,daed-next)) diff --git a/daed-next/files/daed-next.config b/daed-next/files/daed-next.config new file mode 100644 index 000000000..de287cd1a --- /dev/null +++ b/daed-next/files/daed-next.config @@ -0,0 +1,8 @@ + +config daed-next 'config' + option enabled '0' + option listen_port '3000' + option log_enabled '0' + option log_maxbackups '1' + option log_maxsize '1' + diff --git a/daed-next/files/daed-next.init b/daed-next/files/daed-next.init new file mode 100755 index 000000000..2212ec376 --- /dev/null +++ b/daed-next/files/daed-next.init @@ -0,0 +1,59 @@ +#!/bin/sh /etc/rc.common + +START=99 +USE_PROCD=1 +NODE_BIN=/usr/bin/node +WING_BIN=/usr/bin/dae-wing +WEB_SQUASHFS=/usr/share/daed-next/daed-web.squashfs +GEODATA=/usr/share/v2ray + +get_config() { + config_get_bool enabled $1 enabled "0" + config_get listen_port $1 listen_port "3000" + config_get log_enabled $1 log_enabled "0" + config_get log_maxbackups $1 log_maxbackups "1" + config_get log_maxsize $1 log_maxsize "1" +} + +start_service() { + config_load "daed-next" + config_foreach get_config "daed-next" + + [ "$enabled" -eq "1" ] || return 1 + [ "$log_enabled" -eq "1" ] && log_path="/var/log/daed-next/daed-next.log" || log_path="/dev/null" + mkdir -p /var/log/daed-next + true > /var/log/daed-next/daed-next.log + + # dae-wing + procd_open_instance dae-wing + procd_set_param env DAE_LOCATION_ASSET=$GEODATA + procd_set_param command $WING_BIN run + procd_append_param command --config "/etc/daed-next/" + procd_append_param command --logfile "$log_path" + procd_append_param command --logfile-maxbackups "$log_maxbackups" + procd_append_param command --logfile-maxsize "$log_maxsize" + procd_set_param limits core="unlimited" + procd_set_param limits nofile="1000000 1000000" + procd_set_param stdout 0 + procd_set_param stderr 0 + procd_set_param respawn 300 5 10 + procd_close_instance dae-wing +} + +service_triggers() { + procd_add_reload_trigger "daed-next" +} + +stop_service() { + sleep 2 + mount_points=$(mount | grep '/tmp/daed-next' | awk '{print $3}') + for mp in $mount_points; do + umount -l "$mp" || { echo "Failed to force unmount $mp"; } + done +} + +reload_service() { + stop + sleep 2 + start +} diff --git a/luci-app-airconnect/Makefile b/luci-app-airconnect/Makefile index e5a217680..22da016af 100644 --- a/luci-app-airconnect/Makefile +++ b/luci-app-airconnect/Makefile @@ -1,15 +1,20 @@ - +# Copyright (C) 2016 Openwrt.org +# +# This is free software, licensed under the Apache License, Version 2.0 . +# include $(TOPDIR)/rules.mk -PKG_VERSION:=1.0.0-20221219 -PKG_RELEASE:= +PKG_NAME:=luci-app-airconnect +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 -LUCI_TITLE:=LuCI support for airconnect -LUCI_PKGARCH:=all +LUCI_TITLE:=LuCI support for AirConnect LUCI_DEPENDS:=+airconnect +LUCI_PKGARCH:=all + +PKG_MAINTAINER:=sbwml include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature - diff --git a/luci-app-airconnect/luasrc/controller/airconnect.lua b/luci-app-airconnect/luasrc/controller/airconnect.lua index f6bc4d9e3..a9f3311bc 100644 --- a/luci-app-airconnect/luasrc/controller/airconnect.lua +++ b/luci-app-airconnect/luasrc/controller/airconnect.lua @@ -1,7 +1,20 @@ - module("luci.controller.airconnect", package.seeall) function index() - entry({"admin", "services", "airconnect"}, alias("admin", "services", "airconnect", "config"), _("AirConnect"), 90).dependent = true - entry({"admin", "services", "airconnect", "config"}, cbi("airconnect")) + if not nixio.fs.access("/etc/config/airconnect") then + return + end + + local page = entry({"admin", "services", "airconnect"}, cbi("airconnect"), _("AirConnect")) + page.dependent = true + page.acl_depends = { "luci-app-airconnect" } + + entry({"admin", "services", "airconnect", "status"}, call("act_status")).leaf = true +end + +function act_status() + local e = {} + e.running = luci.sys.call("pgrep aircast >/dev/null") == 0 or luci.sys.call("pgrep airupnp >/dev/null") == 0 + luci.http.prepare_content("application/json") + luci.http.write_json(e) end diff --git a/luci-app-airconnect/luasrc/model/cbi/airconnect.lua b/luci-app-airconnect/luasrc/model/cbi/airconnect.lua index 5bbbda7cb..cc0e0c2f3 100644 --- a/luci-app-airconnect/luasrc/model/cbi/airconnect.lua +++ b/luci-app-airconnect/luasrc/model/cbi/airconnect.lua @@ -1,31 +1,30 @@ ---[[ -LuCI - Lua Configuration Interface -]]-- +local i = require 'luci.sys' +local m, e -local m, s, o +m = Map('airconnect', translate('AirConnect')) +m.description = translate('Send audio to UPnP/Sonos/Chromecast players using AirPlay.') -m = Map("airconnect", translate("AirConnect"), translate("Use AirPlay to stream to UPnP/Sonos & Chromecast devices")) +m:section(SimpleSection).template = 'airconnect/airconnect_status' -s = m:section(TypedSection, "main", translate("Global Settings")) -s.addremove=false -s.anonymous=true +e = m:section(TypedSection, 'airconnect') +e.addremove = false +e.anonymous = true -o = s:option(Flag, "enabled", translate("Enable")) -o.default = 0 +o = e:option(Flag, 'enabled', translate('Enabled')) o.rmempty = false -o = s:option(Value, "interface", translate("Interface"), translate("Network interface for serving, usually LAN")) -o.template = "cbi/network_netlist" -o.nocreate = true -o.default = "lan" -o.datatype = "string" - -o = s:option(Flag, "aircast", translate("Supports Chromecast"), translate("Select this if you have Chromecast devices")) -o.default = 1 +o = e:option(Value, 'interface', translate('Bind interface')) +for t, e in ipairs(i.net.devices()) do + if e ~= 'lo' and not string.match(e, '^docker.*$') and not string.match(e, '^sit.*$') and not string.match(e, '^dummy.*$') and not string.match(e, '^teql.*$') and not string.match(e, '^veth.*$') and not string.match(e, '^ztly.*$') then + o:value(e) + end +end o.rmempty = false -o = s:option(Flag, "airupnp", translate("Supports UPnP/Sonos"), translate("Select this if you have UPnP/Sonos devices")) -o.default = 1 +o = e:option(Flag, 'airupnp', translate('UPnP/Sonos'), translate('Enable UPnP/Sonos Device Support')) +o.rmempty = false + +o = e:option(Flag, 'aircast', translate('Chromecast'), translate('Enable Chromecast Device Support')) o.rmempty = false return m diff --git a/luci-app-airconnect/luasrc/view/airconnect/airconnect_status.htm b/luci-app-airconnect/luasrc/view/airconnect/airconnect_status.htm new file mode 100644 index 000000000..bd8e34679 --- /dev/null +++ b/luci-app-airconnect/luasrc/view/airconnect/airconnect_status.htm @@ -0,0 +1,23 @@ + + +
+

+ <%:Collecting data...%> +

+
diff --git a/luci-app-airconnect/po/zh-cn b/luci-app-airconnect/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-airconnect/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-airconnect/po/zh-cn/airconnect.po b/luci-app-airconnect/po/zh-cn/airconnect.po deleted file mode 100644 index 2fee709d9..000000000 --- a/luci-app-airconnect/po/zh-cn/airconnect.po +++ /dev/null @@ -1,23 +0,0 @@ -msgid "" -msgstr "Content-Type: text/plain; charset=UTF-8" - -msgid "Use AirPlay to stream to UPnP/Sonos & Chromecast devices" -msgstr "AirConnect 让 UPnP/Sonos 和 Chromecast 设备支持 AirPlay 音频串流" - -msgid "Global Settings" -msgstr "全局设置" - -msgid "Network interface for serving, usually LAN" -msgstr "提供服务的网络接口,通常是 LAN 口" - -msgid "Supports Chromecast" -msgstr "支持 Chromecast 设备" - -msgid "Supports UPnP/Sonos" -msgstr "支持 UPnP/Sonos 设备" - -msgid "Select this if you have Chromecast devices" -msgstr "如果你有 Chromecast 设备就选中这个" - -msgid "Select this if you have UPnP/Sonos devices" -msgstr "如果你有 UPnP/Sonos 设备就选中这个" diff --git a/luci-app-airconnect/po/zh_Hans b/luci-app-airconnect/po/zh_Hans deleted file mode 120000 index 41451e4a1..000000000 --- a/luci-app-airconnect/po/zh_Hans +++ /dev/null @@ -1 +0,0 @@ -zh-cn \ No newline at end of file diff --git a/luci-app-airconnect/po/zh_Hans/airconnect.po b/luci-app-airconnect/po/zh_Hans/airconnect.po new file mode 100644 index 000000000..d71e297c4 --- /dev/null +++ b/luci-app-airconnect/po/zh_Hans/airconnect.po @@ -0,0 +1,26 @@ +msgid "AirConnect" +msgstr "隔空播放" + +msgid "Send audio to UPnP/Sonos/Chromecast players using AirPlay." +msgstr "通过 AirPlay 将音频流传输到 UPnP/Sonos 和 Chromecast 设备。" + +msgid "RUNNING" +msgstr "运行中" + +msgid "NOT RUNNING" +msgstr "未运行" + +msgid "Collecting data..." +msgstr "获取数据中..." + +msgid "Enabled" +msgstr "启用" + +msgid "Bind interface" +msgstr "绑定接口" + +msgid "Enable UPnP/Sonos Device Support" +msgstr "启用 UPnP/Sonos 设备支持" + +msgid "Enable Chromecast Device Support" +msgstr "启用 Chromecast 设备支持" diff --git a/luci-app-airconnect/root/etc/uci-defaults/luci-airconnect b/luci-app-airconnect/root/etc/uci-defaults/luci-airconnect new file mode 100755 index 000000000..e3d54c159 --- /dev/null +++ b/luci-app-airconnect/root/etc/uci-defaults/luci-airconnect @@ -0,0 +1,11 @@ +#!/bin/sh + +uci -q batch <<-EOF >/dev/null + delete ucitrack.@airconnect[-1] + add ucitrack airconnect + set ucitrack.@airconnect[-1].init=airconnect + commit ucitrack +EOF + +rm -rf /tmp/luci-indexcache* +exit 0 diff --git a/luci-app-airconnect/root/usr/share/rpcd/acl.d/luci-app-airconnect.json b/luci-app-airconnect/root/usr/share/rpcd/acl.d/luci-app-airconnect.json new file mode 100644 index 000000000..5dfd17e61 --- /dev/null +++ b/luci-app-airconnect/root/usr/share/rpcd/acl.d/luci-app-airconnect.json @@ -0,0 +1,11 @@ +{ + "luci-app-airconnect": { + "description": "Grant UCI access for luci-app-airconnect", + "read": { + "uci": [ "airconnect" ] + }, + "write": { + "uci": [ "airconnect" ] + } + } +} diff --git a/luci-app-daed-next/Makefile b/luci-app-daed-next/Makefile new file mode 100644 index 000000000..862db4123 --- /dev/null +++ b/luci-app-daed-next/Makefile @@ -0,0 +1,18 @@ +# Copyright (C) 2016 Openwrt.org +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-daed-next +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +LUCI_TITLE:=LuCI support for daed-next +LUCI_PKGARCH:=all +LUCI_DEPENDS:=+daed-next +bash +luci-compat + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-daed-next/luasrc/controller/daed-next.lua b/luci-app-daed-next/luasrc/controller/daed-next.lua new file mode 100644 index 000000000..5b3d31857 --- /dev/null +++ b/luci-app-daed-next/luasrc/controller/daed-next.lua @@ -0,0 +1,36 @@ +local sys = require "luci.sys" +local http = require "luci.http" + +module("luci.controller.daed-next", package.seeall) + +function index() + if not nixio.fs.access("/etc/config/daed-next") then + return + end + + local page = entry({"admin", "services", "daed-next"}, alias("admin", "services", "daed-next", "basic"), _("DAED Next"), -1) + page.dependent = true + page.acl_depends = { "luci-app-daed-next" } + + entry({"admin", "services", "daed-next", "basic"}, cbi("daed-next/basic"), _("Basic Setting"), 1).leaf = true + entry({"admin", "services", "daed-next", "dashboard"}, template("daed-next/dashboard"), _("Dashboard"), 2).leaf = true + entry({"admin", "services", "daed-next", "log"}, cbi("daed-next/log"), _("Logs"), 3).leaf = true + entry({"admin", "services", "daed-next", "status"}, call("act_status")).leaf = true + entry({"admin", "services", "daed-next", "get_log"}, call("get_log")).leaf = true + entry({"admin", "services", "daed-next", "clear_log"}, call("clear_log")).leaf = true +end + +function act_status() + local e = {} + e.running = sys.call("pgrep -x /usr/bin/dae-wing >/dev/null") == 0 + http.prepare_content("application/json") + http.write_json(e) +end + +function get_log() + http.write(sys.exec("cat /var/log/daed-next/daed-next.log")) +end + +function clear_log() + sys.call("true > /var/log/daed-next/daed-next.log") +end diff --git a/luci-app-daed-next/luasrc/model/cbi/daed-next/basic.lua b/luci-app-daed-next/luasrc/model/cbi/daed-next/basic.lua new file mode 100644 index 000000000..c1395db48 --- /dev/null +++ b/luci-app-daed-next/luasrc/model/cbi/daed-next/basic.lua @@ -0,0 +1,76 @@ +local m, s + +m = Map("daed-next", translate("DAE Dashboard")) +m.description = translate("A Linux high-performance transparent proxy solution based on eBPF") + +m:section(SimpleSection).template = "daed-next/daed-next_status" + +s = m:section(TypedSection, "daed-next") +s.addremove = false +s.anonymous = true + +if nixio.fs.stat("/sys/fs/bpf","type") ~= "dir" then + s.rawhtml = true + s.template = "daed-next/daed-next_error" +end + +o = s:option(Flag, "enabled", translate("Enabled")) +o.rmempty = false + +o = s:option(Button, "Dashboard Toggle", translate("Dashboard Toggle")) +o.inputtitle = translate("Toggle") +o.inputstyle = "reload" +o.description = translate("Dashboard is a frontend management panel, meant for configuration use only.") +o.write = function() + luci.sys.exec("/etc/daed-next/dashboard.sh &> /dev/null &") +end + +enable = s:option(Flag, "subscribe_auto_update", translate("Enable Auto Subscribe Update")) +enable.rmempty = false + +o = s:option(Value, "daed_username", translate("Username")) +o.default = Username +o.password = true +o:depends('subscribe_auto_update', '1') + +o = s:option(Value, "daed_password", translate("Password")) +o.default = Password +o.password = true +o:depends('subscribe_auto_update', '1') + +o = s:option(ListValue, "subscribe_update_week_time", translate("Update Cycle")) +o:value("*", translate("Every Day")) +o:value("1", translate("Every Monday")) +o:value("2", translate("Every Tuesday")) +o:value("3", translate("Every Wednesday")) +o:value("4", translate("Every Thursday")) +o:value("5", translate("Every Friday")) +o:value("6", translate("Every Saturday")) +o:value("7", translate("Every Sunday")) +o.default = "*" +o:depends('subscribe_auto_update', '1') + +update_time = s:option(ListValue, "subscribe_update_day_time", translate("Update Time (Every Day)")) +for t = 0, 23 do + update_time:value(t, t..":00") +end +update_time.default = 0 +update_time:depends('subscribe_auto_update', '1') + +o = s:option(Value, "listen_port", translate("Web Listen port")) +o.datatype = "and(port,min(1))" +o.default = 3000 + +o = s:option(Flag, "log_enabled", translate("Enable Logs")) +o.default = 0 +o.rmempty = false + +o = s:option(Value, "log_maxbackups", translate("Logfile retention count")) +o.default = 1 +o:depends("log_enabled", "1") + +o = s:option(Value, "log_maxsize", translate("Logfile Max Size (MB)")) +o.default = 1 +o:depends("log_enabled", "1") + +return m diff --git a/luci-app-daed-next/luasrc/model/cbi/daed-next/log.lua b/luci-app-daed-next/luasrc/model/cbi/daed-next/log.lua new file mode 100644 index 000000000..e59778422 --- /dev/null +++ b/luci-app-daed-next/luasrc/model/cbi/daed-next/log.lua @@ -0,0 +1,5 @@ +m = Map("daed-next") + +m:append(Template("daed-next/daed-next_log")) + +return m diff --git a/luci-app-daed-next/luasrc/view/daed-next/daed-next_error.htm b/luci-app-daed-next/luasrc/view/daed-next/daed-next_error.htm new file mode 100644 index 000000000..85de62715 --- /dev/null +++ b/luci-app-daed-next/luasrc/view/daed-next/daed-next_error.htm @@ -0,0 +1,6 @@ + + + diff --git a/luci-app-daed-next/luasrc/view/daed-next/daed-next_log.htm b/luci-app-daed-next/luasrc/view/daed-next/daed-next_log.htm new file mode 100644 index 000000000..e661abe33 --- /dev/null +++ b/luci-app-daed-next/luasrc/view/daed-next/daed-next_log.htm @@ -0,0 +1,33 @@ + +
+ + +
diff --git a/luci-app-daed-next/luasrc/view/daed-next/daed-next_status.htm b/luci-app-daed-next/luasrc/view/daed-next/daed-next_status.htm new file mode 100644 index 000000000..fcfcec576 --- /dev/null +++ b/luci-app-daed-next/luasrc/view/daed-next/daed-next_status.htm @@ -0,0 +1,46 @@ + + + + + +
+

+ <%:Collecting data...%> +

+

+ <%:Collecting data...%> +

+
diff --git a/luci-app-daed-next/luasrc/view/daed-next/dashboard.htm b/luci-app-daed-next/luasrc/view/daed-next/dashboard.htm new file mode 100644 index 000000000..b00354765 --- /dev/null +++ b/luci-app-daed-next/luasrc/view/daed-next/dashboard.htm @@ -0,0 +1,27 @@ +<%+header%> +<% + local running = luci.sys.exec("pgrep -x /usr/bin/dae-wing") + local dashrunning = luci.sys.exec("[ -e '/tmp/log/daed-next/dashboard.pid' ] && echo '1' || echo 'x'") +%> +<% if tonumber(running) ~= nil and tonumber(dashrunning) ~= nil then %> +
+ +
+ +<% else %> + +
+ +

<%:dae-wing or dashboard is not running%>

+

<%:Please start the dae-wing service and the dashboard service first and try again%>

+
+<% end -%> + +<%+footer%> \ No newline at end of file diff --git a/luci-app-daed-next/po/zh-cn b/luci-app-daed-next/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-daed-next/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-daed-next/po/zh_Hans/daed-next.po b/luci-app-daed-next/po/zh_Hans/daed-next.po new file mode 100644 index 000000000..ff04a0f78 --- /dev/null +++ b/luci-app-daed-next/po/zh_Hans/daed-next.po @@ -0,0 +1,86 @@ +msgid "DAED Next" +msgstr "" + +msgid "DAE Dashboard" +msgstr "DAE 仪表板" + +msgid "A Linux high-performance transparent proxy solution based on eBPF" +msgstr "一个基于 eBPF 的 Linux 高性能透明代理解决方案" + +msgid "Enabled" +msgstr "启用" + +msgid "Web Listen port" +msgstr "Web 监听端口" + +msgid "Enable Logs" +msgstr "启用日志" + +msgid "Logfile retention count" +msgstr "日志文件保留数量" + +msgid "Logfile Max Size (MB)" +msgstr "日志文件大小(MB)" + +msgid "Logs" +msgstr "日志" + +msgid "Clear logs" +msgstr "清空日志" + +msgid "Basic Setting" +msgstr "基本设置" + +msgid "RUNNING" +msgstr "运行中" + +msgid "NOT RUNNING" +msgstr "未运行" + +msgid "Collecting data..." +msgstr "收集数据..." + +msgid "Open Web Interface" +msgstr "打开 Web 界面" + +msgid "The kernel does not support eBPF" +msgstr "内核不支持 eBPF" + +msgid "Dashboard" +msgstr "仪表板" + +msgid "DAED Next is not running" +msgstr "DAED Next 未运行" + +msgid "Please start the DAED Next service first and try again" +msgstr "请先启动 DAED Next 服务后重试" + +msgid "dae-wing or dashboard is not running" +msgstr "DAE-WING 或 仪表板 未运行" + +msgid "Please start the dae-wing service and the dashboard service first and try again" +msgstr "请先启动 DAE-WING 和 仪表板 服务后重试" + +msgid "Enable Auto Subscribe Update" +msgstr "启用订阅自动更新" + +msgid "Update Cycle" +msgstr "更新周期" + +msgid "Update Time (Every Day)" +msgstr "更新时间(每天)" + +msgid "Username" +msgstr "用户名" + +msgid "Password" +msgstr "密码" + +msgid "Dashboard Toggle" +msgstr "仪表板开关" + +msgid "Toggle" +msgstr "开/关" + +msgid "Dashboard is a frontend management panel, meant for configuration use only." +msgstr "仪表板是一个前端管理面板,仅应在配置过程中启用。" diff --git a/luci-app-daed-next/root/etc/daed-next/daed-next_sub.sh b/luci-app-daed-next/root/etc/daed-next/daed-next_sub.sh new file mode 100755 index 000000000..4b8a03fda --- /dev/null +++ b/luci-app-daed-next/root/etc/daed-next/daed-next_sub.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +USERNAME=$(uci -q get daed-next.config.daed_username) +PASSWORD=$(uci -q get daed-next.config.daed_password) +PORT=2023 +GRAPHQL_URL="http://127.0.0.1:"$PORT"/graphql" +CRON_FILE="/etc/crontabs/root" +RANDOM_SEED=$RANDOM +RANDOM_NUM=$((RANDOM_SEED % 10 + 1)) + +login() { + LOGIN=$(curl -s -X POST -H "Content-Type: application/json" -d '{"query":"query Token($username: String!, $password: String!) {\n token(username: $username, password: $password)\n}","variables":{"username":"'"$USERNAME"'","password":"'"$PASSWORD"'"}}' $GRAPHQL_URL) + JSON=${LOGIN#\"} + JSON=${LOGIN%\"} + TOKEN=$(echo $JSON | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') +} + +update_subscription() { + SUBSCRIPTION_ID_LIST=$(curl -s -X POST -H "Authorization: $TOKEN" -d '{"query": "query Subscriptions {\n subscriptions {\nid\ntag\nstatus\nlink\ninfo\nupdatedAt\nnodes {\nedges {\nid\nname\nprotocol\nlink\n}\n}\n}\n}", "operationName": "Subscriptions"}' $GRAPHQL_URL | grep -o '"id":"[^"]*","tag"' | grep -o 'id":"[^"]*' | grep -o '[^"]*$') + echo "$SUBSCRIPTION_ID_LIST" | while read -r id; do + curl -X POST -H "Authorization: $TOKEN" -d '{"query":"mutation UpdateSubscription($id: ID!) {\n updateSubscription(id: $id) {\n id\n }\n}","variables":{"id":"'"$id"'"},"operationName":"UpdateSubscription"}' $GRAPHQL_URL + done +} + +reload() { + curl -X POST -H "Authorization: $TOKEN" -d '{"query":"mutation Run($dry: Boolean!) {\n run(dry: $dry)\n}","variables":{"dry":false},"operationName":"Run"}' $GRAPHQL_URL +} + +resetcron() { + touch $CRON_FILE + sed -i '/daed-next_sub.sh/d' $CRON_FILE 2>/dev/null + [ "$(uci -q get daed-next.config.subscribe_auto_update)" -eq 1 ] && echo "${RANDOM_NUM} $(uci -q get daed-next.config.subscribe_update_day_time) * * $(uci -q get daed-next.config.subscribe_update_week_time) /etc/daed-next/daed-next_sub.sh >/dev/null 2>&1" >>$CRON_FILE + crontab $CRON_FILE +} + +login && update_subscription && reload && resetcron \ No newline at end of file diff --git a/luci-app-daed-next/root/etc/daed-next/dashboard.sh b/luci-app-daed-next/root/etc/daed-next/dashboard.sh new file mode 100755 index 000000000..076f7c777 --- /dev/null +++ b/luci-app-daed-next/root/etc/daed-next/dashboard.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +NODE_BIN=/usr/bin/node +WEB_SQUASHFS=/usr/share/daed-next/daed-web.squashfs +PID_FILE=/tmp/log/daed-next/dashboard.pid +DAED_NEXT_DIR=/var/daed-next + +start_server() { + listen_port=$(uci -q get daed-next.config.listen_port) + if [ ! -d "$DAED_NEXT_DIR" ]; then + mkdir -p "$DAED_NEXT_DIR" + fi + mount -t squashfs "$WEB_SQUASHFS" "$DAED_NEXT_DIR" || { echo "Mount failed"; } + + ARGS="PORT=$listen_port HOSTNAME=0.0.0.0" + /bin/sh -c "$ARGS $NODE_BIN $DAED_NEXT_DIR/server.js" & + echo $! > "$PID_FILE" +} + +stop_server() { + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + kill "$PID" || { echo "Failed to kill process $PID"; } + rm -f "$PID_FILE" + + mount_points=$(mount | grep '/tmp/daed-next' | awk '{print $3}') + for mp in $mount_points; do + umount -l "$mp" || { echo "Failed to force unmount $mp"; } + done + fi +} + +if [ -e "$PID_FILE" ]; then + stop_server +else + start_server +fi diff --git a/luci-app-daed-next/root/etc/init.d/luci_daed-next b/luci-app-daed-next/root/etc/init.d/luci_daed-next new file mode 100755 index 000000000..585c61a73 --- /dev/null +++ b/luci-app-daed-next/root/etc/init.d/luci_daed-next @@ -0,0 +1,44 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2023 Tianling Shen + +USE_PROCD=0 +START=99 + +CONF="daed-next" +PROG="/usr/bin/dae-wing" +LOG="/var/log/daed-next/daed-next.log" +CRON_FILE="/etc/crontabs/root" +RANDOM_SEED=$RANDOM +RANDOM_NUM=$((RANDOM_SEED % 10 + 1)) + +setcron() { + touch $CRON_FILE + sed -i '/daed-next_sub.sh/d' $CRON_FILE 2>/dev/null + [ "$(uci -q get daed-next.config.subscribe_auto_update)" -eq 1 ] && echo "${RANDOM_NUM} $(uci -q get daed-next.config.subscribe_update_day_time) * * $(uci -q get daed-next.config.subscribe_update_week_time) /etc/daed-next/daed-next_sub.sh >/dev/null 2>&1" >>$CRON_FILE + crontab $CRON_FILE +} + +delcron() { + sed -i '/daed-next_sub.sh/d' $CRON_FILE 2>/dev/null + crontab $CRON_FILE +} + +start_service() { + config_load "$CONF" + + local enabled + config_get_bool enabled "config" "enabled" "0" + if [ "$enabled" -eq 0 ]; then + delcron + return 1 + fi + setcron +} + +stop_service() { + delcron +} + +service_triggers() { + procd_add_reload_trigger "$CONF" +} diff --git a/luci-app-daed-next/root/etc/uci-defaults/luci-daed-next b/luci-app-daed-next/root/etc/uci-defaults/luci-daed-next new file mode 100755 index 000000000..97e1f8d3c --- /dev/null +++ b/luci-app-daed-next/root/etc/uci-defaults/luci-daed-next @@ -0,0 +1,5 @@ +#!/bin/sh + +rm -rf /tmp/luci-* + +exit 0 diff --git a/luci-app-daed-next/root/usr/share/rpcd/acl.d/luci-app-daed-next.json b/luci-app-daed-next/root/usr/share/rpcd/acl.d/luci-app-daed-next.json new file mode 100644 index 000000000..848c286e1 --- /dev/null +++ b/luci-app-daed-next/root/usr/share/rpcd/acl.d/luci-app-daed-next.json @@ -0,0 +1,11 @@ +{ + "luci-app-daed-next": { + "description": "Grant UCI access for luci-app-daed-next", + "read": { + "uci": [ "daed-next" ] + }, + "write": { + "uci": [ "daed-next" ] + } + } +} diff --git a/luci-app-internet-detector/LICENSE b/luci-app-internet-detector/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/luci-app-internet-detector/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/luci-app-internet-detector/README.md b/luci-app-internet-detector/README.md new file mode 100644 index 000000000..9942b2e15 --- /dev/null +++ b/luci-app-internet-detector/README.md @@ -0,0 +1,49 @@ +# Internet detector for OpenWrt. +Internet-detector is an application for checking the availability of the Internet. Performs periodic connections to a known public host (8.8.8.8, 1.1.1.1) and determines the actual Internet availability. + +**OpenWrt** >= 19.07. + +**Dependences:** lua, luaposix, libuci-lua. + +**Features:** + - It can run continuously as a system service or only in an open web interface. + - Checking the availability of a host using ping or by connecting via TCP to a specified port. + - LED indication of Internet availability. +![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/internet-led.jpg) + - Performing actions when connecting and disconnecting the Internet (Restarting network, modem or device. Executing custom shell scripts). + - Sending email notification when Internet access is restored. + - The daemon is written entirely in Lua using the luaposix library. + +## Installation notes + +**OpenWrt >= 21.02:** + + opkg update + wget --no-check-certificate -O /tmp/internet-detector_1.2-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector_1.2-0_all.ipk + opkg install /tmp/internet-detector_1.2-0_all.ipk + rm /tmp/internet-detector_1.2-0_all.ipk + /etc/init.d/internet-detector start + /etc/init.d/internet-detector enable + + wget --no-check-certificate -O /tmp/luci-app-internet-detector_1.2-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-app-internet-detector_1.2-0_all.ipk + opkg install /tmp/luci-app-internet-detector_1.2-0_all.ipk + rm /tmp/luci-app-internet-detector_1.2-0_all.ipk + /etc/init.d/rpcd restart + +Email notification: + + opkg install mailsend + +i18n-ru: + + wget --no-check-certificate -O /tmp/luci-i18n-internet-detector-ru_1.2-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-i18n-internet-detector-ru_1.2-0_all.ipk + opkg install /tmp/luci-i18n-internet-detector-ru_1.2-0_all.ipk + rm /tmp/luci-i18n-internet-detector-ru_1.2-0_all.ipk + +**[OpenWrt 19.07](https://github.com/gSpotx2f/luci-app-internet-detector/tree/19.07)** + +## Screenshots: + +![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/01.jpg) +![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/02.jpg) +![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/03.jpg) diff --git a/luci-app-internet-detector/internet-detector/Makefile b/luci-app-internet-detector/internet-detector/Makefile new file mode 100644 index 000000000..5f489f5b0 --- /dev/null +++ b/luci-app-internet-detector/internet-detector/Makefile @@ -0,0 +1,63 @@ +# +# (с) 2024 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=internet-detector +PKG_VERSION:=1.2 +PKG_RELEASE:=0 +PKG_MAINTAINER:=gSpot + +include $(INCLUDE_DIR)/package.mk + +define Package/$(PKG_NAME) + SECTION:=net + CATEGORY:=Network + TITLE:=Internet detector + URL:=https://github.com/gSpotx2f/luci-app-internet-detector + PKGARCH:=all + DEPENDS:=+lua +luaposix +libuci-lua +endef + +define Package/$(PKG_NAME)/description + Internet-detector is a small daemon + for checking Internet availability. + Written in Lua using the luaposix library. +endef + +define Package/$(PKG_NAME)/conffiles +/etc/config/internet-detector +/etc/internet-detector/down-script.internet +/etc/internet-detector/up-script.internet +/etc/internet-detector/public-ip-script.internet +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/internet-detector $(1)/etc/config/internet-detector + $(INSTALL_DIR) $(1)/etc/internet-detector + $(INSTALL_DATA) ./files/etc/internet-detector/down-script.internet $(1)/etc/internet-detector/down-script.internet + $(INSTALL_DATA) ./files/etc/internet-detector/up-script.internet $(1)/etc/internet-detector/up-script.internet + $(INSTALL_DATA) ./files/etc/internet-detector/public-ip-script.internet $(1)/etc/internet-detector/public-ip-script.internet + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/internet-detector $(1)/etc/init.d/internet-detector + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/usr/bin/internet-detector $(1)/usr/bin/internet-detector + $(INSTALL_DIR) $(1)/usr/lib/internet-detector + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_email.lua $(1)/usr/lib/internet-detector/mod_email.lua + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_public_ip.lua $(1)/usr/lib/internet-detector/mod_public_ip.lua + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_led_control.lua $(1)/usr/lib/internet-detector/mod_led_control.lua + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_modem_restart.lua $(1)/usr/lib/internet-detector/mod_modem_restart.lua + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_network_restart.lua $(1)/usr/lib/internet-detector/mod_network_restart.lua + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_reboot.lua $(1)/usr/lib/internet-detector/mod_reboot.lua + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_user_scripts.lua $(1)/usr/lib/internet-detector/mod_user_scripts.lua +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/luci-app-internet-detector/internet-detector/files/etc/config/internet-detector b/luci-app-internet-detector/internet-detector/files/etc/config/internet-detector new file mode 100644 index 000000000..54356caef --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/etc/config/internet-detector @@ -0,0 +1,39 @@ +config main 'config' + option mode '1' + option enable_logger '1' + +config instance 'internet' + option enabled '1' + list hosts '8.8.8.8' + list hosts '1.1.1.1' + option check_type '0' + option tcp_port '53' + option interval_up '30' + option interval_down '5' + option connection_attempts '2' + option connection_timeout '2' + option mod_led_control_enabled '0' + option mod_reboot_enabled '0' + option mod_reboot_dead_period '3600' + option mod_reboot_force_reboot_delay '300' + option mod_network_restart_enabled '0' + option mod_network_restart_dead_period '900' + option mod_network_restart_attempts '1' + option mod_network_restart_restart_timeout '0' + option mod_modem_restart_enabled '0' + option mod_modem_restart_dead_period '600' + option mod_modem_restart_any_band '0' + option mod_public_ip_enabled '0' + option mod_public_ip_provider 'opendns1' + option mod_public_ip_qtype '0' + option mod_public_ip_interval '600' + option mod_public_ip_timeout '3' + option mod_public_ip_enable_ip_script '0' + option mod_email_enabled '0' + option mod_email_alive_period '0' + option mod_email_mail_smtp 'smtp.gmail.com' + option mod_email_mail_smtp_port '587' + option mod_email_mail_security 'tls' + option mod_user_scripts_enabled '0' + option mod_user_scripts_alive_period '0' + option mod_user_scripts_dead_period '0' diff --git a/luci-app-internet-detector/internet-detector/files/etc/init.d/internet-detector b/luci-app-internet-detector/internet-detector/files/etc/init.d/internet-detector new file mode 100755 index 000000000..95d5ca512 --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/etc/init.d/internet-detector @@ -0,0 +1,25 @@ +#!/bin/sh /etc/rc.common + +START=97 +STOP=01 + +PROG="/usr/bin/internet-detector" + +run_instance() { + config_get enabled "$1" enabled "0" + if [ $enabled = "1" ]; then + $PROG service "$1" + fi +} + +start() { + config_load internet-detector + config_get mode "config" mode "0" + if [ $mode = "1" ]; then + config_foreach run_instance "instance" + fi +} + +stop() { + $PROG stop +} diff --git a/luci-app-internet-detector/internet-detector/files/etc/internet-detector/down-script.internet b/luci-app-internet-detector/internet-detector/files/etc/internet-detector/down-script.internet new file mode 100644 index 000000000..e1f53ea49 --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/etc/internet-detector/down-script.internet @@ -0,0 +1 @@ +# Shell commands to run when disconnected from the Internet diff --git a/luci-app-internet-detector/internet-detector/files/etc/internet-detector/public-ip-script.internet b/luci-app-internet-detector/internet-detector/files/etc/internet-detector/public-ip-script.internet new file mode 100644 index 000000000..cb9723f1c --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/etc/internet-detector/public-ip-script.internet @@ -0,0 +1,2 @@ +# Shell commands that run when the public IP address changes. +# New IP is available as value of the $PUBLIC_IP variable. diff --git a/luci-app-internet-detector/internet-detector/files/etc/internet-detector/up-script.internet b/luci-app-internet-detector/internet-detector/files/etc/internet-detector/up-script.internet new file mode 100644 index 000000000..b49624099 --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/etc/internet-detector/up-script.internet @@ -0,0 +1 @@ +# Shell commands that run when connected to the Internet diff --git a/luci-app-internet-detector/internet-detector/files/usr/bin/internet-detector b/luci-app-internet-detector/internet-detector/files/usr/bin/internet-detector new file mode 100755 index 000000000..19491d3ad --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/usr/bin/internet-detector @@ -0,0 +1,704 @@ +#!/usr/bin/env lua + +--[[ + Internet detector daemon for OpenWrt. + + Dependences: + lua + luaposix + libuci-lua + + (с) 2024 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) +--]] + +-- Importing packages + +local dirent = require("posix.dirent") +local fcntl = require("posix.fcntl") +local signal = require("posix.signal") +local socket = require("posix.sys.socket") +local stat = require("posix.sys.stat") +local syslog = require("posix.syslog") +local time = require("posix.time") +local unistd = require("posix.unistd") +local uci = require("uci") + +-- Default settings + +local InternetDetector = { + mode = 0, -- 0: disabled, 1: Service, 2: UI detector + enableLogger = true, + hostname = "OpenWrt", + appName = "internet-detector", + commonDir = "/tmp/run", + pingCmd = "/bin/ping", + pingParams = "-c 1", + uiRunTime = 30, + noModules = false, + uiAvailModules = { mod_public_ip = true }, + debug = false, + serviceConfig = { + hosts = { + [1] = "8.8.8.8", + [2] = "1.1.1.1", + }, + check_type = 0, -- 0: TCP, 1: ICMP + tcp_port = 53, + icmp_packet_size = 56, + interval_up = 30, + interval_down = 5, + connection_attempts = 2, + connection_timeout = 2, + iface = nil, + instance = nil, + }, + modules = {}, + parsedHosts = {}, + uiCounter = 0, +} +InternetDetector.configDir = string.format("/etc/%s", InternetDetector.appName) +InternetDetector.modulesDir = string.format("/usr/lib/%s", InternetDetector.appName) + +-- Loading settings from UCI + +local uciCursor = uci.cursor() +InternetDetector.mode = tonumber( + uciCursor:get(InternetDetector.appName, "config", "mode")) +InternetDetector.enableLogger = (tonumber( + uciCursor:get(InternetDetector.appName, "config", "enable_logger")) ~= 0) +local hostname = uciCursor:get("system", "@[0]", "hostname") +if hostname ~= nil then + InternetDetector.hostname = hostname +end + +local RUNNING + +function InternetDetector:prequire(package) + local retVal, pkg = pcall(require, package) + return retVal and pkg +end + +function InternetDetector:loadUCIConfig(sType, instance) + local success + local num = 0 + uciCursor:foreach( + self.appName, + sType, + function(s) + if s[".name"] == instance then + for k, v in pairs(s) do + if type(v) == "string" and v:match("^[%d]+$") then + v = tonumber(v) + end + self.serviceConfig[k] = v + end + success = true + self.serviceConfig.instanceNum = num + end + num = num + 1 + end + ) + self.serviceConfig.instance = instance + self.pidFile = string.format( + "%s/%s.%s.pid", self.commonDir, self.appName, instance) + self.statusFile = string.format( + "%s/%s.%s.status", self.commonDir, self.appName, instance) + return success +end + +function InternetDetector:writeValueToFile(filePath, str) + local retValue = false + local fh = io.open(filePath, "w") + if fh then + fh:setvbuf("no") + fh:write(string.format("%s\n", str)) + fh:close() + retValue = true + end + return retValue +end + +function InternetDetector:readValueFromFile(filePath) + local retValue + local fh = io.open(filePath, "r") + if fh then + retValue = fh:read("*l") + fh:close() + end + return retValue +end + +function InternetDetector:statusJson(inet, instance, t) + local lines = { [1] = string.format( + '{"instance":"%s","num":"%d","inet":%d', + instance, + self.serviceConfig.instanceNum, + inet)} + if t then + for k, v in pairs(t) do + lines[#lines + 1] = string.format('"%s":"%s"', k, v) + end + end + return table.concat(lines, ",") .. "}" +end + +function InternetDetector:writeLogMessage(level, msg) + if self.enableLogger then + local levels = { + emerg = syslog.LOG_EMERG, + alert = syslog.LOG_ALERT, + crit = syslog.LOG_CRIT, + err = syslog.LOG_ERR, + warning = syslog.LOG_WARNING, + notice = syslog.LOG_NOTICE, + info = syslog.LOG_INFO, + debug = syslog.LOG_DEBUG, + } + syslog.syslog(levels[level] or syslog.LOG_INFO, string.format( + "%s: %s", self.serviceConfig.instance or "", msg)) + end +end + +function InternetDetector:loadModules() + package.path = string.format("%s;%s/?.lua", package.path, self.modulesDir) + self.modules = {} + local ok, modulesDir = pcall(dirent.files, self.modulesDir) + if ok then + for item in modulesDir do + if item:match("^mod_") then + local modName = item:gsub("%.lua$", "") + if self.noModules and not self.uiAvailModules[modName] then + else + local modConfig = {} + for k, v in pairs(self.serviceConfig) do + if k:match("^" .. modName) then + modConfig[k:gsub("^" .. modName .. "_", "")] = v + end + end + if modConfig.enabled == 1 then + local m = self:prequire(modName) + if m then + m.config = self + m.syslog = function(level, msg) self:writeLogMessage(level, msg) end + m.writeValue = function(filePath, str) return self:writeValueToFile(filePath, str) end + m.readValue = function(filePath) return self:readValueFromFile(filePath) end + m:init(modConfig) + self.modules[#self.modules + 1] = m + end + end + end + end + end + table.sort(self.modules, function(a, b) return a.runPrio < b.runPrio end) + end +end + +function InternetDetector:parseHost(host) + local addr, port = host:match("^([^%[%]:]+):?(%d?%d?%d?%d?%d?)$") + if not addr then + addr, port = host:match("^%[?([^%[%]]+)%]?:?(%d?%d?%d?%d?%d?)$") + end + return addr, tonumber(port) +end + +function InternetDetector:parseHosts() + self.parsedHosts = {} + for k, v in ipairs(self.serviceConfig.hosts) do + local addr, port = self:parseHost(v) + self.parsedHosts[k] = { addr = addr, port = port } + end +end + +function InternetDetector:pingHost(host) + local ping = string.format( + "%s %s -W %d -s %d%s %s > /dev/null 2>&1", + self.pingCmd, + self.pingParams, + self.serviceConfig.connection_timeout, + self.serviceConfig.icmp_packet_size, + self.serviceConfig.iface and (" -I " .. self.serviceConfig.iface) or "", + host + ) + local retCode = os.execute(ping) + + if self.debug then + io.stdout:write(string.format( + "--- Ping ---\ntime = %s\n%s\nretCode = %s\n", os.time(), ping, retCode) + ) + io.stdout:flush() + end + + return retCode +end + +function InternetDetector:TCPConnectionToHost(host, port) + local retCode = 1 + local saTable, errMsg, errNum = socket.getaddrinfo(host, port or self.serviceConfig.tcp_port) + + if not saTable then + if self.debug then + io.stdout:write(string.format( + "GETADDRINFO ERROR: %s, %s\n", errMsg, errNum)) + end + else + local family = saTable[1].family + + if family then + local sock, errMsg, errNum = socket.socket(family, socket.SOCK_STREAM, 0) + + if not sock then + if self.debug then + io.stdout:write(string.format( + "SOCKET ERROR: %s, %s\n", errMsg, errNum)) + end + return retCode + end + + socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_SNDTIMEO, self.serviceConfig.connection_timeout, 0) + socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_RCVTIMEO, self.serviceConfig.connection_timeout, 0) + + if self.serviceConfig.iface then + local ok, errMsg, errNum = socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_BINDTODEVICE, self.serviceConfig.iface) + if not ok then + if self.debug then + io.stdout:write(string.format( + "SOCKET ERROR: %s, %s\n", errMsg, errNum)) + end + unistd.close(sock) + return retCode + end + end + + local success = socket.connect(sock, saTable[1]) + + if self.debug then + if not success then + io.stdout:write(string.format( + "SOCKET CONNECT ERROR: %s\n", tostring(success))) + end + local sockTable, err_s, e_s = socket.getsockname(sock) + local peerTable, err_p, e_p = socket.getpeername(sock) + if not sockTable then + sockTable = {} + io.stdout:write( + string.format("SOCKET ERROR: %s, %s\n", err_s, e_s)) + end + if not peerTable then + peerTable = {} + io.stdout:write( + string.format("SOCKET ERROR: %s, %s\n", err_p, e_p)) + end + io.stdout:write(string.format( + "--- TCP ---\ntime = %s\nconnection_timeout = %s\niface = %s\nhost:port = [%s]:%s\nsockname = [%s]:%s\npeername = [%s]:%s\nsuccess = %s\n", + os.time(), + self.serviceConfig.connection_timeout, + tostring(self.serviceConfig.iface), + host, + port or self.serviceConfig.tcp_port, + tostring(sockTable.addr), + tostring(sockTable.port), + tostring(peerTable.addr), + tostring(peerTable.port), + tostring(success)) + ) + io.stdout:flush() + end + + socket.shutdown(sock, socket.SHUT_RDWR) + unistd.close(sock) + retCode = success and 0 or 1 + end + end + return retCode +end + +function InternetDetector:checkHosts() + local checkFunc = (self.serviceConfig.check_type == 1) and self.pingHost or self.TCPConnectionToHost + local retCode = 1 + for k, v in ipairs(self.parsedHosts) do + for i = 1, self.serviceConfig.connection_attempts do + if checkFunc(self, v.addr, v.port) == 0 then + retCode = 0 + break + end + end + if retCode == 0 then + break + end + end + return retCode +end + +function InternetDetector:breakMain(signo) + RUNNING = false +end + +function InternetDetector:resetUiCounter(signo) + self.uiCounter = 0 +end + +function InternetDetector:main() + signal.signal(signal.SIGTERM, function(signo) self:breakMain(signo) end) + signal.signal(signal.SIGINT, function(signo) self:breakMain(signo) end) + signal.signal(signal.SIGQUIT, function(signo) self:breakMain(signo) end) + signal.signal(signal.SIGUSR1, function(signo) self:resetUiCounter(signo) end) + + local lastStatus, currentStatus, mTimeNow, mTimeDiff, mLastTime, uiTimeNow, uiLastTime + local interval = self.serviceConfig.interval_up + local counter = 0 + local onStart = true + RUNNING = true + while RUNNING do + if counter == 0 or counter >= interval then + currentStatus = self:checkHosts() + if onStart or not stat.stat(self.statusFile) then + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance)) + onStart = false + end + + if currentStatus == 0 then + interval = self.serviceConfig.interval_up + if lastStatus ~= nil and currentStatus ~= lastStatus then + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance)) + self:writeLogMessage("notice", "Connected") + end + else + interval = self.serviceConfig.interval_down + if lastStatus ~= nil and currentStatus ~= lastStatus then + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance)) + self:writeLogMessage("notice", "Disconnected") + end + end + + counter = 0 + end + + mTimeDiff = 0 + for _, e in ipairs(self.modules) do + mTimeNow = time.clock_gettime(time.CLOCK_MONOTONIC).tv_sec + if mLastTime then + mTimeDiff = mTimeDiff + mTimeNow - mLastTime + else + mTimeDiff = 1 + end + mLastTime = mTimeNow + e:run(currentStatus, lastStatus, mTimeDiff) + end + + local modulesStatus = {} + for k, v in ipairs(self.modules) do + if v.status ~= nil then + modulesStatus[v.name] = v.status + end + end + if next(modulesStatus) then + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance, modulesStatus)) + end + + lastStatus = currentStatus + unistd.sleep(1) + counter = counter + 1 + + if self.mode == 2 then + uiTimeNow = time.clock_gettime(time.CLOCK_MONOTONIC).tv_sec + if uiLastTime then + self.uiCounter = self.uiCounter + uiTimeNow - uiLastTime + else + self.uiCounter = self.uiCounter + 1 + end + uiLastTime = uiTimeNow + if self.uiCounter >= self.uiRunTime then + self:breakMain(signal.SIGTERM) + end + end + end +end + +function InternetDetector:removeProcessFiles() + os.remove(string.format( + "%s/%s.%s.pid", self.commonDir, self.appName, self.serviceConfig.instance)) + os.remove(string.format( + "%s/%s.%s.status", self.commonDir, self.appName, self.serviceConfig.instance)) +end + +function InternetDetector:status() + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + local appName = self.appName:gsub("-", "%%-") + for item in commonDir do + if item:match("^" .. appName .. ".-%.pid$") then + return "running" + end + end + end + return "stoped" +end + +function InternetDetector:inetStatus() + local inetStat = '{"instances":[]}' + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + local appName = self.appName:gsub("-", "%%-") + local lines = {} + for item in commonDir do + if item:match("^" .. appName .. ".-%.status$") then + lines[#lines + 1] = self:readValueFromFile( + string.format("%s/%s", self.commonDir, item)) + end + end + inetStat = '{"instances":[' .. table.concat(lines, ",") .. "]}" + end + return inetStat +end + +function InternetDetector:stopInstance(pidFile) + local retVal + if stat.stat(pidFile) then + pidValue = self:readValueFromFile(pidFile) + if pidValue then + local ok, errMsg, errNum + for i = 0, 10 do + ok, errMsg, errNum = signal.kill(tonumber(pidValue), signal.SIGTERM) + if ok then + break + end + end + if not ok then + io.stderr:write(string.format( + 'Process stop error: %s (%s). PID: "%s"\n', errMsg, errNum, pidValue)) + end + if errNum == 3 then + os.remove(pidFile) + end + retVal = true + end + end + if not pidValue then + io.stderr:write( + string.format('PID file "%s" does not exist. %s not running?\n', + pidFile, self.appName)) + end + return retVal +end + +function InternetDetector:stop() + local appName = self.appName:gsub("-", "%%-") + local success + for i = 0, 10 do + success = true + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + for item in commonDir do + if item:match("^" .. appName .. ".-%.pid$") then + self:stopInstance(string.format("%s/%s", self.commonDir, item)) + success = false + end + end + if success then + break + end + unistd.sleep(1) + else + break + end + end +end + +function InternetDetector:setSIGUSR() + local appName = self.appName:gsub("-", "%%-") + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + for item in commonDir do + if item:match("^" .. appName .. ".-%.pid$") then + pidValue = self:readValueFromFile(string.format("%s/%s", self.commonDir, item)) + if pidValue then + signal.kill(tonumber(pidValue), signal.SIGUSR1) + end + end + end + end +end + +function InternetDetector:preRun() + -- Exit if internet-detector mode != (1 or 2) + if self.mode ~= 1 and self.mode ~= 2 then + io.stderr:write(string.format('Start failed, mode != (1 or 2)\n', self.appName)) + os.exit(0) + end + if stat.stat(self.pidFile) then + io.stderr:write( + string.format('PID file "%s" already exist. %s already running?\n', + self.pidFile, self.appName)) + return false + end + return true +end + +function InternetDetector:run() + local pidValue = unistd.getpid() + self:writeValueToFile(self.pidFile, pidValue) + if self.enableLogger then + syslog.openlog(self.appName, syslog.LOG_PID, syslog.LOG_DAEMON) + end + self:writeLogMessage("info", "started") + self:loadModules() + + -- Loaded modules + local modules = {} + for _, v in ipairs(self.modules) do + modules[#modules + 1] = string.format("%s", v.name) + end + if #modules > 0 then + self:writeLogMessage( + "info", string.format("Loaded modules: %s", table.concat(modules, ", ")) + ) + end + + if self.debug then + local function inspectTable() + local tables = {}, f + f = function(t, prefix) + tables[t] = true + for k, v in pairs(t) do + io.stdout:write(string.format( + "%s%s = %s\n", prefix, k, tostring(v)) + ) + if type(v) == "table" and not tables[v] then + f(v, string.format("%s%s.", prefix, k)) + end + end + end + return f + end + + io.stdout:write("--- Config ---\n") + inspectTable()(self, "self.") + io.stdout:flush() + end + + self:writeValueToFile( + self.statusFile, self:statusJson(-1, self.serviceConfig.instance)) + + self:main() + + self:removeProcessFiles() + if self.enableLogger then + self:writeLogMessage("info", "stoped") + syslog.closelog() + end +end + +function InternetDetector:noDaemon() + if not self:preRun() then + return + end + self:run() +end + +function InternetDetector:daemon() + if not self:preRun() then + return + end + -- UNIX double fork + if unistd.fork() == 0 then + unistd.setpid("s") + if unistd.fork() == 0 then + unistd.chdir("/") + stat.umask(0) + local devnull = fcntl.open("/dev/null", fcntl.O_RDWR) + io.stdout:flush() + io.stderr:flush() + unistd.dup2(devnull, 0) -- io.stdin + unistd.dup2(devnull, 1) -- io.stdout + unistd.dup2(devnull, 2) -- io.stderr + self:run() + unistd.close(devnull) + end + os.exit(0) + end + os.exit(0) +end + +function InternetDetector:setServiceConfig(instance) + if self:loadUCIConfig("instance", instance) then + self:parseHosts() + if self.mode == 2 then + self.enableLogger = false + self.noModules = true + end + return true + end +end + +-- Main section + +local function help() + return string.format( + "Usage: %s service | nodaemon | debug | stop | status | inet-status | uipoll | --help", + arg[0] + ) +end + +local helpArgs = { ["-h"] = true, ["--help"] = true, help = true } +if arg[1] == "service" then + if arg[2] then + if InternetDetector:setServiceConfig(arg[2]) then + InternetDetector:daemon() + else + os.exit(126) + end + else + print(help()) + os.exit(1) + end +elseif arg[1] == "nodaemon" then + if arg[2] then + if InternetDetector:setServiceConfig(arg[2]) then + InternetDetector:noDaemon() + else + os.exit(126) + end + else + print(help()) + os.exit(1) + end +elseif arg[1] == "debug" then + if arg[2] then + if InternetDetector:setServiceConfig(arg[2]) then + InternetDetector.debug = true + InternetDetector:noDaemon() + else + os.exit(126) + end + else + print(help()) + os.exit(1) + end +elseif arg[1] == "stop" then + InternetDetector:stop() +elseif arg[1] == "status" then + print(InternetDetector:status()) +elseif arg[1] == "inet-status" then + print(InternetDetector:inetStatus()) +elseif arg[1] == "uipoll" then + if InternetDetector:status() == "stoped" then + os.exit(126) + else + InternetDetector:setSIGUSR() + print(InternetDetector:inetStatus()) + end +elseif helpArgs[arg[1]] then + print(help()) +else + print(help()) + os.exit(1) +end + +os.exit(0) diff --git a/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_email.lua b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_email.lua new file mode 100644 index 000000000..d1f4cb7c9 --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_email.lua @@ -0,0 +1,147 @@ +--[[ + Dependences: + mailsend +--]] +local unistd = require("posix.unistd") + +local Module = { + name = "mod_email", + runPrio = 60, + config = { + debug = false, + }, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + alivePeriod = 0, + hostAlias = "OpenWrt", + mta = "/usr/bin/mailsend", + mailRecipient = "email@gmail.com", + mailSender = "email@gmail.com", + mailUser = "email@gmail.com", + mailPassword = "password", + mailSmtp = "smtp.gmail.com", + mailSmtpPort = '587', + mailSecurity = "tls", + status = nil, + _enabled = false, + _aliveCounter = 0, + _msgSent = true, + _disconnected = true, + _lastDisconnection = nil, + _lastConnection = nil, +} + +function Module:init(t) + self.alivePeriod = tonumber(t.alive_period) + if t.host_alias then + self.hostAlias = t.host_alias + else + self.hostAlias = self.config.hostname + end + + self.mailRecipient = t.mail_recipient + self.mailSender = t.mail_sender + self.mailUser = t.mail_user + self.mailPassword = t.mail_password + self.mailSmtp = t.mail_smtp + self.mailSmtpPort = t.mail_smtp_port + self.mailSecurity = t.mail_security + + if unistd.access(self.mta, "x") then + self._enabled = true + else + self._enabled = false + self.syslog("warning", string.format("%s: %s is not available", self.name, self.mta)) + end + + if (not self.mailRecipient or + not self.mailSender or + not self.mailUser or + not self.mailPassword or + not self.mailSmtp or + not self.mailSmtpPort) then + self._enabled = false + self.syslog("warning", string.format( + "%s: Insufficient data to connect to the SMTP server", self.name)) + end +end + +function Module:sendMessage(msg) + local verboseArg = "" + + -- Debug + if self.config.debug then + verboseArg = " -v" + io.stdout:write(string.format("--- %s ---\n", self.name)) + io.stdout:flush() + end + + local securityArgs = "-starttls -auth-login" + if self.mailSecurity == "ssl" then + securityArgs = "-ssl -auth" + end + + local mtaCmd = string.format( + '%s%s %s -smtp "%s" -port %s -cs utf-8 -user "%s" -pass "%s" -f "%s" -t "%s" -sub "%s" -M "%s"', + self.mta, verboseArg, securityArgs, self.mailSmtp, self.mailSmtpPort, + self.mailUser, self.mailPassword, self.mailSender, self.mailRecipient, + string.format("%s notification", self.hostAlias), + string.format("%s:\n%s", self.hostAlias, msg)) + + if os.execute(mtaCmd) ~= 0 then + self.syslog("err", string.format( + "%s: An error occured while sending message", self.name)) + else + self.syslog("info", string.format( + "%s: Message sent to %s", self.name, self.mailRecipient)) + end +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if not self._enabled then + return + end + + if currentStatus == 1 then + self._aliveCounter = 0 + self._msgSent = false + self._lastConnection = nil + if not self._disconnected then + self._disconnected = true + if not self._lastDisconnection then + self._lastDisconnection = os.date("%Y.%m.%d %H:%M:%S", os.time()) + end + end + + else + + if not self._msgSent then + + if not self._lastConnection then + self._lastConnection = os.date("%Y.%m.%d %H:%M:%S", os.time()) + end + + if self._aliveCounter >= self.alivePeriod then + local message = {} + if self._lastDisconnection then + message[#message + 1] = string.format( + "Internet disconnected: %s", self._lastDisconnection) + self._lastDisconnection = nil + end + if self._lastConnection then + message[#message + 1] = string.format( + "Internet connected: %s", self._lastConnection) + self:sendMessage(table.concat(message, ", ")) + self._msgSent = true + end + else + self._aliveCounter = self._aliveCounter + timeDiff + end + end + + self._disconnected = false + end +end + +return Module diff --git a/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua new file mode 100644 index 000000000..ae57c8699 --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua @@ -0,0 +1,137 @@ + +local unistd = require("posix.unistd") +local dirent = require("posix.dirent") + +local Module = { + name = "mod_led_control", + runPrio = 10, + config = {}, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + runInterval = 5, + sysLedsDir = "/sys/class/leds", + ledName = nil, + ledAction1 = 2, -- 1: off, 2: on, 3: blink + ledAction2 = 1, -- 1: off, 2: on, 3: blink + status = nil, + _enabled = false, + _ledDir = nil, + _ledMaxBrightnessFile = nil, + _ledBrightnessFile = nil, + _ledMaxBrightness = nil, + _ledTriggerFile = nil, + _counter = 0, +} + +function Module:resetLeds() + local ok, dir = pcall(dirent.files, self.sysLedsDir) + if not ok then + return + end + for led in dir do + local brightness = string.format("%s/%s/brightness", self.sysLedsDir, led) + if unistd.access(brightness, "w") then + self.writeValue(brightness, 0) + end + end +end + +function Module:init(t) + self.ledName = t.led_name + if not self.ledName then + return + end + self.ledAction1 = tonumber(t.led_action_1) + self.ledAction2 = tonumber(t.led_action_2) + self._ledDir = string.format("%s/%s", self.sysLedsDir, self.ledName) + self._ledMaxBrightnessFile = string.format("%s/max_brightness", self._ledDir) + self._ledBrightnessFile = string.format("%s/brightness", self._ledDir) + self._ledMaxBrightness = self.readValue(self._ledMaxBrightnessFile) or 1 + self._ledTriggerFile = string.format("%s/trigger", self._ledDir) + + if (not unistd.access(self._ledDir, "r") or + not unistd.access(self._ledBrightnessFile, "rw") or + not unistd.access(self._ledTriggerFile, "rw")) then + self._enabled = false + self.syslog("warning", string.format( + "%s: LED '%s' is not available", self.name, self.ledName)) + else + self._enabled = true + -- Reset all LEDs + --self:resetLeds() + end +end + +function Module:SetTriggerTimer() + self.writeValue(self._ledTriggerFile, "timer") +end + +function Module:SetTriggerNone() + self.writeValue(self._ledTriggerFile, "none") +end + +function Module:getCurrentTrigger() + local trigger = self.readValue(self._ledTriggerFile) + if trigger and trigger:match("%[timer%]") then + return 1 + end +end + +function Module:on() + self:SetTriggerNone() + self.writeValue(self._ledBrightnessFile, self._ledMaxBrightness) +end + +function Module:off() + self:SetTriggerNone() + self.writeValue(self._ledBrightnessFile, 0) +end + +function Module:getCurrentState() + local state = self.readValue(self._ledBrightnessFile) + if state and tonumber(state) > 0 then + return tonumber(state) + end +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if not self._enabled then + return + end + if self._counter == 0 or self._counter >= self.runInterval or currentStatus ~= lastStatus then + if currentStatus == 0 then + if self.ledAction1 == 1 then + if self:getCurrentState() or self:getCurrentTrigger() then + self:off() + end + elseif self.ledAction1 == 2 then + if not self:getCurrentState() or self:getCurrentTrigger() then + self:on() + end + elseif self.ledAction1 == 3 then + if not self:getCurrentTrigger() then + self:SetTriggerTimer() + end + end + else + if self.ledAction2 == 1 then + if self:getCurrentState() or self:getCurrentTrigger() then + self:off() + end + elseif self.ledAction2 == 2 then + if not self:getCurrentState() or self:getCurrentTrigger() then + self:on() + end + elseif self.ledAction2 == 3 then + if not self:getCurrentTrigger() then + self:SetTriggerTimer() + end + end + end + self._counter = 0 + end + self._counter = self._counter + timeDiff +end + +return Module diff --git a/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua new file mode 100644 index 000000000..405e2632e --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua @@ -0,0 +1,86 @@ +--[[ + Dependences: + modemmanager +--]] +local unistd = require("posix.unistd") + +local Module = { + name = "mod_modem_restart", + runPrio = 40, + config = {}, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + mmcli = "/usr/bin/mmcli", + mmInit = "/etc/init.d/modemmanager", + deadPeriod = 0, + iface = nil, + anyBand = false, + status = nil, + _enabled = false, + _deadCounter = 0, + _restarted = false, +} + +function Module:toggleIface(flag) + return os.execute( + string.format("%s %s", (flag and "/sbin/ifup" or "/sbin/ifdown"), self.iface) + ) +end + +function Module:restartMM() + if self.anyBand then + self.syslog("info", string.format( + "%s: resetting current-bands to 'any'", self.name)) + os.execute(string.format("%s -m any --set-current-bands=any", self.mmcli)) + end + + self.syslog("info", string.format("%s: reconnecting modem", self.name)) + os.execute(string.format("%s restart", self.mmInit)) + + if self.iface then + self.syslog("info", string.format( + "%s: restarting network interface '%s'", self.name, self.iface)) + self:toggleIface(false) + self:toggleIface(true) + end +end + +function Module:init(t) + self.deadPeriod = tonumber(t.dead_period) + self.iface = t.iface + self.anyBand = (tonumber(t.any_band) ~= 0) + + if not unistd.access(self.mmcli, "x") then + self.anyBand = false + end + + if unistd.access(self.mmInit, "x") then + self._enabled = true + else + self._enabled = false + self.syslog("warning", string.format( + "%s: modemmanager service is not available", self.name)) + end +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if not self._enabled then + return + end + if currentStatus == 1 then + if not self._restarted then + if self._deadCounter >= self.deadPeriod then + self:restartMM() + self._restarted = true + else + self._deadCounter = self._deadCounter + timeDiff + end + end + else + self._deadCounter = 0 + self._restarted = false + end +end + +return Module diff --git a/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua new file mode 100644 index 000000000..66456f26a --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua @@ -0,0 +1,94 @@ + +local unistd = require("posix.unistd") + +local Module = { + name = "mod_network_restart", + runPrio = 30, + config = {}, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + iface = false, + attempts = 0, + deadPeriod = 0, + restartTimeout = 0, + status = nil, + _attemptsCounter = 0, + _deadCounter = 0, +} + +function Module:toggleFunc(flag) + return +end + +function Module:toggleDevice(flag) + local ip = "/sbin/ip" + if unistd.access(ip, "x") then + return os.execute(string.format( + "%s link set dev %s %s", ip, self.iface, (flag and "up" or "down")) + ) + end +end + +function Module:toggleIface(flag) + return os.execute( + string.format("%s %s", (flag and "/sbin/ifup" or "/sbin/ifdown"), self.iface) + ) +end + +function Module:ifaceUp() + self:toggleFunc(true) +end + +function Module:ifaceDown() + self:toggleFunc(false) +end + +function Module:networkRestart() + return os.execute("/etc/init.d/network restart") +end + +function Module:init(t) + local iface = t.iface + if iface then + self.iface = iface + if self.iface:match("^@") then + self.iface = self.iface:gsub("^@", "") + self.toggleFunc = self.toggleIface + else + self.toggleFunc = self.toggleDevice + end + end + self.attempts = tonumber(t.attempts) + self.deadPeriod = tonumber(t.dead_period) + self.restartTimeout = tonumber(t.restart_timeout) +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if currentStatus == 1 then + if self.attempts == 0 or self._attemptsCounter < self.attempts then + if self._deadCounter >= self.deadPeriod then + if self.iface then + self.syslog("info", string.format( + "%s: restarting network interface '%s'", self.name, self.iface)) + self:ifaceDown() + unistd.sleep(self.restartTimeout) + self:ifaceUp() + else + self.syslog("info", string.format( + "%s: restarting network", self.name)) + self:networkRestart() + end + self._deadCounter = 0 + self._attemptsCounter = self._attemptsCounter + 1 + else + self._deadCounter = self._deadCounter + timeDiff + end + end + else + self._attemptsCounter = 0 + self._deadCounter = 0 + end +end + +return Module diff --git a/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_public_ip.lua b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_public_ip.lua new file mode 100644 index 000000000..76039f914 --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_public_ip.lua @@ -0,0 +1,405 @@ + +local socket = require("posix.sys.socket") +local stdlib = require("posix.stdlib") +local unistd = require("posix.unistd") + +local Module = { + name = "mod_public_ip", + runPrio = 50, + config = { + noModules = false, + debug = false, + serviceConfig = { + iface = nil, + }, + }, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + port = 53, + runInterval = 600, + runIntervalFailed = 60, + timeout = 3, + reqAttempts = 3, + providers = { + opendns1 = { + name = "opendns1", host = "myip.opendns.com", + server = "208.67.222.222", server6 = "2620:119:35::35", + port = 53, queryType = "A", queryType6 = "AAAA", + }, + opendns2 = { + name = "opendns2", host = "myip.opendns.com", + server = "208.67.220.220", server6 = "2620:119:35::35", + port = 53, queryType = "A", queryType6 = "AAAA", + }, + opendns3 = { + name = "opendns3", host = "myip.opendns.com", + server = "208.67.222.220", server6 = "2620:119:35::35", + port = 53, queryType = "A", queryType6 = "AAAA", + }, + opendns4 = { + name = "opendns4", host = "myip.opendns.com", + server = "208.67.220.222", server6 = "2620:119:35::35", + port = 53, queryType = "A", queryType6 = "AAAA", + }, + akamai = { + name = "akamai", host = "whoami.akamai.net", + server = "ns1-1.akamaitech.net", server6 = "ns1-1.akamaitech.net", + port = 53, queryType = "A", queryType6 = "AAAA", + }, + google = { + name = "google", host = "o-o.myaddr.l.google.com", + server = "ns1.google.com", server6 = "ns1.google.com", + port = 53, queryType = "TXT", queryType6 = "TXT", + }, + }, + ipScript = "", + enableIpScript = false, + status = nil, + _provider = nil, + _qtype = false, + _currentIp = nil, + _enabled = false, + _counter = 0, + _interval = 600, + _DNSPacket = nil, +} + +function Module:runIpScript() + if not self.config.noModules and self.enableIpScript and unistd.access(self.ipScript, "r") then + stdlib.setenv("PUBLIC_IP", self.status) + os.execute(string.format('/bin/sh "%s" &', self.ipScript)) + end +end + +function Module:getQueryType(type) + local types = { + A = 1, + NS = 2, + MD = 3, + MF = 4, + CNAME = 5, + SOA = 6, + MB = 7, + MG = 8, + MR = 9, + NULL = 10, + WKS = 11, + PTS = 12, + HINFO = 13, + MINFO = 14, + MX = 15, + TXT = 16, + AAAA = 28, + } + return types[type] +end + +function Module:buildMessage(address, queryType) + if not queryType then + queryType = "A" + end + queryType = self:getQueryType(queryType) + + local addressString = "" + for part in address:gmatch("[^.]+") do + local t = {} + for i in part:gmatch(".") do + t[#t + 1] = i + end + addrLen = #part + addrPart = table.concat(t) + addressString = addressString .. string.char(addrLen) .. addrPart + end + + local data = ( + string.char( + 0xaa, 0xaa, + 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ) .. + addressString .. + string.char( + 0x00, + 0x00, queryType, + 0x00, 0x01 + ) + ) + return data +end + +function Module:sendUDPMessage(message, server, port) + local success + local retCode = 1 + local data + + if self.config.debug then + io.stdout:write(string.format("--- %s ---\n", self.name)) + io.stdout:flush() + end + + local saTable, errMsg, errNum = socket.getaddrinfo(server, port) + + if not saTable then + if self.config.debug then + io.stdout:write(string.format( + "GETADDRINFO ERROR: %s, %s\n", errMsg, errNum)) + end + else + local family = saTable[1].family + + if family then + local sock, errMsg, errNum = socket.socket(family, socket.SOCK_DGRAM, 0) + + if not sock then + if self.config.debug then + io.stdout:write(string.format( + "SOCKET ERROR: %s, %s\n", errMsg, errNum)) + end + return retCode + end + + socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_SNDTIMEO, self.timeout, 0) + socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_RCVTIMEO, self.timeout, 0) + + if self.config.serviceConfig.iface then + local ok, errMsg, errNum = socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_BINDTODEVICE, self.config.serviceConfig.iface) + if not ok then + if self.config.debug then + io.stdout:write(string.format( + "SOCKET ERROR: %s, %s\n", errMsg, errNum)) + end + unistd.close(sock) + return retCode + end + end + + local ok, errMsg, errNum = socket.sendto(sock, message, saTable[1]) + local response = {} + if ok then + local ret, resp, errNum = socket.recvfrom(sock, 1024) + data = ret + if data then + success = true + response = resp + elseif self.config.debug then + io.stdout:write(string.format( + "SOCKET RECV ERROR: %s, %s\n", tostring(resp), tostring(errNum))) + end + elseif self.config.debug then + io.stdout:write(string.format( + "SOCKET SEND ERROR: %s, %s\n", tostring(errMsg), tostring(errNum))) + end + + if self.config.debug then + io.stdout:write(string.format( + "--- UDP ---\ntime = %s\nconnection_timeout = %s\niface = %s\nserver = %s:%s\nsockname = %s:%s\nsuccess = %s\n", + os.time(), + self.timeout, + tostring(self.config.serviceConfig.iface), + server, + tostring(port), + tostring(response.addr), + tostring(response.port), + tostring(success)) + ) + io.stdout:flush() + end + + unistd.close(sock) + retCode = success and 0 or 1 + end + end + return retCode, tostring(data) +end + +function Module:parseParts(message, start, parts) + local partStart = start + 2 + local partLen = message:sub(start, start + 1) + + if #partLen == 0 then + return parts + end + + local partEnd = partStart + (tonumber(partLen, 16) * 2) + + parts[#parts + 1] = message:sub(partStart, partEnd - 1) + if message:sub(partEnd, partEnd + 1) == "00" or partEnd > #message then + return parts + else + return self:parseParts(message, partEnd, parts) + end +end + +function Module:decodeMessage(message) + local retTable = {} + local t = {} + for i = 1, #message do + t[#t + 1] = string.format("%.2x", string.byte(message, i)) + end + message = table.concat(t) + + local ANCOUNT = message:sub(13, 16) + local NSCOUNT = message:sub(17, 20) + local ARCOUNT = message:sub(21, 24) + + local questionSectionStarts = 25 + local questionParts = self:parseParts(message, questionSectionStarts, {}) + local qtypeStarts = questionSectionStarts + (#table.concat(questionParts)) + (#questionParts * 2) + 1 + local qclassStarts = qtypeStarts + 4 + + local answerSectionStarts = qclassStarts + 4 + local numAnswers = math.max( + tonumber(ANCOUNT, 16), tonumber(NSCOUNT, 16), tonumber(ARCOUNT, 16)) + + if numAnswers > 0 then + for answerCount = 1, numAnswers do + + if answerSectionStarts < #message then + local ATYPE = tonumber( + message:sub(answerSectionStarts + 5, answerSectionStarts + 8), 16) + local RDLENGTH = tonumber( + message:sub(answerSectionStarts + 21, answerSectionStarts + 24), 16) + local RDDATA = message:sub( + answerSectionStarts + 25, answerSectionStarts + 24 + (RDLENGTH * 2)) + local RDDATA_decoded = "" + + if #RDDATA > 0 then + if ATYPE == self:getQueryType("A") or ATYPE == self:getQueryType("AAAA") then + local octets = {} + local sep = "." + if #RDDATA > 8 then + sep = ":" + for i = 1, #RDDATA, 4 do + local string = RDDATA:sub(i, i + 3) + string = string:gsub("^00?0?", "") + octets[#octets + 1] = string + end + else + for i = 1, #RDDATA, 2 do + octets[#octets + 1] = tonumber(RDDATA:sub(i, i + 1), 16) + end + end + RDDATA_decoded = table.concat(octets, sep):gsub("0:[0:]+", "::", 1):gsub("::+", "::") + else + local rdata_t = {} + for _, v in ipairs(self:parseParts(RDDATA, 1, {})) do + local t = {} + for i = 1, #v, 2 do + t[#t + 1] = string.char(tonumber(v:sub(i, i + 1), 16)) + end + rdata_t[#rdata_t + 1] = table.concat(t) + end + RDDATA_decoded = table.concat(rdata_t) + end + end + answerSectionStarts = answerSectionStarts + 24 + (RDLENGTH * 2) + + if RDDATA_decoded:match("^[a-f0-9.:]+$") then + retTable[#retTable + 1] = RDDATA_decoded + end + end + end + end + + return retTable +end + +function Module:resolveIP() + local res + local qtype = self._qtype and self._provider.queryType6 or self._provider.queryType + local server = self._qtype and self._provider.server6 or self._provider.server + local port = self._provider.port or self.port + + if not self._DNSPacket then + self._DNSPacket = self:buildMessage(self._provider.host, qtype) + end + + local retCode, response = self:sendUDPMessage(self._DNSPacket, server, port) + if retCode == 0 then + local retTable = self:decodeMessage(response) + if #retTable > 0 then + res = table.concat(retTable, ", ") + end + else + self.syslog("err", string.format( + "%s: DNS error when requesting an IP address", self.name)) + end + + return res +end + +function Module:init(t) + if t.interval then + self.runInterval = tonumber(t.interval) + end + if t.timeout then + self.timeout = tonumber(t.timeout) + end + if t.provider then + self._provider = self.providers[t.provider] + else + self._provider = self.providers.opendns1 + end + if self.config.configDir then + self.ipScript = string.format( + "%s/public-ip-script.%s", self.config.configDir, self.config.serviceConfig.instance) + if t.enable_ip_script then + self.enableIpScript = (tonumber(t.enable_ip_script) ~= 0) + end + end + self._qtype = (tonumber(t.qtype) ~= 0) + self._currentIp = nil + self._DNSPacket = nil + self._interval = self.runInterval + self._enabled = true +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if not self._enabled then + return + end + if currentStatus == 0 then + if self._counter == 0 or self._counter >= self._interval or currentStatus ~= lastStatus then + + local ip = self:resolveIP() + + if not ip then + ip = "" + self._interval = self.runIntervalFailed + else + self._interval = self.runInterval + end + + if ip ~= self._currentIp then + self.status = ip + self.syslog( + "notice", + string.format("%s: public IP address %s", self.name, (ip == "") and "Undefined" or ip) + ) + if self._counter > 0 then + self:runIpScript() + end + else + self.status = nil + end + + self._currentIp = ip + self._counter = 0 + else + self.status = nil + end + else + self.status = nil + self._currentIp = nil + self._counter = 0 + self._interval = self.runInterval + end + + self._counter = self._counter + timeDiff +end + +return Module diff --git a/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua new file mode 100644 index 000000000..af39b2289 --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua @@ -0,0 +1,47 @@ + +local unistd = require("posix.unistd") + +local Module = { + name = "mod_reboot", + runPrio = 20, + config = {}, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + deadPeriod = 0, + forceRebootDelay = 0, + status = nil, + _deadCounter = 0, +} + +function Module:rebootDevice() + self.syslog("warning", string.format("%s: reboot", self.name)) + os.execute("/sbin/reboot &") + if self.forceRebootDelay > 0 then + unistd.sleep(self.forceRebootDelay) + self.syslog("warning", string.format("%s: force reboot", self.name)) + self.writeValue("/proc/sys/kernel/sysrq", "1") + self.writeValue("/proc/sysrq-trigger", "b") + end +end + +function Module:init(t) + self.deadPeriod = tonumber(t.dead_period) + self.forceRebootDelay = tonumber(t.force_reboot_delay) +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if currentStatus == 1 then + if self._deadCounter >= self.deadPeriod then + self:rebootDevice() + self._deadCounter = 0 + else + self._deadCounter = self._deadCounter + timeDiff + end + + else + self._deadCounter = 0 + end +end + +return Module diff --git a/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua new file mode 100644 index 000000000..ef6f14851 --- /dev/null +++ b/luci-app-internet-detector/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua @@ -0,0 +1,65 @@ + +local unistd = require("posix.unistd") + +local Module = { + name = "mod_user_scripts", + runPrio = 70, + config = {}, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + deadPeriod = 0, + alivePeriod = 0, + upScript = "", + downScript = "", + status = nil, + _deadCounter = 0, + _aliveCounter = 0, + _upScriptExecuted = true, + _downScriptExecuted = true, +} + +function Module:runExternalScript(scriptPath) + if unistd.access(scriptPath, "r") then + os.execute(string.format('/bin/sh "%s" &', scriptPath)) + end +end + +function Module:init(t) + self.deadPeriod = tonumber(t.dead_period) + self.alivePeriod = tonumber(t.alive_period) + if self.config.configDir then + self.upScript = string.format( + "%s/up-script.%s", self.config.configDir, self.config.serviceConfig.instance) + self.downScript = string.format( + "%s/down-script.%s", self.config.configDir, self.config.serviceConfig.instance) + end +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if currentStatus == 1 then + self._aliveCounter = 0 + self._downScriptExecuted = false + if not self._upScriptExecuted then + if self._deadCounter >= self.deadPeriod then + self:runExternalScript(self.downScript) + self._upScriptExecuted = true + else + self._deadCounter = self._deadCounter + timeDiff + end + end + else + self._deadCounter = 0 + self._upScriptExecuted = false + if not self._downScriptExecuted then + if self._aliveCounter >= self.alivePeriod then + self:runExternalScript(self.upScript) + self._downScriptExecuted = true + else + self._aliveCounter = self._aliveCounter + timeDiff + end + end + end +end + +return Module diff --git a/luci-app-internet-detector/luci-app-internet-detector/Makefile b/luci-app-internet-detector/luci-app-internet-detector/Makefile new file mode 100644 index 000000000..44500bed7 --- /dev/null +++ b/luci-app-internet-detector/luci-app-internet-detector/Makefile @@ -0,0 +1,15 @@ +# +# (с) 2024 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) +# + +include $(TOPDIR)/rules.mk + +PKG_VERSION:=1.2-0 +LUCI_TITLE:=LuCI support for internet-detector +LUCI_DEPENDS:=+internet-detector +LUCI_PKGARCH:=all + +#include ../../luci.mk +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-internet-detector/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js b/luci-app-internet-detector/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js new file mode 100644 index 000000000..49a74ed47 --- /dev/null +++ b/luci-app-internet-detector/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js @@ -0,0 +1,1165 @@ +'use strict'; +'require baseclass'; +'require form'; +'require fs'; +'require poll'; +'require rpc'; +'require uci'; +'require ui'; +'require view'; +'require tools.widgets as widgets' + +document.head.append(E('style', {'type': 'text/css'}, +` +:root { + --app-id-font-color: #454545; + --app-id-font-shadow: #fff; + --app-id-connected-color: #6bdebb; + --app-id-disconnected-color: #f8aeba; + --app-id-undefined-color: #dfdfdf; +} +:root[data-darkmode="true"] { + --app-id-font-color: #f6f6f6; + --app-id-font-shadow: #4d4d4d; + --app-id-connected-color: #005F20; + --app-id-disconnected-color: #a93734; + --app-id-undefined-color: #4d4d4d; +} +.id-connected { + --on-color: var(--app-id-font-color); + background-color: var(--app-id-connected-color) !important; + border-color: var(--app-id-connected-color) !important; + color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); +} +.id-disconnected { + --on-color: var(--app-id-font-color); + background-color: var(--app-id-disconnected-color) !important; + border-color: var(--app-id-disconnected-color) !important; + color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); +} +.id-undefined { + --on-color: var(--app-id-font-color); + background-color: var(--app-id-undefined-color) !important; + border-color: var(--app-id-undefined-color) !important; + color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); +} +.id-label-status { + display: inline-block; + word-wrap: break-word; + margin: 2px !important; + padding: 4px 8px; + border: 1px solid; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + font-weight: bold; +} +`)); + +const btnStyleEnabled = 'btn cbi-button-save'; +const btnStyleDisabled = 'btn cbi-button-reset'; +const btnStyleApply = 'btn cbi-button-apply'; + +var Timefield = ui.Textfield.extend({ + secToString(value) { + let string = '0'; + if(/^\d+$/.test(value)) { + value = Number(value); + if(value >= 3600 && (value % 3600) === 0) { + string = String(value / 3600) + 'h'; + } + else if(value >= 60 && (value % 60) === 0) { + string = String(value / 60) + 'm'; + } + else { + string = String(value) + 's'; + }; + }; + return string; + }, + + render() { + let frameEl = E('div', { 'id': this.options.id }), + inputEl = E('input', { + 'id' : this.options.id ? 'widget.' + this.options.id : null, + 'name' : this.options.name, + 'type' : 'text', + 'class' : 'cbi-input-text', + 'readonly' : this.options.readonly ? '' : null, + 'disabled' : this.options.disabled ? '' : null, + 'maxlength' : this.options.maxlength, + 'placeholder': this.options.placeholder, + 'value' : this.secToString(this.value), + }); + frameEl.appendChild(inputEl); + return this.bind(frameEl); + }, + + getValue() { + let rawValue = this.node.querySelector('input').value, + value = 0, + res = rawValue.match(/^(\d+)([hms]?)$/); + if(res) { + if(res[2] === 'h') { + value = Number(res[1]) * 3600; + } + else if(res[2] === 'm') { + value = Number(res[1]) * 60; + } + else if(!res[2] || res[2] === 's') { + value = Number(res[1]); + } + else { + value = 0; + }; + } else { + value = 0; + }; + return String(value); + }, + + setValue(value) { + let inputEl = this.node.querySelector('input'); + inputEl.value = this.secToString(value); + }, +}); + +return view.extend({ + appName : 'internet-detector', + execPath : '/usr/bin/internet-detector', + configDir : '/etc/internet-detector', + ledsPath : '/sys/class/leds', + mtaPath : '/usr/bin/mailsend', + pollInterval : L.env.pollinterval, + appStatus : 'stoped', + initStatus : null, + inetStatus : null, + inetStatusArea : E('div', { 'class': 'cbi-value-field', 'id': 'inetStatusArea' }), + serviceStatusLabel : E('em', { 'id': 'serviceStatusLabel' }), + initButton : null, + currentAppMode : '0', + defaultHosts : [ '8.8.8.8', '1.1.1.1' ], + leds : [], + mm : false, + mta : false, + + callInitStatus: rpc.declare({ + object: 'luci', + method: 'getInitList', + params: [ 'name' ], + expect: { '': {} } + }), + + callInitAction: rpc.declare({ + object: 'luci', + method: 'setInitAction', + params: [ 'name', 'action' ], + expect: { result: false } + }), + + getInitStatus() { + return this.callInitStatus(this.appName).then(res => { + if(res) { + return res[this.appName].enabled; + } else { + throw _('Command failed'); + } + }).catch(e => { + ui.addNotification(null, + E('p', _('Failed to get %s init status: %s').format(this.appName, e))); + }); + }, + + handleServiceAction(action) { + return this.callInitAction(this.appName, action).then(success => { + if(!success) { + throw _('Command failed'); + }; + return true; + }).catch(e => { + ui.addNotification(null, + E('p', _('Service action failed "%s %s": %s').format(this.appName, action, e))); + }); + }, + + callUIPoll: rpc.declare({ + object: 'luci.internet-detector', + method: 'UIPoll', + expect: { '': {} } + }), + + getUIPoll() { + return this.callUIPoll().then(data => { + return data; + }); + }, + + setInternetStatus() { + this.inetStatusArea.innerHTML = ''; + + if(!this.inetStatus || !this.inetStatus.instances || this.inetStatus.instances.length === 0) { + let label = E('span', { 'class': 'id-label-status id-undefined' }, _('Undefined')); + if((this.currentAppMode === '1' && this.appStatus !== 'stoped') || this.currentAppMode === '2') { + label.classList.add('spinning'); + }; + this.inetStatusArea.append(label); + } else { + this.inetStatus.instances.sort((a, b) => a.num > b.num); + + for(let i of this.inetStatus.instances) { + let status = _('Disconnected'); + let className = 'id-label-status id-disconnected'; + if(i.inet == 0) { + status = _('Connected'); + className = 'id-label-status id-connected'; + } + else if(i.inet == -1) { + status = _('Undefined'); + className = 'id-label-status id-undefined spinning'; + }; + + let publicIp = (i.mod_public_ip !== undefined) ? + ' | %s: %s'.format(_('Public IP'), (i.mod_public_ip === '') ? _('Undefined') : _(i.mod_public_ip)) + : ''; + + this.inetStatusArea.append( + E('span', { 'class': className }, '%s%s%s'.format( + i.instance + ': ', status, publicIp) + ) + ); + }; + }; + + if(this.appStatus === 'running') { + this.serviceStatusLabel.textContent = _('Running'); + } else { + this.serviceStatusLabel.textContent = _('Stopped'); + }; + }, + + inetStatusFromJson(res) { + let inetStatData = null; + if(res.code === 0) { + try { + inetStatData = JSON.parse(res.stdout.trim()); + } catch(e) {}; + }; + return inetStatData; + }, + + servicePoll() { + return Promise.all([ + fs.exec(this.execPath, [ 'status' ]), + fs.exec(this.execPath, [ 'inet-status' ]), + ]).then(stat => { + let curAppStatus = (stat[0].code === 0) ? stat[0].stdout.trim() : null; + let inetStatData = this.inetStatusFromJson(stat[1]); + this.appStatus = curAppStatus; + this.inetStatus = inetStatData; + this.setInternetStatus(); + }).catch(e => { + this.appStatus = 'stoped'; + this.inetStatus = {}; + }); + }, + + uiPoll() { + return this.getUIPoll().then(status => { + this.inetStatus = status; + this.setInternetStatus(); + }); + }, + + serviceRestart() { + return this.handleServiceAction('restart'); + }, + + serviceRestartHandler() { + poll.stop(); + return this.serviceRestart().then(() => { + window.setTimeout(() => this.servicePoll(), 1000); + poll.start(); + }); + }, + + CBITimeInput: form.Value.extend({ + __name__ : 'CBI.TimeInput', + + renderWidget(section_id, option_index, cfgvalue) { + let value = (cfgvalue != null) ? cfgvalue : this.default, + widget = new Timefield(value, { + id : this.cbid(section_id), + optional : this.optional || this.rmempty, + maxlength : 3, + placeholder: _('Type a time string'), + validate : L.bind( + function(section, value) { + return (/^$|^\d+[hms]?$/.test(value)) ? true : _('Expecting:') + + ` ${_('One of the following:')}\n - ${_('hours')}: 2h\n - ${_('minutes')}: 10m\n - ${_('seconds')}: 30s\n`; + }, + this, + section_id + ), + disabled : (this.readonly != null) ? this.readonly : this.map.readonly, + }); + return widget.render(); + }, + }), + + CBIBlockInetStatus: form.Value.extend({ + __name__ : 'CBI.BlockInetStatus', + + __init__(map, section, ctx) { + this.map = map; + this.section = section; + this.ctx = ctx; + this.optional = true; + this.rmempty = true; + }, + + renderWidget(section_id, option_index, cfgvalue) { + this.ctx.setInternetStatus(); + + return E([ + E('label', { 'class': 'cbi-value-title', 'for': 'inetStatusArea' }, + _('Internet status') + ), this.ctx.inetStatusArea + ]) + }, + }), + + CBIBlockServiceStatus: form.Value.extend({ + __name__ : 'CBI.BlockServiceStatus', + + __init__(map, section, ctx) { + this.map = map; + this.section = section; + this.ctx = ctx; + this.optional = true; + this.rmempty = true; + }, + + renderWidget(section_id, option_index, cfgvalue) { + return E([ + E('label', { 'class': 'cbi-value-title', 'for': 'serviceStatusLabel' }, + _('Service') + ), + E('div', { 'class': 'cbi-value-field' }, + this.ctx.serviceStatusLabel + ), + ]); + }, + }), + + CBIBlockInitButton: form.Value.extend({ + __name__ : 'CBI.BlockInitButton', + + __init__(map, section, ctx) { + this.map = map; + this.section = section; + this.ctx = ctx; + this.optional = true; + this.rmempty = true; + }, + + renderWidget(section_id, option_index, cfgvalue) { + this.ctx.initButton = E('button', { + 'class': (!this.ctx.initStatus) ? btnStyleDisabled : btnStyleEnabled, + 'click': ui.createHandlerFn(this, () => { + return this.ctx.handleServiceAction( + (!this.ctx.initStatus) ? 'enable' : 'disable' + ).then(success => { + if(!success) { + return; + }; + if(!this.ctx.initStatus) { + this.ctx.initButton.textContent = _('Enabled'); + this.ctx.initButton.className = btnStyleEnabled; + this.ctx.initStatus = true; + } + else { + this.ctx.initButton.textContent = _('Disabled'); + this.ctx.initButton.className = btnStyleDisabled; + this.ctx.initStatus = false; + }; + }); + }), + }, (!this.ctx.initStatus) ? _('Disabled') : _('Enabled')); + + return E( [ + E('label', { 'class': 'cbi-value-title', 'for': 'initButton' }, + _('Run service at startup') + ), + E('div', { 'class': 'cbi-value-field' }, [ + E('div', {}, this.ctx.initButton), + E('input', { + 'id' : 'initButton', + 'type': 'hidden', + }), + ]), + ]); + }, + }), + + CBIBlockFileEdit: form.Value.extend({ + __name__ : 'CBI.BlockFileEdit', + + __init__(map, section, ctx, id, file, title, description, callback) { + this.map = map; + this.section = section; + this.ctx = ctx; + this.id = id, + this.optional = true; + this.rmempty = true; + this.file = file; + this.title = title; + this.description = description; + this.callback = callback; + this.content = ''; + }, + + cfgvalue(section_id, option) { + return this.content; + }, + + formvalue(section_id) { + let value = this.content; + let textarea = document.getElementById('widget.file_edit.content.' + this.id); + if(textarea) { + value = textarea.value.trim().replace(/\r\n/g, '\n') + '\n'; + }; + return value; + }, + + write(section_id, formvalue) { + return fs.write(this.file, formvalue).then(rc => { + ui.addNotification(null, E('p', _('Contents have been saved.')), + 'info'); + if(this.callback) { + return this.callback(rc); + }; + }).catch(e => { + ui.addNotification(null, E('p', _('Unable to save the contents') + + ': %s'.format(e.message))); + }); + }, + + load() { + return L.resolveDefault(fs.read(this.file), '').then(c => { + this.content = c; + }); + }, + + renderWidget(section_id, option_index, cfgvalue) { + return E('textarea', { + 'id' : 'widget.file_edit.content.' + this.id, + 'class' : 'cbi-input-textarea', + 'style' : 'width:100% !important', + 'rows' : 10, + 'wrap' : 'off', + 'spellcheck': 'false', + }, cfgvalue); + }, + }), + + load() { + return Promise.all([ + fs.exec(this.execPath, [ 'status' ]), + this.getInitStatus(), + L.resolveDefault(fs.list(this.ledsPath), []), + this.callInitStatus('modemmanager'), + L.resolveDefault(fs.stat(this.mtaPath), null), + uci.load(this.appName), + ]).catch(e => { + ui.addNotification(null, E('p', _('An error has occurred') + ': %s'.format(e.message))); + }); + }, + + render(data) { + if(!data) { + return; + }; + this.appStatus = (data[0].code === 0) ? data[0].stdout.trim() : null; + this.initStatus = data[1]; + this.leds = data[2]; + if(data[3].modemmanager) { + this.mm = true; + }; + if(data[4]) { + this.mta = true; + }; + this.currentAppMode = uci.get(this.appName, 'config', 'mode'); + + let s, o, ss; + let m = new form.Map(this.appName, + _('Internet Detector'), + _('Checking Internet availability.')); + + + /* Status widget */ + + s = m.section(form.NamedSection, 'config', 'main'); + o = s.option(this.CBIBlockInetStatus, this); + + + s = m.section(form.NamedSection, 'config', 'main'); + + + /* Service widget */ + + if(this.currentAppMode === '1') { + o = s.option(this.CBIBlockServiceStatus, this); + + // restart button + o = s.option(form.Button, + '_restart_btn', _('Restart service') + ); + o.onclick = () => this.serviceRestartHandler(); + o.inputtitle = _('Restart'); + o.inputstyle = btnStyleApply; + + // init button + o = s.option(this.CBIBlockInitButton, this); + }; + + + /* Main settings */ + + // mode + let mode = s.option(form.ListValue, 'mode', + _('Internet detector mode')); + mode.value('0', _('Disabled')); + mode.value('1', _('Service')); + mode.value('2', _('Web UI only (UI detector)')); + mode.description = '%s
%s
%s'.format( + _('Disabled: detector is completely off.'), + _('Service: detector always runs as a system service.'), + _('Web UI only: detector works only when the Web UI is open (UI detector).') + ); + + + /* Service instances configuration */ + + if(this.currentAppMode !== '2') { + + // enable_logger + o = s.option(form.Flag, 'enable_logger', + _('Enable logging'), + _('Write messages to the system log.') + ); + o.rmempty = false; + }; + + s = m.section(form.GridSection, 'instance'); + + s.title = _('Service instances'); + s.addremove = true; + s.sortable = true; + s.nodescriptions = true; + s.addbtntitle = _('Add instance'); + + s.tab('main', _('Main settings')); + + function makeIntervalOptions(list) { + list.value(2, '2 ' + _('sec')); + list.value(5, '5 ' + _('sec')); + list.value(10, '10 ' + _('sec')); + list.value(15, '15 ' + _('sec')); + list.value(20, '20 ' + _('sec')); + list.value(25, '25 ' + _('sec')); + list.value(30, '30 ' + _('sec')); + list.value(60, '1 ' + _('min')); + list.value(120, '2 ' + _('min')); + list.value(300, '5 ' + _('min')); + list.value(600, '10 ' + _('min')); + } + + // enabled + o = s.taboption('main', form.Flag, 'enabled', + _('Enabled'), + ); + o.rmempty = false; + o.default = '1'; + o.editable = true; + o.modalonly = false; + + // hosts + o = s.taboption('main', form.DynamicList, + 'hosts', _('Hosts'), + _('Hosts to check Internet availability. Hosts are polled (in list order) until at least one of them responds.') + ); + //o.datatype = 'or(host,hostport)'; + o.datatype = 'or(or(host,hostport),ipaddrport(1))'; + o.default = this.defaultHosts; + o.rmempty = false; + + // check_type + o = s.taboption('main', form.ListValue, + 'check_type', _('Check type'), + _('Host availability check type.') + ); + o.value(0, _('TCP port connection')); + o.value(1, _('ICMP-echo request (ping)')); + o.default = '0'; + o.modalonly = true; + + // tcp_port + o = s.taboption('main', form.Value, + 'tcp_port', _('TCP port'), + _('Default port value for TCP connections.') + ); + o.datatype = 'port'; + o.default = '53'; + o.depends({ check_type: '0' }); + o.modalonly = true; + + // icmp_packet_size + o = s.taboption('main', form.ListValue, + 'icmp_packet_size', _('ICMP packet data size')); + o.value(1, _('Small: 1 byte')); + o.value(32, _('Windows: 32 bytes')); + o.value(56, _('Standard: 56 bytes')); + o.value(248, _('Big: 248 bytes')); + o.value(1492, _('Huge: 1492 bytes')); + o.value(9000, _('Jumbo: 9000 bytes')); + o.default = '56'; + o.depends({ check_type: '1' }); + o.modalonly = true; + + // iface + o = s.taboption('main', widgets.DeviceSelect, + 'iface', _('Interface'), + _('Network interface for Internet access. If not specified, the default interface is used.') + ); + o.noaliases = true; + + // interval_up + o = s.taboption('main', form.ListValue, + 'interval_up', _('Alive interval'), + _('Hosts polling interval when the Internet is up.') + ); + o.default = '30'; + o.modalonly = true; + makeIntervalOptions(o); + + // interval_down + o = s.taboption('main', form.ListValue, + 'interval_down', _('Dead interval'), + _('Hosts polling interval when the Internet is down.') + ); + o.default = '5'; + o.modalonly = true; + makeIntervalOptions(o); + + // connection_attempts + o = s.taboption('main', form.ListValue, + 'connection_attempts', _('Connection attempts'), + _('Maximum number of attempts to connect to each host.') + ); + o.modalonly = true; + o.value(1); + o.value(2); + o.value(3); + o.value(4); + o.value(5); + o.default = '2'; + + // connection_timeout + o = s.taboption('main', form.ListValue, + 'connection_timeout', _('Connection timeout'), + _('Maximum timeout for waiting for a response from the host.') + ); + o.modalonly = true; + o.value(1, '1 ' + _('sec')); + o.value(2, '2 ' + _('sec')); + o.value(3, '3 ' + _('sec')); + o.value(4, '4 ' + _('sec')); + o.value(5, '5 ' + _('sec')); + o.value(6, '6 ' + _('sec')); + o.value(7, '7 ' + _('sec')); + o.value(8, '8 ' + _('sec')); + o.value(9, '9 ' + _('sec')); + o.value(10, '10 ' + _('sec')); + o.default = '2'; + + + /* Modules */ + + if(this.currentAppMode !== '2') { + s.tab('led_control', _('LED control')); + s.tab('reboot_device', _('Reboot device')); + s.tab('restart_network', _('Restart network')); + s.tab('restart_modem', _('Restart modem')); + }; + + s.tab('public_ip', _('Public IP address')); + + if(this.currentAppMode !== '2') { + s.tab('email', _('Email notification')); + s.tab('user_scripts', _('User scripts')); + }; + + s.addModalOptions = (s, section_id, ev) => { + + if(this.currentAppMode !== '2') { + + // LED control + + o = s.taboption('led_control', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('LED indicates the Internet status.') + + '
'; + o.modalonly = true; + + if(this.leds.length > 0) { + this.leds.sort((a, b) => a.name > b.name); + + // enabled + o = s.taboption('led_control', form.Flag, 'mod_led_control_enabled', + _('Enabled')); + o.rmempty = false; + o.modalonly = true; + + // led_name + o = s.taboption('led_control', form.ListValue, 'mod_led_control_led_name', + _('LED Name')); + o.depends({ mod_led_control_enabled: '1' }); + o.modalonly = true; + this.leds.forEach(e => o.value(e.name)); + + // led_action_1 + o = s.taboption('led_control', form.ListValue, 'mod_led_control_led_action_1', + _('Action when connected')); + o.depends({ mod_led_control_enabled: '1' }); + o.modalonly = true; + o.value(1, _('Off')); + o.value(2, _('On')); + o.value(3, _('Blink')); + o.default = '2'; + + // led_action_2 + o = s.taboption('led_control', form.ListValue, 'mod_led_control_led_action_2', + _('Action when disconnected')); + o.depends({ mod_led_control_enabled: '1' }); + o.modalonly = true; + o.value(1, _('Off')); + o.value(2, _('On')); + o.value(3, _('Blink')); + o.default = '1'; + } else { + o = s.taboption('led_control', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('No LEDs available...') + + '
'; + o.modalonly = true; + }; + + // Reboot device + + o = s.taboption('reboot_device', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('Device will be rebooted when the Internet is disconnected.') + + '
'; + o.modalonly = true; + + // enabled + o = s.taboption('reboot_device', form.Flag, 'mod_reboot_enabled', + _('Enabled')); + o.rmempty = false; + o.modalonly = true; + + // dead_period + o = s.taboption('reboot_device', this.CBITimeInput, + 'mod_reboot_dead_period', _('Dead period'), + _('Longest period of time without Internet access until the device is rebooted.') + ); + o.default = '3600'; + o.rmempty = false; + o.modalonly = true; + + // force_reboot_delay + o = s.taboption('reboot_device', form.ListValue, + 'mod_reboot_force_reboot_delay', _('Forced reboot delay'), + _('Waiting for a reboot to complete before performing a forced reboot.') + ); + o.modalonly = true; + o.value(0, _('Disable forced reboot')); + o.value(60, '1 ' + _('min')); + o.value(120, '2 ' + _('min')); + o.value(300, '5 ' + _('min')); + o.value(600, '10 ' + _('min')); + o.value(1800, '30 ' + _('min')); + o.value(3600, '1 ' + _('hour')); + o.default = '300'; + + // Restart network + + o = s.taboption('restart_network', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('Network will be restarted when the Internet is disconnected.') + + '
'; + o.modalonly = true; + + // enabled + o = s.taboption('restart_network', form.Flag, 'mod_network_restart_enabled', + _('Enabled')); + o.rmempty = false; + o.modalonly = true; + + // dead_period + o = s.taboption('restart_network', this.CBITimeInput, + 'mod_network_restart_dead_period', _('Dead period'), + _('Longest period of time without Internet access before network restart.') + ); + o.default = '900'; + o.rmempty = false; + o.modalonly = true; + + // attempts + o = s.taboption('restart_network', form.ListValue, + 'mod_network_restart_attempts', _('Restart attempts'), + _('Maximum number of network restart attempts before Internet access is available.') + ); + o.modalonly = true; + o.value(1); + o.value(2); + o.value(3); + o.value(4); + o.value(5); + o.default = '1'; + + // iface + o = s.taboption('restart_network', widgets.DeviceSelect, 'mod_network_restart_iface', + _('Interface'), + _('Network interface to restart. If not specified, then the network service is restarted.') + ); + o.modalonly = true; + + // restart_timeout + o = s.taboption('restart_network', form.ListValue, + 'mod_network_restart_restart_timeout', _('Restart timeout'), + _('Timeout between stopping and starting the interface.') + ); + o.modalonly = true; + o.value(0, '0 ' + _('sec')); + o.value(1, '1 ' + _('sec')); + o.value(2, '2 ' + _('sec')); + o.value(3, '3 ' + _('sec')); + o.value(4, '4 ' + _('sec')); + o.value(5, '5 ' + _('sec')); + o.value(6, '6 ' + _('sec')); + o.value(7, '7 ' + _('sec')); + o.value(8, '8 ' + _('sec')); + o.value(9, '9 ' + _('sec')); + o.value(10, '10 ' + _('sec')); + o.default = '0'; + + // Restart modem + + o = s.taboption('restart_modem', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('Modem will be restarted when the Internet is disconnected.') + + '
'; + o.modalonly = true; + + if(this.mm) { + + // enabled + o = s.taboption('restart_modem', form.Flag, 'mod_modem_restart_enabled', + _('Enabled'), + ); + o.rmempty = false; + o.modalonly = true; + + // dead_period + o = s.taboption('restart_modem', this.CBITimeInput, + 'mod_modem_restart_dead_period', _('Dead period'), + _('Longest period of time without Internet access before modem restart.') + ); + o.default = '600'; + o.rmempty = false; + o.modalonly = true; + + // any_band + o = s.taboption('restart_modem', form.Flag, + 'mod_modem_restart_any_band', _('Unlock modem bands'), + _('Set the modem to be allowed to use any band.') + ); + o.rmempty = false; + o.modalonly = true; + + // iface + o = s.taboption('restart_modem', widgets.NetworkSelect, 'mod_modem_restart_iface', + _('Interface'), + _('ModemManger interface. If specified, it will be restarted after restarting ModemManager.') + ); + o.multiple = false; + o.nocreate = true; + o.modalonly = true; + + } else { + o = s.taboption('restart_modem', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('ModemManager is not available...') + + '
'; + o.modalonly = true; + }; + + }; + + // Public IP address + + o = s.taboption('public_ip', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('Checking the real public IP address.') + + '
'; + o.modalonly = true; + + // enabled + o = s.taboption('public_ip', form.Flag, 'mod_public_ip_enabled', + _('Enabled')); + o.rmempty = false; + o.modalonly = true; + + // provider + o = s.taboption('public_ip', form.ListValue, + 'mod_public_ip_provider', _('DNS provider'), + _('Service for determining the public IP address through DNS.') + ); + o.modalonly = true; + o.value('opendns1'); + o.value('opendns2'); + o.value('opendns3'); + o.value('opendns4'); + o.value('akamai'); + o.value('google'); + o.default = 'opendns1'; + + // ipv6 + o = s.taboption('public_ip', form.ListValue, + 'mod_public_ip_qtype', _('DNS query type'), + _('The type of record requested in the DNS query (if the service supports it).') + ); + o.modalonly = true; + o.value('0', 'A (IPv4)'); + o.value('1', 'AAAA (IPv6)'); + o.default = '0'; + + // interval + o = s.taboption('public_ip', form.ListValue, + 'mod_public_ip_interval', _('Polling interval'), + _('Interval between IP address requests.') + ); + o.default = '600'; + o.modalonly = true; + o.value(60, '1' + ' ' + _('min')); + o.value(300, '5' + ' ' + _('min')); + o.value(600, '10' + ' ' + _('min')); + o.value(1800, '30' + ' ' + _('min')); + o.value(3600, '1' + ' ' + _('hour')); + o.value(10800, '3' + ' ' + _('hour')); + + // timeout + o = s.taboption('public_ip', form.ListValue, + 'mod_public_ip_timeout', _('Server response timeout') + ); + o.default = '3' + o.modalonly = true; + for(let i = 1; i <= 5; i++) { + o.value(i, i + ' ' + _('sec')); + }; + + if(this.currentAppMode !== '2') { + + // enable_ip_script + o = s.taboption('public_ip', form.Flag, 'mod_public_ip_enable_ip_script', + _('Enable public-ip-script')); + o.rmempty = false; + o.modalonly = true; + + // public-ip-script edit dialog + o = s.taboption('public_ip', this.CBIBlockFileEdit, this, + 'public-ip-script', + this.configDir + '/public-ip-script.' + s.section, + _('Edit public-ip-script'), + _('Shell commands that run when the public IP address changes. New IP is available as value of the $PUBLIC_IP variable (empty string if undefined).') + ); + o.modalonly = true; + + + // Email notification + + o = s.taboption('email', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('An email will be sent when the internet connection is restored after being disconnected.') + + '
'; + o.modalonly = true; + + if(this.mta) { + + // enabled + o = s.taboption('email', form.Flag, 'mod_email_enabled', + _('Enabled')); + o.rmempty = false; + o.modalonly = true; + + // alive_period + o = s.taboption('email', this.CBITimeInput, + 'mod_email_alive_period', _('Alive period'), + _('Longest period of time after connecting to the Internet before sending a message.') + ); + o.rmempty = false; + o.modalonly = true; + + // host_alias + o = s.taboption('email', form.Value, 'mod_email_host_alias', + _('Host alias'), + _('Host identifier in messages. If not specified, hostname will be used.')); + o.modalonly = true; + + // mail_recipient + o = s.taboption('email', form.Value, + 'mod_email_mail_recipient', _('Recipient')); + o.description = _('Email address of the recipient.'); + o.modalonly = true; + + // mail_sender + o = s.taboption('email', form.Value, + 'mod_email_mail_sender', _('Sender')); + o.description = _('Email address of the sender.'); + o.modalonly = true; + + // mail_user + o = s.taboption('email', form.Value, + 'mod_email_mail_user', _('User')); + o.description = _('Username for SMTP authentication.'); + o.modalonly = true; + + // mail_password + o = s.taboption('email', form.Value, + 'mod_email_mail_password', _('Password')); + o.description = _('Password for SMTP authentication.'); + o.password = true; + o.modalonly = true; + + // mail_smtp + o = s.taboption('email', form.Value, + 'mod_email_mail_smtp', _('SMTP server')); + o.description = _('Hostname/IP address of the SMTP server.'); + o.datatype = 'host'; + o.default = 'smtp.gmail.com'; + o.modalonly = true; + + // mail_smtp_port + o = s.taboption('email', form.Value, + 'mod_email_mail_smtp_port', _('SMTP server port')); + o.datatype = 'port'; + o.default = '587'; + o.modalonly = true; + + // mail_security + o = s.taboption('email', form.ListValue, + 'mod_email_mail_security', _('Security')); + o.description = '%s
%s'.format( + _('TLS: use STARTTLS if the server supports it.'), + _('SSL: SMTP over SSL.'), + ); + o.value('tls', 'TLS'); + o.value('ssl', 'SSL'); + o.default = 'tls'; + o.modalonly = true; + + } else { + o = s.taboption('email', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('Mailsend is not available...') + + '
'; + o.modalonly = true; + }; + + // User scripts + + o = s.taboption('user_scripts', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('Shell commands to run when connected or disconnected from the Internet.') + + '
'; + o.modalonly = true; + + // enabled + o = s.taboption('user_scripts', form.Flag, 'mod_user_scripts_enabled', + _('Enabled')); + o.rmempty = false; + o.modalonly = true; + + // up_script edit dialog + o = s.taboption('user_scripts', this.CBIBlockFileEdit, this, + 'up_script', + this.configDir + '/up-script.' + s.section, + _('Edit up-script'), + _('Shell commands that run when connected to the Internet.') + ); + o.modalonly = true; + + // alive_period + o = s.taboption('user_scripts', this.CBITimeInput, + 'mod_user_scripts_alive_period', _('Alive period'), + _('Longest period of time after connecting to Internet before "up-script" runs.') + ); + o.default = '0'; + o.rmempty = false; + o.modalonly = true; + + // down_script edit dialog + o = s.taboption('user_scripts', this.CBIBlockFileEdit, this, + 'down_script', + this.configDir + '/down-script.' + s.section, + _('Edit down-script'), + _('Shell commands to run when disconnected from the Internet.') + ); + o.modalonly = true; + + // dead_period + o = s.taboption('user_scripts', this.CBITimeInput, + 'mod_user_scripts_dead_period', _('Dead period'), + _('Longest period of time after disconnecting from Internet before "down-script" runs.') + ); + o.default = '0'; + o.rmempty = false; + o.modalonly = true; + }; + + }; + + if(this.currentAppMode !== '0') { + poll.add( + L.bind((this.currentAppMode === '1') ? this.servicePoll : this.uiPoll, this), + this.pollInterval + ); + }; + + let mapPromise = m.render(); + mapPromise.then(node => node.classList.add('fade-in')); + return mapPromise; + }, + + handleSaveApply(ev, mode) { + poll.stop(); + return this.handleSave(ev).then(() => { + ui.changes.apply(mode == '0'); + window.setTimeout(() => this.serviceRestart(), 3000); + }); + }, +}); diff --git a/luci-app-internet-detector/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js b/luci-app-internet-detector/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js new file mode 100644 index 000000000..c15556a4f --- /dev/null +++ b/luci-app-internet-detector/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js @@ -0,0 +1,150 @@ +'use strict'; +'require baseclass'; +'require fs'; +'require rpc'; +'require uci'; + +document.head.append(E('style', {'type': 'text/css'}, +` +:root { + --app-id-font-color: #454545; + --app-id-font-shadow: #fff; + --app-id-connected-color: #6bdebb; + --app-id-disconnected-color: #f8aeba; + --app-id-undefined-color: #dfdfdf; +} +:root[data-darkmode="true"] { + --app-id-font-color: #f6f6f6; + --app-id-font-shadow: #4d4d4d; + --app-id-connected-color: #005F20; + --app-id-disconnected-color: #a93734; + --app-id-undefined-color: #4d4d4d; +} +.id-connected { + --on-color: var(--app-id-font-color); + background-color: var(--app-id-connected-color) !important; + border-color: var(--app-id-connected-color) !important; + color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); +} +.id-disconnected { + --on-color: var(--app-id-font-color); + background-color: var(--app-id-disconnected-color) !important; + border-color: var(--app-id-disconnected-color) !important; + color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); +} +.id-undefined { + --on-color: var(--app-id-font-color); + background-color: var(--app-id-undefined-color) !important; + border-color: var(--app-id-undefined-color) !important; + color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); +} +.id-label-status { + display: inline-block; + word-wrap: break-word; + margin: 2px !important; + padding: 4px 8px; + border: 1px solid; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + font-weight: bold; +} +`)); + +return baseclass.extend({ + title : _('Internet'), + appName : 'internet-detector', + execPath : '/usr/bin/internet-detector', + currentAppMode : null, + inetStatus : null, + + callUIPoll: rpc.declare({ + object: 'luci.internet-detector', + method: 'UIPoll', + expect: { '': {} } + }), + + getUIPoll() { + return this.callUIPoll().then(data => { + return data; + }); + }, + + inetStatusFromJson(res) { + let inetStatData = null; + if(res.code === 0) { + try { + inetStatData = JSON.parse(res.stdout.trim()); + } catch(e) {}; + }; + return inetStatData; + }, + + async load() { + if(!this.currentAppMode) { + await uci.load(this.appName).then(data => { + this.currentAppMode = uci.get(this.appName, 'config', 'mode'); + }).catch(e => {}); + }; + + if(this.currentAppMode === '2') { + return this.getUIPoll(); + } + else if(this.currentAppMode === '1') { + return L.resolveDefault(fs.exec(this.execPath, [ 'inet-status' ]), null); + }; + }, + + render(data) { + if(this.currentAppMode === '0') { + return; + } + else if(this.currentAppMode === '1' && data) { + data = this.inetStatusFromJson(data); + }; + this.inetStatus = data; + + let inetStatusArea = E('div', {}); + + if(!this.inetStatus || !this.inetStatus.instances || this.inetStatus.instances.length === 0) { + let label = E('span', { 'class': 'id-label-status id-undefined' }, _('Undefined')); + if(this.currentAppMode === '2') { + label.classList.add('spinning'); + }; + inetStatusArea.append(label); + } else { + this.inetStatus.instances.sort((a, b) => a.num > b.num); + + for(let i of this.inetStatus.instances) { + let status = _('Disconnected'); + let className = 'id-label-status id-disconnected'; + if(i.inet == 0) { + status = _('Connected'); + className = 'id-label-status id-connected'; + } + else if(i.inet == -1) { + status = _('Undefined'); + className = 'id-label-status id-undefined spinning'; + }; + + let publicIp = (i.mod_public_ip !== undefined) ? + ' | %s: %s'.format(_('Public IP'), (i.mod_public_ip === '') ? _('Undefined') : _(i.mod_public_ip)) + : ''; + + inetStatusArea.append( + E('span', { 'class': className }, '%s%s%s'.format( + i.instance + ': ', status, publicIp) + ) + ); + }; + }; + + return E('div', { + 'class': 'cbi-section', + 'style': 'margin-bottom:1em', + }, inetStatusArea); + }, +}); diff --git a/luci-app-internet-detector/luci-app-internet-detector/po/ru/internet-detector.po b/luci-app-internet-detector/luci-app-internet-detector/po/ru/internet-detector.po new file mode 100644 index 000000000..2b75ae28d --- /dev/null +++ b/luci-app-internet-detector/luci-app-internet-detector/po/ru/internet-detector.po @@ -0,0 +1,499 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.2\n" +"Last-Translator: \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? " +"1 : 2);\n" +"Language: ru\n" + +msgid "LED Name" +msgstr "Имя LED" + +msgid "LED control" +msgstr "Управление LED" + +msgid "" +"LED indicates the Internet status." +msgstr "LED отображает статус Интернет." + +msgid "Action when connected" +msgstr "Действие при подключении" + +msgid "Action when disconnected" +msgstr "Действие при отключении" + +msgid "Add instance" +msgstr "Добавить экземпляр" + +msgid "Alive interval" +msgstr "Интервал при подключении" + +msgid "Alive period" +msgstr "Период после подключения" + +msgid "" +"An email will be sent when the internet connection is restored after being " +"disconnected." +msgstr "Сообщение будет отправлено при восстановлении соединения с Интернет после отключения." + +msgid "An error has occurred" +msgstr "Произошла ошибка" + +msgid "Big: 248 bytes" +msgstr "Большой: 248 байт" + +msgid "Blink" +msgstr "Мигание" + +msgid "Check type" +msgstr "Тип проверки" + +msgid "Checking Internet availability." +msgstr "Проверка доступности Интернет." + +msgid "Checking the real public IP address." +msgstr "Проверка реального публичного IP адреса." + +msgid "Command failed" +msgstr "Команда не выполнена" + +msgid "Connected" +msgstr "Подключен" + +msgid "Connection attempts" +msgstr "Попытки подключения" + +msgid "Connection timeout" +msgstr "Таймаут соединения" + +msgid "Contents have been saved." +msgstr "Содержимое сохранено." + +msgid "Dead interval" +msgstr "Интервал при отключении" + +msgid "Dead period" +msgstr "Период после отключения" + +msgid "Default port value for TCP connections." +msgstr "Стандартное значение порта для TCP-подключений." + +msgid "Device will be rebooted when the Internet is disconnected." +msgstr "Устройство будет перезагружено при отключении Интернет." + +msgid "Disable forced reboot" +msgstr "Отключить принудительную перезагрузку" + +msgid "Disabled" +msgstr "Отключен" + +msgid "Disabled: detector is completely off." +msgstr "Отключен: детектор полностью выключен." + +msgid "Disconnected" +msgstr "Отключен" + +msgid "Dismiss" +msgstr "Закрыть" + +msgid "DNS query type" +msgstr "Тип DNS-запроса" + +msgid "DNS provider" +msgstr "DNS провайдер" + +msgid "Edit" +msgstr "Изменить" + +msgid "Edit down-script" +msgstr "Изменить down-script" + +msgid "Edit public-ip-script" +msgstr "Изменить public-ip-script" + +msgid "Edit up-script" +msgstr "Изменить up-script" + +msgid "Email notification" +msgstr "Уведомление по email" + +msgid "Email address of the recipient." +msgstr "Email-адрес получателя." + +msgid "Email address of the sender." +msgstr "Email-адрес отправителя." + +msgid "Enable" +msgstr "Включить" + +msgid "Enable logging" +msgstr "Запись событий в лог" + +msgid "Enable public-ip-script" +msgstr "Включить public-ip-script" + +msgid "Enabled" +msgstr "Включен" + +msgid "Expecting:" +msgstr "Ожидается:" + +msgid "Public IP" +msgstr "Публичный IP" + +msgid "Public IP address" +msgstr "Публичный IP адрес" + +msgid "Failed to get %s init status: %s" +msgstr "Не удалось получить статус инициализации %s: %s" + +msgid "Forced reboot delay" +msgstr "Задержка принудительной перезагрузки" + +msgid "Host alias" +msgstr "Псевдоним хоста" + +msgid "Host availability check type." +msgstr "Тип проверки доступности хоста." + +msgid "Host identifier in messages. If not specified, hostname will be used." +msgstr "Идентификатор хоста в сообщениях. Если не указан, будет использовано имя хоста." + +msgid "Hostname/IP address of the SMTP server." +msgstr "Имя хоста/IP-адрес SMTP-сервера." + +msgid "Hosts" +msgstr "Хосты" + +msgid "Hosts polling interval when the Internet is down." +msgstr "Интервал опроса хостов если Интернет не доступен." + +msgid "Hosts polling interval when the Internet is up." +msgstr "Интервал опроса хостов если Интернет доступен." + +msgid "" +"Hosts to check Internet availability. Hosts are polled (in list order) until " +"at least one of them responds." +msgstr "" +"Хосты для проверки доступности Интернет. Хосты опрашиваются (в порядке " +"списка) до тех пор, пока хотя бы один из них не ответит." + +msgid "Huge: 1492 bytes" +msgstr "Огромный: 1492 байта" + +msgid "ICMP-echo request (ping)" +msgstr "Запрос ICMP-echo (ping)" + +msgid "ICMP packet data size" +msgstr "Размер данных ICMP-пакета" + +msgid "Interface" +msgstr "Интерфейс" + +msgid "Internet" +msgstr "Интернет" + +msgid "Internet Detector" +msgstr "Интернет-детектор" + +msgid "Internet detector mode" +msgstr "Режим интернет-детектора" + +msgid "Internet status" +msgstr "Статус Интернет" + +msgid "Interval between IP address requests." +msgstr "Интервал между запросами IP адреса." + +msgid "Jumbo: 9000 bytes" +msgstr "Гигантский: 9000 байт" + +msgid "LED control" +msgstr "Управление LED" + +msgid "Loading" +msgstr "Загрузка" + +msgid "" +"Longest period of time after connecting to Internet before \"up-script\" " +"runs." +msgstr "" +"Максимальный промежуток времени после подключения к Интернет перед запуском " +"\"up-script\"." + +msgid "" +"Longest period of time after connecting to the Internet before sending a " +"message." +msgstr "Максимальный промежуток времени после подключения Интернет перед отправкой сообщения." + +msgid "" +"Longest period of time after disconnecting from Internet before \"down-script" +"\" runs." +msgstr "" +"Максимальный промежуток времени после отключения Интернет перед запуском " +"\"down-script\"." + +msgid "Longest period of time without Internet access before modem restart." +msgstr "" +"Максимальное время отсутствия доступа в Интренет перед перезапуском модема." + +msgid "Longest period of time without Internet access before network restart." +msgstr "" +"Максимальное время отсутствия доступа в Интренет перед перезапуском сети." + +msgid "" +"Longest period of time without Internet access until the device is rebooted." +msgstr "" +"Максимальное время отсутствия доступа в Интренет перед перезагрузкой " +"устройства." + +msgid "Mailsend is not available..." +msgstr "Mailsend недоступен..." + +msgid "Main settings" +msgstr "Основные настройки" + +msgid "Maximum number of attempts to connect to each host." +msgstr "Максимальное количество попыток подключения к каждому хосту." + +msgid "" +"Maximum number of network restart attempts before Internet access is " +"available." +msgstr "" +"Максимальное количество попыток перезапуска сети до появления доступа в " +"Интренет." + +msgid "Maximum timeout for waiting for a response from the host." +msgstr "Максимальный таймаут ожидания ответа от хоста." + +msgid "Modem will be restarted when the Internet is disconnected." +msgstr "Модем будет перезапущен при отключении Интернет." + +msgid "ModemManager is not available..." +msgstr "ModemManager недоступен..." + +msgid "" +"ModemManger interface. If specified, it will be restarted after restarting " +"ModemManager." +msgstr "" +"Интерфейс ModemManager. Если задан, то будет перезапущен после перезапуска " +"ModemManger." + +msgid "" +"Network interface for Internet access. If not specified, the default " +"interface is used." +msgstr "" +"Сетевой интерфейс для доступа в Интернет. Если не указан, используется " +"интерфейс по умолчанию." + +msgid "" +"Network interface to restart. If not specified, then the network service is restarted." +msgstr "" +"Сетевой интерфейс для перезапуска. Если не задан, то будет перезапущена сетевая " +"служба." + +msgid "Network will be restarted when the Internet is disconnected." +msgstr "Сеть будет перезапущена при отключении Интернет." + +msgid "No LEDs available..." +msgstr "Нет доступных LED..." + +msgid "Off" +msgstr "Выключить" + +msgid "On" +msgstr "Включить" + +msgid "One of the following:" +msgstr "Одно из следующих значений:" + +msgid "Password" +msgstr "Пароль" + +msgid "Password for SMTP authentication." +msgstr "Пароль для SMTP-аутентификации." + +msgid "Polling interval" +msgstr "Интервал опроса" + +msgid "Reboot device" +msgstr "Перезагрузка устройства" + +msgid "Recipient" +msgstr "Получатель" + +msgid "Restart" +msgstr "Перезапуск" + +msgid "Restart attempts" +msgstr "Попытки перезапуска" + +msgid "Restart modem" +msgstr "Перезапуск модема" + +msgid "Restart network" +msgstr "Перезапуск сети" + +msgid "Restart service" +msgstr "Перезапуск службы" + +msgid "Restart timeout" +msgstr "Таймаут перезапуска" + +msgid "Run service at startup" +msgstr "Запуск службы при старте" + +msgid "Running" +msgstr "Выполняется" + +msgid "SMTP server" +msgstr "SMTP-сервер" + +msgid "SMTP server port" +msgstr "Порт SMTP-сервера" + +msgid "SSL: SMTP over SSL." +msgstr "SSL: SMTP поверх SSL." + +msgid "Save" +msgstr "Сохранить" + +msgid "Security" +msgstr "Безопасность" + +msgid "Sender" +msgstr "Отправитель" + +msgid "Server response timeout" +msgstr "Таймаут ответа сервера" + +msgid "Service" +msgstr "Служба" + +msgid "Service action failed \"%s %s\": %s" +msgstr "Не удалось выполнить действие службы \"%s %s\": %s" + +msgid "Service configuration" +msgstr "Конфигурация службы" + +msgid "Service for determining the public IP address through DNS." +msgstr "Сервис для определения публичного IP адреса через DNS." + +msgid "Service: detector always runs as a system service." +msgstr "Служба: детектор работает постоянно, как системная служба." + +msgid "Service instances" +msgstr "Экземпляры службы" + +msgid "Set the modem to be allowed to use any band." +msgstr "Разрешить модему использование любой частоты." + +msgid "Shell commands that run when connected to the Internet." +msgstr "Команды shell выполняемые при подключении к Интернет." + +msgid "Shell commands that run when the public IP address changes. New IP is available as value of the $PUBLIC_IP variable (empty string if undefined)." +msgstr "Команды shell выполняемые при изменении публичного IP адреса. Новый IP доступен как значение переменной $PUBLIC_IP (пустая строка если не определён)." + +msgid "Shell commands to run when connected or disconnected from the Internet." +msgstr "Команды shell выполняемые при подключении или отключении Интернет." + +msgid "Shell commands to run when disconnected from the Internet." +msgstr "Команды shell выполняемые при отключении от Интернет." + +msgid "Small: 1 byte" +msgstr "Маленький: 1 байт" + +msgid "Standard: 56 bytes" +msgstr "Стандартный: 56 байт" + +msgid "Stopped" +msgstr "Остановлена" + +msgid "TCP port" +msgstr "TCP-порт" + +msgid "TCP port connection" +msgstr "Подключение к TCP-порту" + +msgid "The type of record requested in the DNS query (if the service supports it)." +msgstr "Тип записи запрашиваемой в DNS-запросе (если сервис поддерживает)." + +msgid "TLS: use STARTTLS if the server supports it." +msgstr "TLS: использовать STARTTLS если сервер поддерживает." + +msgid "Timeout between stopping and starting the interface." +msgstr "Таймаут между остановкой и запуском интерфейса." + +msgid "Type a time string" +msgstr "Введите строку времени" + +msgid "Unable to read the contents" +msgstr "Невозможно прочитать содержимое" + +msgid "Unable to save the contents" +msgstr "Невозможно сохранить содержимое" + +msgid "Undefined" +msgstr "Неопределён" + +msgid "Unlock modem bands" +msgstr "Освободить частоты модема" + +msgid "User" +msgstr "Пользователь" + +msgid "User scripts" +msgstr "Пользовательские скрипты" + +msgid "Username for SMTP authentication." +msgstr "Имя пользователя для SMTP-аутентификации." + +msgid "Waiting for a reboot to complete before performing a forced reboot." +msgstr "" +"Ожидание завершения перезагрузки перед выполнением принудительной " +"перезагрузки." + +msgid "Web UI only (UI detector)" +msgstr "Только web-интерфейс (UI детектор)" + +msgid "Web UI only: detector works only when the Web UI is open (UI detector)." +msgstr "" +"Только web-интерфейс: детектор работает только в web-интерфейсе (UI " +"детектор)." + +msgid "Windows: 32 bytes" +msgstr "Windows: 32 байта" + +msgid "Write messages to the system log." +msgstr "Записывать сообщения в системный журнал." + +msgid "down-script" +msgstr "down-script" + +msgid "hour" +msgstr "час" + +msgid "hours" +msgstr "часы" + +msgid "min" +msgstr "мин" + +msgid "minutes" +msgstr "минуты" + +msgid "sec" +msgstr "сек" + +msgid "seconds" +msgstr "секунды" + +msgid "up-script" +msgstr "up-script" diff --git a/luci-app-internet-detector/luci-app-internet-detector/po/templates/internet-detector.pot b/luci-app-internet-detector/luci-app-internet-detector/po/templates/internet-detector.pot new file mode 100644 index 000000000..bd5881485 --- /dev/null +++ b/luci-app-internet-detector/luci-app-internet-detector/po/templates/internet-detector.pot @@ -0,0 +1,465 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +msgid "LED Name" +msgstr "" + +msgid "LED control" +msgstr "" + +msgid "" +"LED indicates the Internet status." +msgstr "" + +msgid "Action when connected" +msgstr "" + +msgid "Action when disconnected" +msgstr "" + +msgid "Add instance" +msgstr "" + +msgid "Alive interval" +msgstr "" + +msgid "Alive period" +msgstr "" + +msgid "" +"An email will be sent when the internet connection is restored after being " +"disconnected." +msgstr "" + +msgid "An error has occurred" +msgstr "" + +msgid "Big: 248 bytes" +msgstr "" + +msgid "Blink" +msgstr "" + +msgid "Check type" +msgstr "" + +msgid "Checking Internet availability." +msgstr "" + +msgid "Checking the real public IP address." +msgstr "" + +msgid "Command failed" +msgstr "" + +msgid "Connected" +msgstr "" + +msgid "Connection attempts" +msgstr "" + +msgid "Connection timeout" +msgstr "" + +msgid "Contents have been saved." +msgstr "" + +msgid "Dead interval" +msgstr "" + +msgid "Dead period" +msgstr "" + +msgid "Default port value for TCP connections." +msgstr "" + +msgid "Device will be rebooted when the Internet is disconnected." +msgstr "" + +msgid "Disable forced reboot" +msgstr "" + +msgid "Disabled" +msgstr "" + +msgid "Disabled: detector is completely off." +msgstr "" + +msgid "Disconnected" +msgstr "" + +msgid "Dismiss" +msgstr "" + +msgid "DNS query type" +msgstr "" + +msgid "DNS provider" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Edit down-script" +msgstr "" + +msgid "Edit public-ip-script" +msgstr "" + +msgid "Edit up-script" +msgstr "" + +msgid "Email notification" +msgstr "" + +msgid "Email address of the recipient." +msgstr "" + +msgid "Email address of the sender." +msgstr "" + +msgid "Enable" +msgstr "" + +msgid "Enable logging" +msgstr "" + +msgid "Enable public-ip-script" +msgstr "" + +msgid "Enabled" +msgstr "" + +msgid "Expecting:" +msgstr "" + +msgid "Public IP" +msgstr "" + +msgid "Public IP address" +msgstr "" + +msgid "Failed to get %s init status: %s" +msgstr "" + +msgid "Forced reboot delay" +msgstr "" + +msgid "Host alias" +msgstr "" + +msgid "Host availability check type." +msgstr "" + +msgid "Host identifier in messages. If not specified, hostname will be used." +msgstr "" + +msgid "Hostname/IP address of the SMTP server." +msgstr "" + +msgid "Hosts" +msgstr "" + +msgid "Hosts polling interval when the Internet is down." +msgstr "" + +msgid "Hosts polling interval when the Internet is up." +msgstr "" + +msgid "" +"Hosts to check Internet availability. Hosts are polled (in list order) until " +"at least one of them responds." +msgstr "" + +msgid "Huge: 1492 bytes" +msgstr "" + +msgid "ICMP-echo request (ping)" +msgstr "" + +msgid "ICMP packet data size" +msgstr "" + +msgid "Interface" +msgstr "" + +msgid "Internet" +msgstr "" + +msgid "Internet Detector" +msgstr "" + +msgid "Internet detector mode" +msgstr "" + +msgid "Internet status" +msgstr "" + +msgid "Interval between IP address requests." +msgstr "" + +msgid "Jumbo: 9000 bytes" +msgstr "" + +msgid "LED control" +msgstr "" + +msgid "Loading" +msgstr "" + +msgid "" +"Longest period of time after connecting to Internet before \"up-script\" " +"runs." +msgstr "" + +msgid "" +"Longest period of time after connecting to the Internet before sending a " +"message." +msgstr "" + +msgid "" +"Longest period of time after disconnecting from Internet before \"down-script" +"\" runs." +msgstr "" + +msgid "Longest period of time without Internet access before modem restart." +msgstr "" + +msgid "Longest period of time without Internet access before network restart." +msgstr "" + +msgid "" +"Longest period of time without Internet access until the device is rebooted." +msgstr "" + +msgid "Mailsend is not available..." +msgstr "" + +msgid "Main settings" +msgstr "" + +msgid "Maximum number of attempts to connect to each host." +msgstr "" + +msgid "" +"Maximum number of network restart attempts before Internet access is " +"available." +msgstr "" + +msgid "Maximum timeout for waiting for a response from the host." +msgstr "" + +msgid "Modem will be restarted when the Internet is disconnected." +msgstr "" + +msgid "ModemManager is not available..." +msgstr "" + +msgid "" +"ModemManger interface. If specified, it will be restarted after restarting " +"ModemManager." +msgstr "" + +msgid "" +"Network interface for Internet access. If not specified, the default " +"interface is used." +msgstr "" + +msgid "" +"Network interface to restart. If not specified, then the network service is restarted." +msgstr "" + +msgid "Network will be restarted when the Internet is disconnected." +msgstr "" + +msgid "No LEDs available..." +msgstr "" + +msgid "Off" +msgstr "" + +msgid "On" +msgstr "" + +msgid "One of the following:" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password for SMTP authentication." +msgstr "" + +msgid "Polling interval" +msgstr "" + +msgid "Reboot device" +msgstr "" + +msgid "Recipient" +msgstr "" + +msgid "Restart" +msgstr "" + +msgid "Restart attempts" +msgstr "" + +msgid "Restart modem" +msgstr "" + +msgid "Restart network" +msgstr "" + +msgid "Restart service" +msgstr "" + +msgid "Restart timeout" +msgstr "" + +msgid "Run service at startup" +msgstr "" + +msgid "Running" +msgstr "" + +msgid "SMTP server" +msgstr "" + +msgid "SMTP server port" +msgstr "" + +msgid "SSL: SMTP over SSL." +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Security" +msgstr "" + +msgid "Sender" +msgstr "" + +msgid "Server response timeout" +msgstr "" + +msgid "Service" +msgstr "" + +msgid "Service action failed \"%s %s\": %s" +msgstr "" + +msgid "Service configuration" +msgstr "" + +msgid "Service for determining the public IP address through DNS." +msgstr "" + +msgid "Service: detector always runs as a system service." +msgstr "" + +msgid "Service instances" +msgstr "" + +msgid "Set the modem to be allowed to use any band." +msgstr "" + +msgid "Shell commands that run when connected to the Internet." +msgstr "" + +msgid "Shell commands that run when the public IP address changes. New IP is available as value of the $PUBLIC_IP variable (empty string if undefined)." +msgstr "" + +msgid "Shell commands to run when connected or disconnected from the Internet." +msgstr "" + +msgid "Shell commands to run when disconnected from the Internet." +msgstr "" + +msgid "Small: 1 byte" +msgstr "" + +msgid "Standard: 56 bytes" +msgstr "" + +msgid "Stopped" +msgstr "" + +msgid "TCP port" +msgstr "" + +msgid "TCP port connection" +msgstr "" + +msgid "The type of record requested in the DNS query (if the service supports it)." +msgstr "" + +msgid "TLS: use STARTTLS if the server supports it." +msgstr "" + +msgid "Timeout between stopping and starting the interface." +msgstr "" + +msgid "Type a time string" +msgstr "" + +msgid "Unable to read the contents" +msgstr "" + +msgid "Unable to save the contents" +msgstr "" + +msgid "Undefined" +msgstr "" + +msgid "Unlock modem bands" +msgstr "" + +msgid "User" +msgstr "" + +msgid "User scripts" +msgstr "" + +msgid "Username for SMTP authentication." +msgstr "" + +msgid "Waiting for a reboot to complete before performing a forced reboot." +msgstr "" + +msgid "Web UI only (UI detector)" +msgstr "" + +msgid "Web UI only: detector works only when the Web UI is open (UI detector)." +msgstr "" + +msgid "Windows: 32 bytes" +msgstr "" + +msgid "Write messages to the system log." +msgstr "" + +msgid "down-script" +msgstr "" + +msgid "hour" +msgstr "" + +msgid "hours" +msgstr "" + +msgid "min" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "sec" +msgstr "" + +msgid "seconds" +msgstr "" + +msgid "up-script" +msgstr "" diff --git a/luci-app-internet-detector/luci-app-internet-detector/root/usr/libexec/rpcd/luci.internet-detector b/luci-app-internet-detector/luci-app-internet-detector/root/usr/libexec/rpcd/luci.internet-detector new file mode 100755 index 000000000..6ff71d0f1 --- /dev/null +++ b/luci-app-internet-detector/luci-app-internet-detector/root/usr/libexec/rpcd/luci.internet-detector @@ -0,0 +1,46 @@ +#!/bin/sh + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +readonly ID_EXEC="/usr/bin/internet-detector" + +run_instance() { + config_get enabled "$1" enabled "0" + if [ $enabled = "1" ]; then + $ID_EXEC service "$1" > /dev/null 2>&1 + fi +} + +start_ui_instances() { + config_load internet-detector + config_get mode "config" mode "0" + if [ $mode = "2" ]; then + config_foreach run_instance "instance" + fi +} + +ui_poll() { + $ID_EXEC uipoll + if [ $? -eq 126 ]; then + start_ui_instances + $ID_EXEC inet-status + fi +} + +case "$1" in + list) + json_init + json_add_object "UIPoll" + json_close_object + json_dump + json_cleanup + ;; + call) + case "$2" in + UIPoll) + ui_poll + ;; + esac + ;; +esac diff --git a/luci-app-internet-detector/luci-app-internet-detector/root/usr/share/luci/menu.d/luci-app-internet-detector.json b/luci-app-internet-detector/luci-app-internet-detector/root/usr/share/luci/menu.d/luci-app-internet-detector.json new file mode 100644 index 000000000..2299a2c03 --- /dev/null +++ b/luci-app-internet-detector/luci-app-internet-detector/root/usr/share/luci/menu.d/luci-app-internet-detector.json @@ -0,0 +1,17 @@ +{ + "admin/services/internet-detector": { + "title": "Internet Detector", + "order": 80, + "action": { + "type": "view", + "path": "internet-detector" + }, + "depends": { + "acl": [ "luci-app-internet-detector" ], + "fs": { + "/usr/bin/internet-detector": "executable" + }, + "uci": { "internet-detector": true } + } + } +} diff --git a/luci-app-internet-detector/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json b/luci-app-internet-detector/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json new file mode 100644 index 000000000..7bc459ef2 --- /dev/null +++ b/luci-app-internet-detector/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json @@ -0,0 +1,28 @@ +{ + "luci-app-internet-detector": { + "description": "Grant access to internet-detector procedures", + "read": { + "file": { + "/sys/class/leds": [ "list" ], + "/etc/internet-detector/up-script*": [ "read" ], + "/etc/internet-detector/down-script*": [ "read" ], + "/etc/internet-detector/public-ip-script*": [ "read" ], + "/usr/bin/internet-detector*": [ "exec" ], + "/usr/bin/mailsend": [ "exec" ] + }, + "uci": [ "internet-detector" ], + "ubus": { + "luci": [ "getInitList", "setInitAction" ], + "luci.internet-detector": [ "UIPoll" ] + } + }, + "write": { + "file": { + "/etc/internet-detector/up-script*": [ "write" ], + "/etc/internet-detector/down-script*": [ "write" ], + "/etc/internet-detector/public-ip-script*": [ "write" ] + }, + "uci": [ "internet-detector" ] + } + } +} diff --git a/luci-app-internet-detector/screenshots/01.jpg b/luci-app-internet-detector/screenshots/01.jpg new file mode 100644 index 000000000..cef637fc1 Binary files /dev/null and b/luci-app-internet-detector/screenshots/01.jpg differ diff --git a/luci-app-internet-detector/screenshots/02.jpg b/luci-app-internet-detector/screenshots/02.jpg new file mode 100644 index 000000000..c0794b6de Binary files /dev/null and b/luci-app-internet-detector/screenshots/02.jpg differ diff --git a/luci-app-internet-detector/screenshots/03.jpg b/luci-app-internet-detector/screenshots/03.jpg new file mode 100644 index 000000000..5fd9b1503 Binary files /dev/null and b/luci-app-internet-detector/screenshots/03.jpg differ diff --git a/luci-app-internet-detector/screenshots/internet-led.jpg b/luci-app-internet-detector/screenshots/internet-led.jpg new file mode 100644 index 000000000..d24bc3794 Binary files /dev/null and b/luci-app-internet-detector/screenshots/internet-led.jpg differ diff --git a/luci-app-natmap/LICENSE b/luci-app-natmap/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/luci-app-natmap/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/luci-app-natmap/README.md b/luci-app-natmap/README.md new file mode 100644 index 000000000..bc0699d78 --- /dev/null +++ b/luci-app-natmap/README.md @@ -0,0 +1,83 @@ +## 介绍 + +#### 本脚本为基于 openwrt master 分支的 natmap 插件 + +## 基本功能 + +### 1.目前支持第三方服务调用功能 +#### 1.1.qBittorrent +打洞成功后,自动修改 qBittorrent 的端口号,并配置转发(可选)。 +需要配置 qBittorrent 地址、账号、密码用于修改端口。 +需要配置 qBittorrent 使用网卡的 IP 用于配置转发,端口填 0,会转发到修改后的端口。 + +#### 1.2.Transmission +打洞成功后,自动修改 Transmission 的端口号,并配置转发(可选)。 +需要配置 Transmission 地址、账号、密码用于修改端口。 +需要配置 Transmission 使用网卡的 IP 用于配置转发,端口填 0,会转发到修改后的端口。 + +#### 1.3.Emby +配合 Emby Connect 使用时,用户登录账号后,会从服务器获取最新的连接地址信息,此模式就是用于配置这些信息的。 +需要配置 Emby 地址和 API Key 用于修改连接地址信息。 +此模式必须配置转发,默认不更新「外部域」,如果有配置 DDNS,将 DDNS 域名填入外部域后将不需要再次修改。 +若没有域名,需要将 IP 填入外部域,可以勾选 「Update host with IP」,若对外提供的是 HTTPS 服务,需要勾选 「Update HTTPS Port」。 + +#### 1.4.Cloudflare Origin Rules +Cloudflare Origin Rules 可以设置回源端口,配合 DDNS 使用时,可以将 DDNS 域名指向 Cloudflare,然后将回源端口设置为打洞后的端口,这样就可以通过 Cloudflare 的 CDN 加速访问。 +需要配置 Cloudflare 的 API Key,邮箱 和 Zone ID,Zone ID 可以在 Cloudflare 的域名首页找到。 +API Key 请访问 https://dash.cloudflare.com/profile/api-tokens 复制 Global API Key。 +需要先在 Cloudflare 后台的 Rules - Origin Rules 下添加一个 Origin Rules,然后将 Origin Rules 的 Name 填入配置中。 +注意:Name 请保持唯一,否则会出现奇怪的问题。 + +#### 1.5.Cloudflare Redirect Rules + + +### 2.目前支持的通知功能 +#### 2.1. Telegram Bot +#### 2.2. PushPlus +#### 2.3. server酱 +#### 2.4. Gotify + +### 3.端口转发功能 +#### 3.1.natmap转发 +支持使用natmap转发tcp和udp.. + +#### 3.2.firewall dnat转发 +支持使用openwrt防火墙转发tcp和udp。 + +#### 3.3.ikuai端口映射 +当前仅支持使用爱快系统作为主路由,可以自动设置主网关爱快系统的端口映射。 + +### 4.自定义脚本 +支持自定义脚本 + + +## 截图展示 + +![图1](./img/natmap-1.png) +![图2](./img/natmap-2.png) + + +## 使用 + +### openwrt编译时添加软件源至feeds.conf.default首行,以覆盖openwrt内置luci-app-natmap + +``` +src-git zzz https://github.com/blueberry-pie-11/luci-app-natmap +``` + +### 编译源码,尽量使用编译固件而非插件安装 + +``` +./scripts/feeds update -a +./scripts/feeds install -a +make +``` + +## 本脚本相关功能依据以下代码改写: +1. https://github.com/EkkoG/luci-app-natmap +2. https://github.com/EkkoG/openwrt-natmap +3. https://github.com/loyux/ikuai_local_api +4. https://github.com/ztc1997/ikuai-bypass + + + diff --git a/luci-app-natmap/img/natmap-1.png b/luci-app-natmap/img/natmap-1.png new file mode 100644 index 000000000..cd705583e Binary files /dev/null and b/luci-app-natmap/img/natmap-1.png differ diff --git a/luci-app-natmap/img/natmap-2.png b/luci-app-natmap/img/natmap-2.png new file mode 100644 index 000000000..d8c8ef60e Binary files /dev/null and b/luci-app-natmap/img/natmap-2.png differ diff --git a/luci-app-natmap/luci-app-natmap/Makefile b/luci-app-natmap/luci-app-natmap/Makefile new file mode 100755 index 000000000..37efd6314 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/Makefile @@ -0,0 +1,21 @@ +# This is free software, licensed under the Apache License, Version 2.0 + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-natmap +PKG_VERSION:=1.4.0 +PKG_RELEASE:=2 + +LUCI_TITLE:=LuCI Support for natmap +LUCI_DEPENDS:=+natmap +jq +curl +openssl-util +bash + +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=Richard Yu + +define Package/${PKG_NAME}/conffiles +/etc/config/natmap +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-natmap/luci-app-natmap/README.md b/luci-app-natmap/luci-app-natmap/README.md new file mode 100644 index 000000000..4f53d17a8 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/README.md @@ -0,0 +1,4 @@ +### 支持版本 + +OpenWrt 21 及以上 +### 使用gettext命令提取js文件文本信息 diff --git a/luci-app-natmap/luci-app-natmap/htdocs/luci-static/resources/view/natmap.js b/luci-app-natmap/luci-app-natmap/htdocs/luci-static/resources/view/natmap.js new file mode 100755 index 000000000..3f0baf8b6 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/htdocs/luci-static/resources/view/natmap.js @@ -0,0 +1,764 @@ +"use strict"; +"require form"; +"require fs"; +"require rpc"; +"require view"; +"require tools.widgets as widgets"; + +var callServiceList = rpc.declare({ + object: "service", + method: "list", + params: ["name"], + expect: { "": {} }, +}); + +function getInstances() { + return L.resolveDefault(callServiceList("natmap"), {}).then(function (res) { + try { + return res.natmap.instances || {}; + } catch (e) {} + return {}; + }); +} + +function getStatus() { + return getInstances().then(function (instances) { + var promises = []; + var status = {}; + for (var key in instances) { + var i = instances[key]; + if (i.running && i.pid) { + var f = "/var/run/natmap/" + i.pid + ".json"; + (function (k) { + promises.push( + fs + .read(f) + .then(function (res) { + status[k] = JSON.parse(res); + }) + .catch(function (e) {}) + ); + })(key); + } + } + return Promise.all(promises).then(function () { + return status; + }); + }); +} + +return view.extend({ + load: function () { + return getStatus(); + }, + render: function (status) { + var m, s, o; + + m = new form.Map("natmap", _("NatMap Settings")); + s = m.section(form.GridSection, "natmap"); + s.addremove = true; + s.anonymous = true; + + s.tab("general", _("General Settings")); + s.tab("forward", _("Forward Settings")); + s.tab("notify", _("Notify Settings")); + s.tab("link", _("Link Settings")); + s.tab("custom", _("Custom Settings")); + + // o = s.option(form.DummyValue, '_nat_name', _('Name')); + // o.modalonly = false; + // o.textvalue = function (section_id) { + // var s = status[section_id]; + // if (s) return s.name; + // }; + + // ********************************************************************** + // general + // ********************************************************************** + o = s.taboption("general", form.Value, "general_nat_name", _("Name")); + o.datatype = "string"; + // o.modalonly = true; + o.rmempty = false; + + o = s.taboption( + "general", + form.ListValue, + "general_nat_protocol", + _("Protocol") + ); + o.default = "tcp"; + o.value("tcp", _("TCP")); + o.value("udp", _("UDP")); + + o = s.taboption( + "general", + form.ListValue, + "general_ip_address_family", + _("Restrict to address family") + ); + o.modalonly = true; + o.value("", _("IPv4 and IPv6")); + o.value("ipv4", _("IPv4 only")); + o.value("ipv6", _("IPv6 only")); + + o = s.taboption( + "general", + widgets.NetworkSelect, + "general_wan_interface", + _("Wan Interface") + ); + o.modalonly = true; + o.rmempty = false; + + o = s.taboption( + "general", + form.Value, + "general_interval", + _("Keep-alive interval") + ); + o.datatype = "uinteger"; + o.modalonly = true; + o.rmempty = false; + + o = s.taboption( + "general", + form.Value, + "general_stun_server", + _("STUN server") + ); + o.datatype = "host"; + o.modalonly = true; + o.optional = false; + o.rmempty = false; + + o = s.taboption( + "general", + form.Value, + "general_http_server", + _("HTTP server"), + _("For TCP mode") + ); + o.datatype = "host"; + o.modalonly = true; + o.rmempty = false; + + o = s.taboption("general", form.Value, "general_bind_port", _("Bind port")); + o.datatype = "port"; + o.rmempty = false; + + // ********************************************************************** + // forward + // ********************************************************************** + o = s.taboption( + "forward", + form.Flag, + "forward_enable", + _("Enable Forward") + ); + o.default = false; + o.modalonly = true; + // o.ucioption = 'forward_mode'; + // o.load = function (section_id) { + // return this.super('load', section_id) ? '1' : '0'; + // }; + // o.write = function (section_id, formvalue) { }; + + o = s.taboption( + "forward", + form.ListValue, + "forward_mode", + _("Forward mode") + ); + o.default = "firewall"; + o.value("firewall", _("firewall dnat")); + o.value("natmap", _("natmap")); + o.value("ikuai", _("ikuai")); + // o.depends('forward_enable', '1'); + + // forward_natmap + o = s.taboption( + "forward", + form.Value, + "forward_target_ip", + _("Forward target") + ); + o.datatype = "host"; + o.modalonly = true; + o.depends("forward_mode", "firewall"); + o.depends("forward_mode", "natmap"); + o.depends("forward_mode", "ikuai"); + + o = s.taboption( + "forward", + form.Value, + "forward_target_port", + _("Forward target port"), + _("0 will forward to the out port get from STUN") + ); + o.datatype = "port"; + o.modalonly = true; + o.depends("forward_mode", "firewall"); + o.depends("forward_mode", "natmap"); + o.depends("forward_mode", "ikuai"); + + o = s.taboption( + "forward", + widgets.NetworkSelect, + "forward_natmap_target_interface", + _("Target_Interface") + ); + o.modalonly = true; + o.depends("forward_mode", "firewall"); + + // forward_ikuai + o = s.taboption( + "forward", + form.Value, + "forward_ikuai_web_url", + _("Ikuai Web URL"), + _( + "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP and DNS" + ) + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("forward_mode", "ikuai"); + + o = s.taboption( + "forward", + form.Value, + "forward_ikuai_username", + _("Ikuai Username") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("forward_mode", "ikuai"); + + o = s.taboption( + "forward", + form.Value, + "forward_ikuai_password", + _("Ikuai Password") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("forward_mode", "ikuai"); + + o = s.taboption( + "forward", + form.ListValue, + "forward_ikuai_mapping_protocol", + _("Ikuai Mapping Protocol"), + _("such as tcp or udp or tcp+udp") + ); + o.modalonly = true; + o.value("tcp+udp", _("TCP+UDP")); + o.value("tcp", _("TCP")); + o.value("udp", _("UDP")); + o.depends("forward_mode", "ikuai"); + + o = s.taboption( + "forward", + form.Value, + "forward_ikuai_mapping_wan_interface", + _("Ikuai Mapping Wan Interface"), + _("such as adsl_1 or wan") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("forward_mode", "ikuai"); + + // forward_advanced + o = s.taboption( + "forward", + form.Flag, + "forward_advanced_enable", + _("Advanced Settings") + ); + o.default = false; + o.modalonly = true; + o.depends("forward_mode", "ikuai"); + + o = s.taboption( + "forward", + form.Value, + "forward_advanced_max_retries", + _("Max Retries"), + _("max retries,default 0 means execute only once") + ); + o.datatype = "uinteger"; + o.modalonly = true; + o.depends("forward_advanced_enable", "1"); + + o = s.taboption( + "forward", + form.Value, + "forward_advanced_sleep_time", + _("Retry Interval"), + _("Retry Interval, unit is seconds, default 0 is 3 seconds") + ); + o.datatype = "uinteger"; + o.modalonly = true; + o.depends("forward_advanced_enable", "1"); + + // ********************************************************************** + // notify + // ********************************************************************** + o = s.taboption("notify", form.Flag, "notify_enable", _("Enable Notify")); + o.default = false; + o.modalonly = true; + + o = s.taboption( + "notify", + form.ListValue, + "notify_mode", + _("Notify channel") + ); + o.default = "telegram_bot"; + o.modalonly = true; + o.value("telegram_bot", _("Telegram Bot")); + o.value("pushplus", _("PushPlus")); + o.value("serverchan", _("ServerChan")); + o.value("gotify", _("Gotify")); + + // notify_telegram_bot + o = s.taboption( + "notify", + form.Value, + "notify_telegram_bot_chat_id", + _("Chat ID") + ); + o.description = + _("Get chat_id") + + ' ' + + _("Click here") + + "" + + _( + "
If you want to send to a group/channel, please create a non-Chinese group/channel (for easier chatid lookup, you can rename it later).
Add the bot to the group, send a message, and use https://api.telegram.org/bot token /getUpdates to obtain the chatid." + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("notify_mode", "telegram_bot"); + + o = s.taboption( + "notify", + form.Value, + "notify_telegram_bot_token", + _("Telegram Token") + ); + o.description = + _("Get Bot") + + ' ' + + _("Click here") + + "" + + _("
Send a message to the created bot to initiate a conversation."); + o.datatype = "string"; + o.modalonly = true; + o.depends("notify_mode", "telegram_bot"); + + o = s.taboption( + "notify", + form.Value, + "notify_telegram_bot_proxy", + _("http proxy") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("notify_mode", "telegram_bot"); + + //notify_pushplus + o = s.taboption("notify", form.Value, "notify_pushplus_token", _("PushPlus Token")); + o.description = + _("Get Instructions") + + ' ' + + _("Click here") + + ""; + o.datatype = "string"; + o.modalonly = true; + o.depends("notify_mode", "pushplus"); + + // serverchan + o = s.taboption( + "notify", + form.Value, + "notify_serverchan_sendkey", + _("ServerChan sendkey") + ); + o.description = + _("Get Instructions") + + ' ' + + _("Click here") + + "" + + _( + "
Since the asynchronous push queue is used, only whether the put into the queue is successful is detected." + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("notify_mode", "serverchan"); + + // notify_serverchan_advanced + o = s.taboption( + "notify", + form.Flag, + "notify_serverchan_advanced_enable", + _("ServerChan Advanced Settings") + ); + o.default = false; + o.modalonly = true; + o.depends("notify_mode", "serverchan"); + + o = s.taboption( + "notify", + form.Value, + "notify_serverchan_advanced_url", + _("Self-built Server Url") + ); + o.description = _("such as http://127.0.0.1:8080 or http://ikuai.lan:8080"); + o.datatype = "string"; + o.modalonly = true; + o.depends("notify_serverchan_advanced_enable", "1"); + + // gotify + o = s.taboption("notify", form.Value, "notify_gotify_url", _("Gotify url")); + o.description = + _("Get Instructions") + + ' ' + + _("Click here") + + ""; + o.datatype = "string"; + o.modalonly = true; + o.depends("notify_mode", "gotify"); + + o = s.taboption( + "notify", + form.Value, + "notify_gotify_token", + _("Gotify Token") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("notify_mode", "gotify"); + + o = s.taboption( + "notify", + form.Value, + "notify_gotify_priority", + _("Gotify priority") + ); + o.datatype = "uinteger"; + o.default = 5; + o.modalonly = true; + o.depends("notify_mode", "gotify"); + + // notify_advanced + o = s.taboption( + "notify", + form.Flag, + "notify_advanced_enable", + _("Advanced Settings") + ); + o.default = false; + o.modalonly = true; + o.depends("notify_mode", "pushplus"); + o.depends("notify_mode", "telegram_bot"); + o.depends("notify_mode", "serverchan"); + o.depends("notify_mode", "gotify"); + + o = s.taboption( + "notify", + form.Value, + "notify_advanced_max_retries", + _("Max Retries"), + _("max retries,default 0 means execute only once") + ); + o.datatype = "uinteger"; + o.modalonly = true; + o.depends("notify_advanced_enable", "1"); + + o = s.taboption( + "notify", + form.Value, + "notify_advanced_sleep_time", + _("Retry Interval"), + _("Retry Interval, unit is seconds, default 0 is 3 seconds") + ); + o.datatype = "uinteger"; + o.modalonly = true; + o.depends("notify_advanced_enable", "1"); + + // ********************************************************************** + // link + // ********************************************************************** + o = s.taboption("link", form.Flag, "link_enable", _("Enable link setting")); + o.modalonly = true; + o.default = false; + + o = s.taboption("link", form.ListValue, "link_mode", _("Service")); + o.default = "qbittorrent"; + o.modalonly = true; + o.value("emby", _("Emby")); + o.value("qbittorrent", _("qBittorrent")); + o.value("transmission", _("Transmission")); + o.value("cloudflare_origin_rule", _("Cloudflare Origin Rule")); + o.value("cloudflare_redirect_rule", _("Cloudflare Redirect Rule")); + + // link_cloudflare + o = s.taboption( + "link", + form.Value, + "link_cloudflare_token", + _("Cloudflare Token") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "cloudflare_origin_rule"); + o.depends("link_mode", "cloudflare_redirect_rule"); + + o = s.taboption( + "link", + form.Value, + "link_cloudflare_zone_id", + _("Cloudflare Zone ID") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "cloudflare_origin_rule"); + o.depends("link_mode", "cloudflare_redirect_rule"); + + // link_cloudflare_origin_rule + o = s.taboption( + "link", + form.Value, + "link_cloudflare_origin_rule_name", + _("Origin Rule Name") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "cloudflare_origin_rule"); + + // link_cloudflare_redirect_rule + o = s.taboption( + "link", + form.Value, + "link_cloudflare_redirect_rule_name", + _("Redirect Rule Name") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "cloudflare_redirect_rule"); + + o = s.taboption( + "link", + form.Value, + "link_cloudflare_redirect_rule_target_url", + _("Redirect Rule Target URL") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "cloudflare_redirect_rule"); + + // link_emby + o = s.taboption( + "link", + form.Value, + "link_emby_url", + _("EMBY URL"), + _( + "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP/DNS" + ) + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "emby"); + + o = s.taboption("link", form.Value, "link_emby_api_key", _("API Key")); + o.datatype = "host"; + o.modalonly = true; + o.depends("link_mode", "emby"); + + o = s.taboption( + "link", + form.Flag, + "link_emby_use_https", + _("Update HTTPS Port"), + _("Set to False if you want to use HTTP") + ); + o.default = false; + o.modalonly = true; + o.depends("link_mode", "emby"); + + o = s.taboption( + "link", + form.Flag, + "link_emby_update_host_with_ip", + _("Update host with IP") + ); + o.default = false; + o.modalonly = true; + o.depends("link_mode", "emby"); + + // link_qbittorrent + o = s.taboption( + "link", + form.Value, + "link_qb_web_url", + _("Web UI URL"), + _( + "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP and DNS" + ) + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "qbittorrent"); + + o = s.taboption("link", form.Value, "link_qb_username", _("Username")); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "qbittorrent"); + + o = s.taboption("link", form.Value, "link_qb_password", _("Password")); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "qbittorrent"); + + o = s.taboption("link", form.Flag, "link_qb_allow_ipv6", _("Allow IPv6")); + o.default = false; + o.modalonly = true; + o.depends("link_mode", "qbittorrent"); + + o = s.taboption( + "link", + form.Value, + "link_qb_ipv6_address", + _("IPv6 Address") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_qb_allow_ipv6", "1"); + + // link_transmission + o = s.taboption( + "link", + form.Value, + "link_tr_rpc_url", + _("RPC URL"), + _( + "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP and DNS" + ) + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "transmission"); + + o = s.taboption("link", form.Value, "link_tr_username", _("Username")); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "transmission"); + + o = s.taboption("link", form.Value, "link_tr_password", _("Password")); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_mode", "transmission"); + + o = s.taboption("link", form.Flag, "link_tr_allow_ipv6", _("Allow IPv6")); + o.modalonly = true; + o.default = false; + o.depends("link_mode", "transmission"); + + o = s.taboption( + "link", + form.Value, + "link_tr_ipv6_address", + _("IPv6 Address") + ); + o.datatype = "string"; + o.modalonly = true; + o.depends("link_tr_allow_ipv6", "1"); + + // link_advanced + o = s.taboption( + "link", + form.Flag, + "link_advanced_enable", + _("Advanced Settings") + ); + o.default = false; + o.modalonly = true; + o.depends("link_mode", "transmission"); + o.depends("link_mode", "qbittorrent"); + o.depends("link_mode", "emby"); + o.depends("link_mode", "cloudflare_origin_rule"); + o.depends("link_mode", "cloudflare_redirect_rule"); + + o = s.taboption( + "link", + form.Value, + "link_advanced_max_retries", + _("Max Retries"), + _("max retries,default 0 means execute only once") + ); + o.datatype = "uinteger"; + o.modalonly = true; + o.depends("link_advanced_enable", "1"); + + o = s.taboption( + "link", + form.Value, + "link_advanced_sleep_time", + _("Retry Interval"), + _("Retry Interval, unit is seconds, default 0 is 3 seconds") + ); + o.datatype = "uinteger"; + o.modalonly = true; + o.depends("link_advanced_enable", "1"); + + // ********************************************************************** + // Custom Settings + // ********************************************************************** + o = s.taboption( + "custom", + form.Flag, + "custom_script_enable", + _("Enable custom script's config") + ); + o.modalonly = true; + o.default = false; + + o = s.taboption( + "custom", + form.Value, + "custom_script_path", + _("custom script"), + _("custom script path,such as /etc/natmap/custom.sh") + ); + // o.depends('custom_script_enable', '1'); + o.datatype = "file"; + o.modalonly = true; + + // ********************************************************************** + // status + // ********************************************************************** + o = s.option(form.DummyValue, "_external_ip", _("External IP")); + o.modalonly = false; + o.textvalue = function (section_id) { + var s = status[section_id]; + if (s) return s.ip; + }; + + o = s.option(form.DummyValue, "_external_port", _("External Port")); + o.modalonly = false; + o.textvalue = function (section_id) { + var s = status[section_id]; + if (s) return s.port; + }; + + // ********************************************************************** + // natmap_enable + // ********************************************************************** + o = s.option(form.Flag, "natmap_enable", _("enable")); + o.editable = true; + o.modalonly = false; + + return m.render(); + }, +}); diff --git a/luci-app-natmap/luci-app-natmap/po/en/luci-app-natmap.po b/luci-app-natmap/luci-app-natmap/po/en/luci-app-natmap.po new file mode 100644 index 000000000..76edfd162 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/po/en/luci-app-natmap.po @@ -0,0 +1,397 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: luci-app-natmap\n" +"Language: en\n" + +#: natmap.js:62 +msgid "General Settings" +msgstr "General Settings" + +#: natmap.js:63 +msgid "Forward Settings" +msgstr "Forward Settings" + +#: natmap.js:64 +msgid "Notify Settings" +msgstr "Notify Settings" + +#: natmap.js:65 +msgid "Link Settings" +msgstr "Link Settings" + +#: natmap.js:66 +msgid "Custom Settings" +msgstr "Custom Settings" + +#: natmap.js:78 +msgid "Name" +msgstr "Name" + +#: natmap.js:87 +msgid "Protocol" +msgstr "Protocol" + +#: natmap.js:90 natmap.js:256 +msgid "TCP" +msgstr "TCP" + +#: natmap.js:91 natmap.js:257 +msgid "UDP" +msgstr "UDP" + +#: natmap.js:97 +msgid "Restrict to address family" +msgstr "Restrict to address family" + +#: natmap.js:100 +msgid "IPv4 and IPv6" +msgstr "IPv4 and IPv6" + +#: natmap.js:101 +msgid "IPv4 only" +msgstr "IPv4 only" + +#: natmap.js:102 +msgid "IPv6 only" +msgstr "IPv6 only" + +#: natmap.js:108 +msgid "Wan Interface" +msgstr "Wan Interface" + +#: natmap.js:117 +msgid "Keep-alive interval" +msgstr "Keep-alive interval" + +#: natmap.js:127 +msgid "STUN server" +msgstr "STUN server" + +#: natmap.js:138 +msgid "HTTP server" +msgstr "HTTP server" + +#: natmap.js:139 +msgid "For TCP mode" +msgstr "For TCP mode" + +#: natmap.js:145 +msgid "Bind port" +msgstr "Bind port" + +#: natmap.js:156 +msgid "Enable Forward" +msgstr "Enable Forward" + +#: natmap.js:170 +msgid "Forward mode" +msgstr "Forward mode" + +#: natmap.js:173 +msgid "firewall dnat" +msgstr "firewall dnat" + +#: natmap.js:174 +msgid "natmap" +msgstr "natmap" + +#: natmap.js:175 +msgid "ikuai" +msgstr "ikuai" + +#: natmap.js:183 +msgid "Forward target" +msgstr "Forward target" + +#: natmap.js:195 +msgid "Forward target port" +msgstr "Forward target port" + +#: natmap.js:196 +msgid "0 will forward to the out port get from STUN" +msgstr "0 will forward to the out port get from STUN" + +#: natmap.js:208 +msgid "Target_Interface" +msgstr "Target_Interface" + +#: natmap.js:218 +msgid "Ikuai Web URL" +msgstr "Ikuai Web URL" + +#: natmap.js:220 natmap.js:607 natmap.js:646 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP and DNS" +msgstr "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP and DNS" + +#: natmap.js:231 +msgid "Ikuai Username" +msgstr "Ikuai Username" + +#: natmap.js:241 +msgid "Ikuai Password" +msgstr "Ikuai Password" + +#: natmap.js:251 +msgid "Ikuai Mapping Protocol" +msgstr "Ikuai Mapping Protocol" + +#: natmap.js:252 +msgid "such as tcp or udp or tcp+udp" +msgstr "such as tcp or udp or tcp+udp" + +#: natmap.js:255 +msgid "TCP+UDP" +msgstr "TCP+UDP" + +#: natmap.js:264 +msgid "Ikuai Mapping Wan Interface" +msgstr "Ikuai Mapping Wan Interface" + +#: natmap.js:265 +msgid "such as adsl_1 or wan" +msgstr "such as adsl_1 or wan" + +#: natmap.js:276 natmap.js:458 natmap.js:683 +msgid "Advanced Settings" +msgstr "Advanced Settings" + +#: natmap.js:286 natmap.js:471 natmap.js:697 +msgid "Max Retries" +msgstr "Max Retries" + +#: natmap.js:287 natmap.js:472 natmap.js:698 +msgid "max retries,default 0 means execute only once" +msgstr "max retries,default 0 means execute only once" + +#: natmap.js:297 natmap.js:482 natmap.js:708 +msgid "Retry Interval" +msgstr "Retry Interval" + +#: natmap.js:298 natmap.js:483 natmap.js:709 +msgid "Retry Interval, unit is seconds, default 0 is 3 seconds" +msgstr "Retry Interval, unit is seconds, default 0 is 3 seconds" + +#: natmap.js:307 +msgid "Enable Notify" +msgstr "Enable Notify" + +#: natmap.js:315 +msgid "Notify channel" +msgstr "Notify channel" + +#: natmap.js:319 +msgid "Telegram Bot" +msgstr "Telegram Bot" + +#: natmap.js:320 +msgid "PushPlus" +msgstr "PushPlus" + +#: natmap.js:321 +msgid "ServerChan" +msgstr "ServerChan" + +#: natmap.js:322 +msgid "Gotify" +msgstr "Gotify" + +#: natmap.js:329 +msgid "Chat ID" +msgstr "Chat ID" + +#: natmap.js:332 +msgid "Get chat_id" +msgstr "Get chat_id" + +#: natmap.js:334 natmap.js:352 natmap.js:374 natmap.js:390 natmap.js:426 +msgid "Click here" +msgstr "Click here" + +#: natmap.js:337 +msgid "
If you want to send to a group/channel, please create a non-Chinese group/channel (for easier chatid lookup, you can rename it later).
Add the bot to the group, send a message, and use https://api.telegram.org/bot token /getUpdates to obtain the chatid." +msgstr "
If you want to send to a group/channel, please create a non-Chinese group/channel (for easier chatid lookup, you can rename it later).
Add the bot to the group, send a message, and use https://api.telegram.org/bot token /getUpdates to obtain the chatid." + +#: natmap.js:350 +msgid "Get Bot" +msgstr "Get Bot" + +#: natmap.js:354 +msgid "
Send a message to the created bot to initiate a conversation." +msgstr "
Send a message to the created bot to initiate a conversation." + +#: natmap.js:363 +msgid "http proxy" +msgstr "http proxy" + +#: natmap.js:372 natmap.js:388 natmap.js:424 +msgid "Get Instructions" +msgstr "Get Instructions" + +#: natmap.js:385 +msgid "ServerChan sendkey" +msgstr "ServerChan sendkey" + +#: natmap.js:393 +msgid "
Since the asynchronous push queue is used, only whether the put into the queue is successful is detected." +msgstr "
Since the asynchronous push queue is used, only whether the put into the queue is successful is detected." + +#: natmap.js:404 +msgid "ServerChan Advanced Settings" +msgstr "ServerChan Advanced Settings" + +#: natmap.js:414 +msgid "Self-built Server Url" +msgstr "Self-built Server Url" + +#: natmap.js:416 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080" +msgstr "such as http://127.0.0.1:8080 or http://ikuai.lan:8080" + +#: natmap.js:422 +msgid "Gotify url" +msgstr "Gotify url" + +#: natmap.js:446 +msgid "Gotify priority" +msgstr "Gotify priority" + +#: natmap.js:492 +msgid "Enable link setting" +msgstr "Enable link setting" + +#: natmap.js:496 +msgid "Service" +msgstr "Service" + +#: natmap.js:499 +msgid "Emby" +msgstr "Emby" + +#: natmap.js:500 +msgid "qBittorrent" +msgstr "qBittorrent" + +#: natmap.js:501 +msgid "Transmission" +msgstr "Transmission" + +#: natmap.js:502 +msgid "Cloudflare Origin Rule" +msgstr "Cloudflare Origin Rule" + +#: natmap.js:503 +msgid "Cloudflare Redirect Rule" +msgstr "Cloudflare Redirect Rule" + +#: natmap.js:574 +msgid "API Key" +msgstr "API Key" + +#: natmap.js:565 +msgid "EMBY URL" +msgstr "EMBY URL" + +#: natmap.js:583 +msgid "Update HTTPS Port" +msgstr "Update HTTPS Port" + +#: natmap.js:584 +msgid "Set to False if you want to use HTTP" +msgstr "Set to False if you want to use HTTP" + +#: natmap.js:594 +msgid "Update host with IP" +msgstr "Update host with IP" + +#: natmap.js:605 +msgid "Web UI URL" +msgstr "Web UI URL" + +#: natmap.js:614 natmap.js:653 +msgid "Username" +msgstr "Username" + +#: natmap.js:619 natmap.js:658 +msgid "Password" +msgstr "Password" + +#: natmap.js:624 natmap.js:663 +msgid "Allow IPv6" +msgstr "Allow IPv6" + +#: natmap.js:633 natmap.js:672 +msgid "IPv6 Address" +msgstr "IPv6 Address" + +#: natmap.js:644 +msgid "RPC URL" +msgstr "RPC URL" + +#: natmap.js:722 +msgid "Enable custom script's config" +msgstr "Enable custom script's config" + +#: natmap.js:731 +msgid "custom script" +msgstr "custom script" + +#: natmap.js:732 +msgid "custom script path,such as /etc/natmap/custom.sh" +msgstr "custom script path,such as /etc/natmap/custom.sh" + +#: natmap.js:741 +msgid "External IP" +msgstr "External IP" + +#: natmap.js:748 +msgid "External Port" +msgstr "External Port" + +#: natmap.js:758 +msgid "enable" +msgstr "enable" + +#: natmap.js:57 +msgid "NatMap Settings" +msgstr "NatMap Settings" + +#: natmap.js:510 +msgid "Cloudflare Token" +msgstr "Cloudflare Token" + +#: natmap.js:521 +msgid "Cloudflare Zone ID" +msgstr "Cloudflare Zone ID" + +#: natmap.js:533 +msgid "Origin Rule Name" +msgstr "Origin Rule Name" + +#: natmap.js:544 +msgid "Redirect Rule Name" +msgstr "Redirect Rule Name" + +#: natmap.js:347 +msgid "Telegram Token" +msgstr "Telegram Token" + +#: natmap.js:370 +msgid "PushPlus Token" +msgstr "PushPlus Token" + +#: natmap.js:436 +msgid "Gotify Token" +msgstr "Gotify Token" + +#: natmap.js:554 +msgid "Redirect Rule Target URL" +msgstr "Redirect Rule Target URL" + +#: natmap.js:567 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP/DNS" +msgstr "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP/DNS" + diff --git a/luci-app-natmap/luci-app-natmap/po/jp/luci-app-natmap.po b/luci-app-natmap/luci-app-natmap/po/jp/luci-app-natmap.po new file mode 100644 index 000000000..5068ca347 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/po/jp/luci-app-natmap.po @@ -0,0 +1,397 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: luci-app-natmap\n" +"Language: ja\n" + +#: natmap.js:62 +msgid "General Settings" +msgstr "一般設定" + +#: natmap.js:63 +msgid "Forward Settings" +msgstr "転送設定" + +#: natmap.js:64 +msgid "Notify Settings" +msgstr "通知設定" + +#: natmap.js:65 +msgid "Link Settings" +msgstr "リンク設定" + +#: natmap.js:66 +msgid "Custom Settings" +msgstr "カスタム設定" + +#: natmap.js:78 +msgid "Name" +msgstr "名前" + +#: natmap.js:87 +msgid "Protocol" +msgstr "プロトコル" + +#: natmap.js:90 natmap.js:256 +msgid "TCP" +msgstr "TCP" + +#: natmap.js:91 natmap.js:257 +msgid "UDP" +msgstr "UDP" + +#: natmap.js:97 +msgid "Restrict to address family" +msgstr "アドレスを家族に限定する" + +#: natmap.js:100 +msgid "IPv4 and IPv6" +msgstr "IPv4 と IPv6" + +#: natmap.js:101 +msgid "IPv4 only" +msgstr "IPv4のみ" + +#: natmap.js:102 +msgid "IPv6 only" +msgstr "IPv6のみ" + +#: natmap.js:108 +msgid "Wan Interface" +msgstr "WANインターフェース" + +#: natmap.js:117 +msgid "Keep-alive interval" +msgstr "キープアライブ間隔" + +#: natmap.js:127 +msgid "STUN server" +msgstr "STUNサーバー" + +#: natmap.js:138 +msgid "HTTP server" +msgstr "HTTPサーバー" + +#: natmap.js:139 +msgid "For TCP mode" +msgstr "TCPモードの場合" + +#: natmap.js:145 +msgid "Bind port" +msgstr "バインドポート" + +#: natmap.js:156 +msgid "Enable Forward" +msgstr "転送を有効にする" + +#: natmap.js:170 +msgid "Forward mode" +msgstr "順方向モード" + +#: natmap.js:173 +msgid "firewall dnat" +msgstr "firewall dnat" + +#: natmap.js:174 +msgid "natmap" +msgstr "natmap" + +#: natmap.js:175 +msgid "ikuai" +msgstr "ikuai" + +#: natmap.js:183 +msgid "Forward target" +msgstr "前方ターゲット" + +#: natmap.js:195 +msgid "Forward target port" +msgstr "転送ターゲットポート" + +#: natmap.js:196 +msgid "0 will forward to the out port get from STUN" +msgstr "0 は STUN から取得した出力ポートに転送します" + +#: natmap.js:208 +msgid "Target_Interface" +msgstr "ターゲットインターフェース" + +#: natmap.js:218 +msgid "Ikuai Web URL" +msgstr "Ikuai URL" + +#: natmap.js:220 natmap.js:607 natmap.js:646 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP and DNS" +msgstr "http://127.0.0.1:8080 や http://ikuai.lan:8080 など。ホストを使用する場合は、DHCP と DNS の再バインド保護を閉じる必要があります。" + +#: natmap.js:231 +msgid "Ikuai Username" +msgstr "育愛ユーザー名" + +#: natmap.js:241 +msgid "Ikuai Password" +msgstr "育愛パスワード" + +#: natmap.js:251 +msgid "Ikuai Mapping Protocol" +msgstr "イクアイマッピングプロトコル" + +#: natmap.js:252 +msgid "such as tcp or udp or tcp+udp" +msgstr "tcp、udp、tcp+udp など" + +#: natmap.js:255 +msgid "TCP+UDP" +msgstr "TCP+UDP" + +#: natmap.js:264 +msgid "Ikuai Mapping Wan Interface" +msgstr "Ikuai マッピング Wan インターフェース" + +#: natmap.js:265 +msgid "such as adsl_1 or wan" +msgstr "adsl_1 や wan など" + +#: natmap.js:276 natmap.js:458 natmap.js:683 +msgid "Advanced Settings" +msgstr "高度な設定" + +#: natmap.js:286 natmap.js:471 natmap.js:697 +msgid "Max Retries" +msgstr "最大再試行回数" + +#: natmap.js:287 natmap.js:472 natmap.js:698 +msgid "max retries,default 0 means execute only once" +msgstr "最大再試行数、デフォルトの 0 は 1 回だけ実行することを意味します" + +#: natmap.js:297 natmap.js:482 natmap.js:708 +msgid "Retry Interval" +msgstr "再試行間隔" + +#: natmap.js:298 natmap.js:483 natmap.js:709 +msgid "Retry Interval, unit is seconds, default 0 is 3 seconds" +msgstr "再試行間隔、単位は秒、デフォルトは 0 で 3 秒です" + +#: natmap.js:307 +msgid "Enable Notify" +msgstr "通知を有効にする" + +#: natmap.js:315 +msgid "Notify channel" +msgstr "通知チャネル" + +#: natmap.js:319 +msgid "Telegram Bot" +msgstr "Telegram Bot" + +#: natmap.js:320 +msgid "PushPlus" +msgstr "PushPlus" + +#: natmap.js:321 +msgid "ServerChan" +msgstr "ServerChan" + +#: natmap.js:322 +msgid "Gotify" +msgstr "Gotify" + +#: natmap.js:329 +msgid "Chat ID" +msgstr "チャットID" + +#: natmap.js:332 +msgid "Get chat_id" +msgstr "chat_id を取得する" + +#: natmap.js:334 natmap.js:352 natmap.js:374 natmap.js:390 natmap.js:426 +msgid "Click here" +msgstr "ここをクリック" + +#: natmap.js:337 +msgid "
If you want to send to a group/channel, please create a non-Chinese group/channel (for easier chatid lookup, you can rename it later).
Add the bot to the group, send a message, and use https://api.telegram.org/bot token /getUpdates to obtain the chatid." +msgstr "
グループ/チャンネルに送信したい場合は、中国語以外のグループ/チャンネルを作成してください (チャット ID の検索を容易にするため、後で名前を変更できます)。
ボットをグループに追加し、メッセージを送信します。メッセージを取得し、https://api.telegram.org/bot token /getUpdates を使用してチャット ID を取得します。" + +#: natmap.js:350 +msgid "Get Bot" +msgstr "ボットを入手" + +#: natmap.js:354 +msgid "
Send a message to the created bot to initiate a conversation." +msgstr "
作成したボットにメッセージを送信して会話を開始します。" + +#: natmap.js:363 +msgid "http proxy" +msgstr "httpプロキシ" + +#: natmap.js:372 natmap.js:388 natmap.js:424 +msgid "Get Instructions" +msgstr "手順を確認する" + +#: natmap.js:385 +msgid "ServerChan sendkey" +msgstr "ServerChan 送信キー" + +#: natmap.js:393 +msgid "
Since the asynchronous push queue is used, only whether the put into the queue is successful is detected." +msgstr "
非同期プッシュキューを使用しているため、キューへの投入が成功したかどうかのみが検出されます。" + +#: natmap.js:404 +msgid "ServerChan Advanced Settings" +msgstr "ServerChan の詳細設定" + +#: natmap.js:414 +msgid "Self-built Server Url" +msgstr "自己構築サーバー URL" + +#: natmap.js:416 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080" +msgstr "http://127.0.0.1:8080 や http://ikuai.lan:8080 など" + +#: natmap.js:422 +msgid "Gotify url" +msgstr "Gotify URL" + +#: natmap.js:446 +msgid "Gotify priority" +msgstr "優先順位を獲得する" + +#: natmap.js:492 +msgid "Enable link setting" +msgstr "リンク設定を有効にする" + +#: natmap.js:496 +msgid "Service" +msgstr "サービス" + +#: natmap.js:499 +msgid "Emby" +msgstr "Emby" + +#: natmap.js:500 +msgid "qBittorrent" +msgstr "qBittorrent" + +#: natmap.js:501 +msgid "Transmission" +msgstr "Transmission" + +#: natmap.js:502 +msgid "Cloudflare Origin Rule" +msgstr "Cloudflare Origin Rule" + +#: natmap.js:503 +msgid "Cloudflare Redirect Rule" +msgstr "Cloudflare Redirect Rule" + +#: natmap.js:574 +msgid "API Key" +msgstr "API Key" + +#: natmap.js:565 +msgid "EMBY URL" +msgstr "EMBY URL" + +#: natmap.js:583 +msgid "Update HTTPS Port" +msgstr "HTTPSポートを更新する" + +#: natmap.js:584 +msgid "Set to False if you want to use HTTP" +msgstr "HTTP を使用する場合は False に設定します。" + +#: natmap.js:594 +msgid "Update host with IP" +msgstr "IPを使用してホストを更新する" + +#: natmap.js:605 +msgid "Web UI URL" +msgstr "ウェブUIのURL" + +#: natmap.js:614 natmap.js:653 +msgid "Username" +msgstr "ユーザー名" + +#: natmap.js:619 natmap.js:658 +msgid "Password" +msgstr "パスワード" + +#: natmap.js:624 natmap.js:663 +msgid "Allow IPv6" +msgstr "IPv6 を許可する" + +#: natmap.js:633 natmap.js:672 +msgid "IPv6 Address" +msgstr "IPv6アドレス" + +#: natmap.js:644 +msgid "RPC URL" +msgstr "RPC URL" + +#: natmap.js:722 +msgid "Enable custom script's config" +msgstr "カスタムスクリプトの設定を有効にする" + +#: natmap.js:731 +msgid "custom script" +msgstr "カスタムスクリプト" + +#: natmap.js:732 +msgid "custom script path,such as /etc/natmap/custom.sh" +msgstr "カスタム スクリプト パス (/etc/natmap/custom.sh など)" + +#: natmap.js:741 +msgid "External IP" +msgstr "外部IP" + +#: natmap.js:748 +msgid "External Port" +msgstr "外部ポート" + +#: natmap.js:758 +msgid "enable" +msgstr "有効にする" + +#: natmap.js:57 +msgid "NatMap Settings" +msgstr "NatMap Settings" + +#: natmap.js:510 +msgid "Cloudflare Token" +msgstr "Cloudflare Token" + +#: natmap.js:521 +msgid "Cloudflare Zone ID" +msgstr "Cloudflare Zone ID" + +#: natmap.js:533 +msgid "Origin Rule Name" +msgstr "Origin Rule Name" + +#: natmap.js:544 +msgid "Redirect Rule Name" +msgstr "Redirect Rule Name" + +#: natmap.js:347 +msgid "Telegram Token" +msgstr "Telegram Token" + +#: natmap.js:370 +msgid "PushPlus Token" +msgstr "PushPlus Token" + +#: natmap.js:436 +msgid "Gotify Token" +msgstr "Gotify Token" + +#: natmap.js:554 +msgid "Redirect Rule Target URL" +msgstr "重定方向规则目标URL" + +#: natmap.js:567 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP/DNS" +msgstr "例: http://127.0.0.1:8080 または http://ikuai.lan:8080。IP ではなくホスト アドレスを使用する場合は、DHCP/DNS 内の再設定保護を解除する必要があります。" + diff --git a/luci-app-natmap/luci-app-natmap/po/zh_Hans/luci-app-natmap.po b/luci-app-natmap/luci-app-natmap/po/zh_Hans/luci-app-natmap.po new file mode 100644 index 000000000..bda729a61 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/po/zh_Hans/luci-app-natmap.po @@ -0,0 +1,397 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: luci-app-natmap\n" +"Language: zh-Hans\n" + +#: natmap.js:337 +msgid "
If you want to send to a group/channel, please create a non-Chinese group/channel (for easier chatid lookup, you can rename it later).
Add the bot to the group, send a message, and use https://api.telegram.org/bot token /getUpdates to obtain the chatid." +msgstr "
如需通过群组/频道推送,请创建一个非中文的群组或频道(便于查找 chatid,后续再更名)
将机器人添加至群组,发送信息后通过 https://api.telegram.org/bot token /getUpdates 获取 chatid" + +#: natmap.js:354 +msgid "
Send a message to the created bot to initiate a conversation." +msgstr "
与创建的机器人发一条消息,开启对话" + +#: natmap.js:393 +msgid "
Since the asynchronous push queue is used, only whether the put into the queue is successful is detected." +msgstr "
由于使用异步推送队列,所以仅检测放入队列是否成功" + +#: natmap.js:196 +msgid "0 will forward to the out port get from STUN" +msgstr "填写 0 ,则转发到从STUN获取的外部端口" + +#: natmap.js:276 natmap.js:458 natmap.js:683 +msgid "Advanced Settings" +msgstr "高级设置" + +#: natmap.js:624 natmap.js:663 +msgid "Allow IPv6" +msgstr "允许 IPv6" + +#: natmap.js:574 +msgid "API Key" +msgstr "API 密钥" + +#: natmap.js:145 +msgid "Bind port" +msgstr "绑定端口" + +#: natmap.js:329 +msgid "Chat ID" +msgstr "会话ID" + +#: natmap.js:334 natmap.js:352 natmap.js:374 natmap.js:390 natmap.js:426 +msgid "Click here" +msgstr "点击此处" + +#: natmap.js:502 +msgid "Cloudflare Origin Rule" +msgstr "Cloudflare Origin Rule" + +#: natmap.js:503 +msgid "Cloudflare Redirect Rule" +msgstr "Cloudflare Redirect Rule" + +#: natmap.js:510 +msgid "Cloudflare Token" +msgstr "Cloudflare 令牌" + +#: natmap.js:521 +msgid "Cloudflare Zone ID" +msgstr "Cloudflare Zone ID" + +#: natmap.js:731 +msgid "custom script" +msgstr "自定义脚本" + +#: natmap.js:732 +msgid "custom script path,such as /etc/natmap/custom.sh" +msgstr "自定义脚本路径,例如:/etc/natmap/custom.sh" + +#: natmap.js:66 +msgid "Custom Settings" +msgstr "自定义设置" + +#: natmap.js:499 +msgid "Emby" +msgstr "Emby" + +#: natmap.js:565 +msgid "EMBY URL" +msgstr "Emby URL" + +#: natmap.js:758 +msgid "enable" +msgstr "启用" + +#: natmap.js:722 +msgid "Enable custom script's config" +msgstr "启用自定义脚本" + +#: natmap.js:156 +msgid "Enable Forward" +msgstr "启用转发" + +#: natmap.js:492 +msgid "Enable link setting" +msgstr "启用关联设置" + +#: natmap.js:307 +msgid "Enable Notify" +msgstr "启用通知" + +#: natmap.js:741 +msgid "External IP" +msgstr "外部 ip" + +#: natmap.js:748 +msgid "External Port" +msgstr "外部端口" + +#: natmap.js:173 +msgid "firewall dnat" +msgstr "firewall dnat" + +#: natmap.js:139 +msgid "For TCP mode" +msgstr "用于 tcp 模式" + +#: natmap.js:170 +msgid "Forward mode" +msgstr "转发模式" + +#: natmap.js:63 +msgid "Forward Settings" +msgstr "转发设置" + +#: natmap.js:183 +msgid "Forward target" +msgstr "转发目标" + +#: natmap.js:195 +msgid "Forward target port" +msgstr "转发目标端口" + +#: natmap.js:62 +msgid "General Settings" +msgstr "常规设置" + +#: natmap.js:350 +msgid "Get Bot" +msgstr "获取机器人" + +#: natmap.js:332 +msgid "Get chat_id" +msgstr "获取 chat_id" + +#: natmap.js:372 natmap.js:388 natmap.js:424 +msgid "Get Instructions" +msgstr "获取介绍" + +#: natmap.js:322 +msgid "Gotify" +msgstr "Gotify" + +#: natmap.js:446 +msgid "Gotify priority" +msgstr "Gotify 通知优先级" + +#: natmap.js:436 +msgid "Gotify Token" +msgstr "Gotify 令牌" + +#: natmap.js:422 +msgid "Gotify url" +msgstr "Gotify URL" + +#: natmap.js:363 +msgid "http proxy" +msgstr "http 代理" + +#: natmap.js:138 +msgid "HTTP server" +msgstr "HTTP 服务器" + +#: natmap.js:175 +msgid "ikuai" +msgstr "ikuai" + +#: natmap.js:251 +msgid "Ikuai Mapping Protocol" +msgstr "端口映射协议" + +#: natmap.js:264 +msgid "Ikuai Mapping Wan Interface" +msgstr "端口映射外网接口" + +#: natmap.js:241 +msgid "Ikuai Password" +msgstr "爱快密码" + +#: natmap.js:231 +msgid "Ikuai Username" +msgstr "爱快用户名" + +#: natmap.js:218 +msgid "Ikuai Web URL" +msgstr "爱快 URL" + +#: natmap.js:100 +msgid "IPv4 and IPv6" +msgstr "IPv4 和 IPv6" + +#: natmap.js:101 +msgid "IPv4 only" +msgstr "仅 IPv4" + +#: natmap.js:633 natmap.js:672 +msgid "IPv6 Address" +msgstr "IPv6 地址" + +#: natmap.js:102 +msgid "IPv6 only" +msgstr "仅 IPv6" + +#: natmap.js:117 +msgid "Keep-alive interval" +msgstr "keep-alive 间隔" + +#: natmap.js:65 +msgid "Link Settings" +msgstr "关联设置" + +#: natmap.js:286 natmap.js:471 natmap.js:697 +msgid "Max Retries" +msgstr "最大重试次数" + +#: natmap.js:287 natmap.js:472 natmap.js:698 +msgid "max retries,default 0 means execute only once" +msgstr "最大重试次数,默认 0 表示仅执行一次调用" + +#: natmap.js:78 +msgid "Name" +msgstr "名称" + +#: natmap.js:174 +msgid "natmap" +msgstr "natmap" + +#: natmap.js:57 +msgid "NatMap Settings" +msgstr "NatMap 设置" + +#: natmap.js:315 +msgid "Notify channel" +msgstr "通知通道" + +#: natmap.js:64 +msgid "Notify Settings" +msgstr "通知设置" + +#: natmap.js:533 +msgid "Origin Rule Name" +msgstr "Origin Rules 名称" + +#: natmap.js:619 natmap.js:658 +msgid "Password" +msgstr "密码" + +#: natmap.js:87 +msgid "Protocol" +msgstr "协议" + +#: natmap.js:320 +msgid "PushPlus" +msgstr "PushPlus" + +#: natmap.js:370 +msgid "PushPlus Token" +msgstr "PushPlus 令牌" + +#: natmap.js:500 +msgid "qBittorrent" +msgstr "qBittorrent" + +#: natmap.js:544 +msgid "Redirect Rule Name" +msgstr "重定向规则名称" + +#: natmap.js:554 +msgid "Redirect Rule Target URL" +msgstr "重定向规则目标URL" + +#: natmap.js:97 +msgid "Restrict to address family" +msgstr "地址族限制" + +#: natmap.js:297 natmap.js:482 natmap.js:708 +msgid "Retry Interval" +msgstr "重试间隔时间" + +#: natmap.js:298 natmap.js:483 natmap.js:709 +msgid "Retry Interval, unit is seconds, default 0 is 3 seconds" +msgstr "单次重试间隔时间,单位是秒,默认 0 表示 3秒" + +#: natmap.js:644 +msgid "RPC URL" +msgstr "RPC URL" + +#: natmap.js:414 +msgid "Self-built Server Url" +msgstr "自建服务器 URL" + +#: natmap.js:321 +msgid "ServerChan" +msgstr "Server酱" + +#: natmap.js:404 +msgid "ServerChan Advanced Settings" +msgstr "Server酱 高级设置" + +#: natmap.js:385 +msgid "ServerChan sendkey" +msgstr "Server酱 sendkey" + +#: natmap.js:496 +msgid "Service" +msgstr "服务" + +#: natmap.js:584 +msgid "Set to False if you want to use HTTP" +msgstr "如果使用 HTTP,请勿勾选" + +#: natmap.js:127 +msgid "STUN server" +msgstr "STUN 服务器" + +#: natmap.js:265 +msgid "such as adsl_1 or wan" +msgstr "例如:adsl_1 或 wan" + +#: natmap.js:416 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080" +msgstr "例如:http://127.0.0.1:8080 或 http://ikuai.lan:8080" + +#: natmap.js:220 natmap.js:607 natmap.js:646 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP and DNS" +msgstr "例如:http://127.0.0.1:8080 或 http://ikuai.lan:8080。如果使用 host ,必须关闭 DHCP/DNS 中的重绑定保护" + +#: natmap.js:567 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP/DNS" +msgstr "例如 http://127.0.0.1:8080 或 http://ikuai.lan:8080。如果使用 host 地址而不是ip,则必须关闭 DHCP/DNS 中的重绑定保护" + +#: natmap.js:252 +msgid "such as tcp or udp or tcp+udp" +msgstr "例如:tcp 或 udp 或 tcp+udp" + +#: natmap.js:208 +msgid "Target_Interface" +msgstr "目标接口" + +#: natmap.js:90 natmap.js:256 +msgid "TCP" +msgstr "TCP" + +#: natmap.js:255 +msgid "TCP+UDP" +msgstr "TCP+UDP" + +#: natmap.js:319 +msgid "Telegram Bot" +msgstr "Telegram bot" + +#: natmap.js:347 +msgid "Telegram Token" +msgstr "Telegram 令牌" + +#: natmap.js:501 +msgid "Transmission" +msgstr "Transmission" + +#: natmap.js:91 natmap.js:257 +msgid "UDP" +msgstr "UDP" + +#: natmap.js:594 +msgid "Update host with IP" +msgstr "使用 IP 更新 host" + +#: natmap.js:583 +msgid "Update HTTPS Port" +msgstr "更新 HTTPS 端口" + +#: natmap.js:614 natmap.js:653 +msgid "Username" +msgstr "用户名" + +#: natmap.js:108 +msgid "Wan Interface" +msgstr "WAN 接口" + +#: natmap.js:605 +msgid "Web UI URL" +msgstr "网页 URL" + diff --git a/luci-app-natmap/luci-app-natmap/po/zh_Hant/luci-app-natmap.po b/luci-app-natmap/luci-app-natmap/po/zh_Hant/luci-app-natmap.po new file mode 100644 index 000000000..00ddc93ef --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/po/zh_Hant/luci-app-natmap.po @@ -0,0 +1,397 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: luci-app-natmap\n" +"Language: zh-Hant\n" + +#: natmap.js:62 +msgid "General Settings" +msgstr "常規設定" + +#: natmap.js:63 +msgid "Forward Settings" +msgstr "轉送設定" + +#: natmap.js:64 +msgid "Notify Settings" +msgstr "通知設定" + +#: natmap.js:65 +msgid "Link Settings" +msgstr "關聯設定" + +#: natmap.js:66 +msgid "Custom Settings" +msgstr "自訂設定" + +#: natmap.js:78 +msgid "Name" +msgstr "名稱" + +#: natmap.js:87 +msgid "Protocol" +msgstr "協定" + +#: natmap.js:90 natmap.js:256 +msgid "TCP" +msgstr "TCP" + +#: natmap.js:91 natmap.js:257 +msgid "UDP" +msgstr "UDP" + +#: natmap.js:97 +msgid "Restrict to address family" +msgstr "位址戶數限制" + +#: natmap.js:100 +msgid "IPv4 and IPv6" +msgstr "IPv4 和 IPv6" + +#: natmap.js:101 +msgid "IPv4 only" +msgstr "單一IPv4" + +#: natmap.js:102 +msgid "IPv6 only" +msgstr "單一IPv6" + +#: natmap.js:108 +msgid "Wan Interface" +msgstr "WAN 介面" + +#: natmap.js:117 +msgid "Keep-alive interval" +msgstr "Keep-alive 間隔" + +#: natmap.js:127 +msgid "STUN server" +msgstr "STUN伺服器" + +#: natmap.js:138 +msgid "HTTP server" +msgstr "HTTP伺服器" + +#: natmap.js:139 +msgid "For TCP mode" +msgstr "用於tcp模式" + +#: natmap.js:145 +msgid "Bind port" +msgstr "綁定連接埠" + +#: natmap.js:156 +msgid "Enable Forward" +msgstr "啟用轉送" + +#: natmap.js:170 +msgid "Forward mode" +msgstr "轉送模式" + +#: natmap.js:173 +msgid "firewall dnat" +msgstr "firewall dnat" + +#: natmap.js:174 +msgid "natmap" +msgstr "natmap" + +#: natmap.js:175 +msgid "ikuai" +msgstr "ikuai" + +#: natmap.js:183 +msgid "Forward target" +msgstr "轉送目標" + +#: natmap.js:195 +msgid "Forward target port" +msgstr "轉送目標連接埠" + +#: natmap.js:196 +msgid "0 will forward to the out port get from STUN" +msgstr "填寫 0 ,則轉送至從STUN取得的外部端口" + +#: natmap.js:208 +msgid "Target_Interface" +msgstr "目標介面" + +#: natmap.js:218 +msgid "Ikuai Web URL" +msgstr "愛快網址" + +#: natmap.js:220 natmap.js:607 natmap.js:646 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP and DNS" +msgstr "例如:http://127.0.0.1:8080 或 http://ikuai.lan:8080。如果使用 host ,必須關閉 DHCP/DNS 中的重綁定保護" + +#: natmap.js:231 +msgid "Ikuai Username" +msgstr "愛快用戶名" + +#: natmap.js:241 +msgid "Ikuai Password" +msgstr "愛快密碼" + +#: natmap.js:251 +msgid "Ikuai Mapping Protocol" +msgstr "連接埠映射協定" + +#: natmap.js:252 +msgid "such as tcp or udp or tcp+udp" +msgstr "例如:tcp 或 udp 或 tcp+udp" + +#: natmap.js:255 +msgid "TCP+UDP" +msgstr "TCP+UDP" + +#: natmap.js:264 +msgid "Ikuai Mapping Wan Interface" +msgstr "連接埠映射外網介面" + +#: natmap.js:265 +msgid "such as adsl_1 or wan" +msgstr "例如:adsl_1 或 wan" + +#: natmap.js:276 natmap.js:458 natmap.js:683 +msgid "Advanced Settings" +msgstr "進階設定" + +#: natmap.js:286 natmap.js:471 natmap.js:697 +msgid "Max Retries" +msgstr "最大重試次數" + +#: natmap.js:287 natmap.js:472 natmap.js:698 +msgid "max retries,default 0 means execute only once" +msgstr "最大重試次數,預設 0 表示僅執行一次調用" + +#: natmap.js:297 natmap.js:482 natmap.js:708 +msgid "Retry Interval" +msgstr "重試間隔時間" + +#: natmap.js:298 natmap.js:483 natmap.js:709 +msgid "Retry Interval, unit is seconds, default 0 is 3 seconds" +msgstr "單次重試間隔時間,單位是秒,預設 0 表示 3秒" + +#: natmap.js:307 +msgid "Enable Notify" +msgstr "啟用通知" + +#: natmap.js:315 +msgid "Notify channel" +msgstr "通知頻道" + +#: natmap.js:319 +msgid "Telegram Bot" +msgstr "Telegram Bot" + +#: natmap.js:320 +msgid "PushPlus" +msgstr "PushPlus" + +#: natmap.js:321 +msgid "ServerChan" +msgstr "Server醬" + +#: natmap.js:322 +msgid "Gotify" +msgstr "Gotify" + +#: natmap.js:329 +msgid "Chat ID" +msgstr "會話ID" + +#: natmap.js:332 +msgid "Get chat_id" +msgstr "獲取chat_id" + +#: natmap.js:334 natmap.js:352 natmap.js:374 natmap.js:390 natmap.js:426 +msgid "Click here" +msgstr "點這裡" + +#: natmap.js:337 +msgid "
If you want to send to a group/channel, please create a non-Chinese group/channel (for easier chatid lookup, you can rename it later).
Add the bot to the group, send a message, and use https://api.telegram.org/bot token /getUpdates to obtain the chatid." +msgstr "
如需透過群組/自主頻道,請透過建立一個非中文的群組或頻道(此時找到chatid,後續再更名)
將新增機器人至群組,發送訊息後https: // /api.telegram.org/bot token /getUpdates 取得chatid" + +#: natmap.js:350 +msgid "Get Bot" +msgstr "取得機器人" + +#: natmap.js:354 +msgid "
Send a message to the created bot to initiate a conversation." +msgstr "
與創建的機器人發送訊息,開啟對話" + +#: natmap.js:363 +msgid "http proxy" +msgstr "http代理" + +#: natmap.js:372 natmap.js:388 natmap.js:424 +msgid "Get Instructions" +msgstr "獲取介紹" + +#: natmap.js:385 +msgid "ServerChan sendkey" +msgstr "Server醬sendkey" + +#: natmap.js:393 +msgid "
Since the asynchronous push queue is used, only whether the put into the queue is successful is detected." +msgstr "
由於使用佇列隊列,所以僅檢測隊列是否成功" + +#: natmap.js:404 +msgid "ServerChan Advanced Settings" +msgstr "伺服器醬高級設定" + +#: natmap.js:414 +msgid "Self-built Server Url" +msgstr "自建伺服器URL" + +#: natmap.js:416 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080" +msgstr "例如:http://127.0.0.1:8080 或 http://ikuai.lan:8080" + +#: natmap.js:422 +msgid "Gotify url" +msgstr "Gotify 網址" + +#: natmap.js:446 +msgid "Gotify priority" +msgstr "Gotify 通知優先級" + +#: natmap.js:492 +msgid "Enable link setting" +msgstr "啟用關聯設定" + +#: natmap.js:496 +msgid "Service" +msgstr "服務" + +#: natmap.js:499 +msgid "Emby" +msgstr "Emby" + +#: natmap.js:500 +msgid "qBittorrent" +msgstr "qBittorrent" + +#: natmap.js:501 +msgid "Transmission" +msgstr "Transmission" + +#: natmap.js:502 +msgid "Cloudflare Origin Rule" +msgstr "Cloudflare Origin Rule" + +#: natmap.js:503 +msgid "Cloudflare Redirect Rule" +msgstr "Cloudflare Redirect Rule" + +#: natmap.js:574 +msgid "API Key" +msgstr "API key" + +#: natmap.js:565 +msgid "EMBY URL" +msgstr "EMBY 網址" + +#: natmap.js:583 +msgid "Update HTTPS Port" +msgstr "更新 HTTPS 連接埠" + +#: natmap.js:584 +msgid "Set to False if you want to use HTTP" +msgstr "若使用HTTP,請勿勾選" + +#: natmap.js:594 +msgid "Update host with IP" +msgstr "使用IP更新host" + +#: natmap.js:605 +msgid "Web UI URL" +msgstr "網址" + +#: natmap.js:614 natmap.js:653 +msgid "Username" +msgstr "使用者名稱" + +#: natmap.js:619 natmap.js:658 +msgid "Password" +msgstr "密碼" + +#: natmap.js:624 natmap.js:663 +msgid "Allow IPv6" +msgstr "允許IPv6" + +#: natmap.js:633 natmap.js:672 +msgid "IPv6 Address" +msgstr "IPv6位址" + +#: natmap.js:644 +msgid "RPC URL" +msgstr "RPC 位址" + +#: natmap.js:722 +msgid "Enable custom script's config" +msgstr "授權自訂腳本" + +#: natmap.js:731 +msgid "custom script" +msgstr "自訂腳本" + +#: natmap.js:732 +msgid "custom script path,such as /etc/natmap/custom.sh" +msgstr "自訂腳本路徑,例如:/etc/natmap/custom.sh" + +#: natmap.js:741 +msgid "External IP" +msgstr "外部ip" + +#: natmap.js:748 +msgid "External Port" +msgstr "外部連接埠" + +#: natmap.js:758 +msgid "enable" +msgstr "啟用" + +#: natmap.js:57 +msgid "NatMap Settings" +msgstr "國家地圖設置" + +#: natmap.js:510 +msgid "Cloudflare Token" +msgstr "Cloudflare 令牌" + +#: natmap.js:521 +msgid "Cloudflare Zone ID" +msgstr "Cloudflare 區域 ID" + +#: natmap.js:533 +msgid "Origin Rule Name" +msgstr "原產地規則名稱" + +#: natmap.js:544 +msgid "Redirect Rule Name" +msgstr "重定向規則名稱" + +#: natmap.js:347 +msgid "Telegram Token" +msgstr "電報令牌" + +#: natmap.js:370 +msgid "PushPlus Token" +msgstr "PushPlus 令牌" + +#: natmap.js:436 +msgid "Gotify Token" +msgstr "Gotify 令牌" + +#: natmap.js:554 +msgid "Redirect Rule Target URL" +msgstr "重定向規則目標URL" + +#: natmap.js:567 +msgid "such as http://127.0.0.1:8080 or http://ikuai.lan:8080.if use host,must close Rebind protection in DHCP/DNS" +msgstr "例如 http://127.0.0.1:8080 或 http://ikuai.lan:8080。如果使用主機位址而不是 ip,則必須關閉 DHCP/DNS 中的重綁定保護" + diff --git a/luci-app-natmap/luci-app-natmap/root/etc/config/natmap b/luci-app-natmap/luci-app-natmap/root/etc/config/natmap new file mode 100755 index 000000000..8d8529b56 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/etc/config/natmap @@ -0,0 +1,68 @@ +config natmap + option natmap_enable '0' + option general_nat_name '' + option general_nat_protocol 'tcp' + option general_ip_address_family '' + option general_wan_interface '' + option general_interval '' + option general_stun_server 'stunserver.stunprotocol.org' + option general_http_server 'example.com' + option general_bind_port '8080' + option forward_enable '0' + option forward_target_ip '' + option forward_target_port '' + option forward_mode 'firewall' + option forward_natmap_target_interface '' + option forward_ikuai_web_url '' + option forward_ikuai_username '' + option forward_ikuai_password '' + option forward_ikuai_mapping_protocol '' + option forward_ikuai_mapping_wan_interface '' + option forward_advanced_enable '0' + option forward_advanced_max_retries '1' + option forward_advanced_sleep_time '3' + option notify_enable '0' + option notify_mode 'telegram_bot' + option notify_telegram_bot_chat_id 'chat_id' + option notify_telegram_bot_token 'token' + option notify_telegram_bot_proxy '' + option notify_pushplus_token 'token' + option notify_serverchan_sendkey '' + option notify_serverchan_advanced_enable '0' + option notify_serverchan_advanced_url '' + option notify_gotify_url '' + option notify_gotify_token '' + option notify_gotify_priority '5' + option notify_advanced_enable '0' + option notify_advanced_max_retries '1' + option notify_advanced_sleep_time '3' + option link_enable '0' + option link_mode 'qbittorrent' + option link_emby_url '' + option link_emby_api_key '' + option link_emby_use_https '0' + option link_emby_update_host_with_ip '0' + option link_qb_web_url '' + option link_qb_username '' + option link_qb_password '' + option link_qb_ipv6_address '::/-64' + option link_qb_allow_ipv6 '1' + option link_tr_rpc_url '' + option link_tr_username '' + option link_tr_password '' + option link_tr_ipv6_address '::/-64' + option link_tr_allow_ipv6 '1' + option link_cloudflare_token '' + option link_cloudflare_zone_id '' + option link_cloudflare_origin_rule_name '' + option link_cloudflare_redirect_rule_name '' + option link_cloudflare_redirect_rule_target_url '' + option link_advanced_enable '0' + option link_advanced_max_retries '1' + option link_advanced_sleep_time '3' + option custom_script_enable '0' + option custom_script_path '' + + + + diff --git a/luci-app-natmap/luci-app-natmap/root/etc/init.d/natmap b/luci-app-natmap/luci-app-natmap/root/etc/init.d/natmap new file mode 100755 index 000000000..ce0ec9040 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/etc/init.d/natmap @@ -0,0 +1,190 @@ +#!/bin/sh /etc/rc.common + +START=99 +USE_PROCD=1 + +NAME=natmap +PROG=/usr/bin/$NAME + +STATUS_PATH=/var/run/natmap + +load_interfaces() { + config_get interface "$1" interface + config_get natmap_enable "$1" natmap_enable 1 + + [ "${natmap_enable}" = "1" ] && interfaces=" ${interface} ${interfaces}" +} + +validate_section_natmap() { + uci_load_validate "${NAME}" natmap "$1" "$2" \ + 'natmap_enable:bool:0' \ + 'general_ip_address_family:string' \ + 'general_nat_protocol:string' \ + 'general_wan_interface:string' \ + 'general_nat_name:string' \ + 'general_interval:uinteger' \ + 'general_stun_server:host' \ + 'general_http_server:host' \ + 'general_bind_port:port' \ + 'forward_enable:bool:0' \ + 'forward_target_ip:host' \ + 'forward_target_port:port' \ + 'forward_mode:string' \ + 'forward_natmap_target_interface:string' \ + 'forward_ikuai_web_url:string' \ + 'forward_ikuai_username:string' \ + 'forward_ikuai_password:string' \ + 'forward_ikuai_mapping_protocol:string' \ + 'forward_ikuai_mapping_wan_interface:string' \ + 'forward_advanced_enable:bool:0' \ + 'forward_advanced_max_retries:uinteger' \ + 'forward_advanced_sleep_time:uinteger' \ + 'notify_enable:bool:0' \ + 'notify_mode:string' \ + 'notify_telegram_bot_chat_id:string' \ + 'notify_telegram_bot_token:string' \ + 'notify_telegram_bot_proxy:string' \ + 'notify_pushplus_token:string' \ + 'notify_serverchan_sendkey:string' \ + 'notify_serverchan_advanced_enable:bool:0' \ + 'notify_serverchan_advanced_url:string' \ + 'notify_gotify_url:string' \ + 'notify_gotify_token:string' \ + 'notify_gotify_priority:uinteger' \ + 'notify_advanced_enable:bool:0' \ + 'notify_advanced_max_retries:uinteger' \ + 'notify_advanced_sleep_time:uinteger' \ + 'link_enable:bool:0' \ + 'link_mode:string' \ + 'link_emby_url:string' \ + 'link_emby_api_key:string' \ + 'link_emby_use_https:bool:0' \ + 'link_emby_update_host_with_ip:bool:0' \ + 'link_qb_web_url:string' \ + 'link_qb_username:string' \ + 'link_qb_password:string' \ + 'link_qb_ipv6_address:string' \ + 'link_qb_allow_ipv6:bool:0' \ + 'link_tr_rpc_url:string' \ + 'link_tr_username:string' \ + 'link_tr_password:string' \ + 'link_tr_allow_ipv6:bool:0' \ + 'link_tr_ipv6_address:string' \ + 'link_cloudflare_token:string' \ + 'link_cloudflare_zone_id:string' \ + 'link_cloudflare_origin_rule_name:string' \ + 'link_cloudflare_redirect_rule_name:string' \ + 'link_cloudflare_redirect_rule_target_url:string' \ + 'link_advanced_enable:bool:0' \ + 'link_advanced_max_retries:uinteger' \ + 'link_advanced_sleep_time:uinteger' \ + 'custom_script_enable:bool:0' \ + 'custom_script_path:file' +} + +natmap_instance() { + [ "$2" = 0 ] || { + echo "validation failed" + return 1 + } + + [ "${natmap_enable}" = 0 ] && return 1 + + procd_open_instance "$1" + procd_set_param command "$PROG" \ + ${general_interval:+-k "$general_interval"} \ + ${general_stun_server:+-s "$general_stun_server"} \ + ${general_http_server:+-h "$general_http_server"} \ + ${general_bind_port:+-b "$general_bind_port"} + + [ "${general_ip_address_family}" = ipv4 ] && procd_append_param command -4 + [ "${general_ip_address_family}" = ipv6 ] && procd_append_param command -6 + [ "${general_nat_protocol}" = 'udp' ] && procd_append_param command -u + + [ -n "${general_wan_interface}" ] && { + local ifname + + network_get_device ifname "$general_wan_interface" || ifname="$general_wan_interface" + procd_append_param command -i "$ifname" + procd_append_param netdev "$ifname" + } + + [ -n "${forward_natmap_target_interface}" ] && { + local ifname + + network_get_device ifname "$forward_natmap_target_interface" || ifname="$forward_natmap_target_interface" + procd_append_param command -i "$ifname" + procd_append_param netdev "$ifname" + } + + [ "${forward_enable}" == 1 ] && [ "${forward_mode}" = natmap ] && [ -n "${forward_target_ip}" ] && procd_append_param command -t "$forward_target_ip" -p "$forward_target_port" + for e in $EXTRA_ENV; do + eval value=\$${e} + option_name=$(echo "$e" | tr 'a-z' 'A-Z') + if [ -n "$value" ]; then + procd_append_param env $option_name="$value" + fi + done + + # console.log "natmap" + procd_append_param command -e /usr/share/natmap/update.sh + + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + + procd_close_instance +} + +clear_status_files() { + find "${STATUS_PATH}" -type f -print0 | xargs -0 rm -f -- +} + +service_triggers() { + local interfaces + + procd_add_reload_trigger "${NAME}" + + config_load "${NAME}" + config_foreach load_interfaces natmap + + [ -n "${interfaces}" ] && { + for n in $interfaces; do + procd_add_reload_interface_trigger $n + done + } + + procd_add_validation validate_section_natmap +} + +start_service() { + . /lib/functions/network.sh + config_cb() { + [ $# -eq 0 ] && return + + option_cb() { + local option="$1" + + EXTRA_ENV="$EXTRA_ENV $option" + } + + list_cb() { + local name="$1" + EXTRA_ENV="$EXTRA_ENV $name" + } + } + mkdir -p "${STATUS_PATH}" + clear_status_files + + config_load "${NAME}" + config_foreach validate_section_natmap natmap natmap_instance +} + +reload_service() { + stop + start +} + +service_stopped() { + clear_status_files +} diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/luci/menu.d/luci-app-natmap.json b/luci-app-natmap/luci-app-natmap/root/usr/share/luci/menu.d/luci-app-natmap.json new file mode 100644 index 000000000..f17616852 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/luci/menu.d/luci-app-natmap.json @@ -0,0 +1,14 @@ +{ + "admin/services/natmap": { + "title": "NATMap", + "action": { + "type": "view", + "path": "natmap" + }, + "depends": { + "acl": [ + "luci-app-natmap" + ] + } + } +} \ No newline at end of file diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/forward.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/forward.sh new file mode 100755 index 000000000..5792c4e9a --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/forward.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# NATMap +outter_ip=$1 +outter_port=$2 +ip4p=$3 +inner_port=$4 +protocol=$5 + +# 如果$forward_target_port为空或者$forward_target_ip为空则退出 +if [ -z "$FORWARD_TARGET_PORT" ] || [ -z "$FORWARD_TARGET_IP" ]; then + exit 0 +fi + +forward_script="" +# case $FORWARD_MODE in +# "firewall") +# forward_script="/usr/share/natmap/plugin-forward/firewall-forward.sh" +# ;; +# "ikuai") +# forward_script="/usr/share/natmap/plugin-forward/ikuai-forward.sh" +# ;; +# *) +# forward_script="" +# ;; +# esac + +# 如果$FORWARD_MODE非空则执行对应的脚本 +if [ -n "${FORWARD_MODE}" ]; then + forward_script="/usr/share/natmap/plugin-forward/${FORWARD_MODE}-forward.sh" +fi + +if [ -n "${forward_script}" ]; then + # echo "$GENERAL_NAT_NAME execute forward script" + bash "$forward_script" "$@" +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/link.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/link.sh new file mode 100755 index 000000000..8a45eb21c --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/link.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# NATMap +outter_ip=$1 +outter_port=$2 +ip4p=$3 +inner_port=$4 +protocol=$5 + +link_script="" +# echo "LINK_MODE: $LINK_MODE" + +# 如果$LINK_MODE非空则执行对应的脚本 +if [ -n "${LINK_MODE}" ]; then + link_script="/usr/share/natmap/plugin-link/${LINK_MODE}.sh" +fi + +if [ -n "${link_script}" ]; then + echo "$GENERAL_NAT_NAME execute link script" + bash "${link_script}" "$@" +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/notify.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/notify.sh new file mode 100755 index 000000000..a7647ce24 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/notify.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# NATMap +outter_ip=$1 +outter_port=$2 +ip4p=$3 +inner_port=$4 +protocol=$(echo $5 | tr 'a-z' 'A-Z') + +msg="${GENERAL_NAT_NAME} +New ${protocol} port mapping: ${inner_port} -> ${outter_ip}:${outter_port} +IP4P: ${ip4p}" +if [ ! -z "$MSG_OVERRIDE" ]; then + msg="$MSG_OVERRIDE" +fi + +# notify_mode 判断 +notify_script="" +# case $NOTIFY_MODE in +# "telegram_bot") +# notify_script="/usr/share/natmap/plugin-notify/telegram_bot.sh" +# ;; +# "pushplus") +# notify_script="/usr/share/natmap/plugin-notify/pushplus.sh" +# ;; +# "serverchan") +# notify_script="/usr/share/natmap/plugin-notify/serverchan.sh" +# ;; +# "gotify") +# notify_script="/usr/share/natmap/plugin-notify/gotify.sh" +# ;; +# *) +# notify_script="" +# ;; +# esac + +# 如果$NOTIFY_MODE非空则执行对应的脚本 +if [ -n "${NOTIFY_MODE}" ]; then + notify_script="/usr/share/natmap/plugin-notify/$NOTIFY_MODE.sh" +fi + +if [ -n "${notify_script}" ]; then + echo "$GENERAL_NAT_NAME execute notify script" + bash "$notify_script" "$msg" +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-forward/firewall-forward.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-forward/firewall-forward.sh new file mode 100644 index 000000000..fba195b32 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-forward/firewall-forward.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# NATMap +outter_ip=$1 +outter_port=$2 +ip4p=$3 +inner_port=$4 +protocol=$5 + +# if [ "$FORWARD_MODE" != firewall ]; then +# exit 0 +# fi + +if [ -z "$FORWARD_TARGET_PORT" ]; then + exit 0 +fi + +if [ -z "$FORWARD_TARGET_IP" ]; then + exit 0 +fi + +# get forward target port +# final_forward_target_port=$([ "${FORWARD_TARGET_PORT}" == 0 ] ? $outter_port : "${FORWARD_TARGET_PORT}") +final_forward_target_port=$((FORWARD_TARGET_PORT == 0 ? outter_port : FORWARD_TARGET_PORT)) + +# ipv4 firewall +rule_name_v4=$(echo "${GENERAL_NAT_NAME}_v4" | sed 's/[^a-zA-Z0-9]/_/g' | awk '{print tolower($0)}') + +# ipv4 redirect +uci set firewall.$rule_name_v4=redirect +uci set firewall.$rule_name_v4.name="$rule_name_v4" +uci set firewall.$rule_name_v4.proto="$protocol" +uci set firewall.$rule_name_v4.src="$GENERAL_WAN_INTERFACE" +uci set firewall.$rule_name_v4.dest="$FORWOARD_TARGET_INTERFACE" +uci set firewall.$rule_name_v4.target='DNAT' +uci set firewall.$rule_name_v4.src_dport="${inner_port}" +uci set firewall.$rule_name_v4.dest_ip="${FORWARD_TARGET_IP}" +uci set firewall.$rule_name_v4.dest_port="${final_forward_target_port}" + +# -------------------------------------------------------------------------------------------- +# QB and TR ipv6 forward +# 检测link_enable +if [ "${LINK_ENABLE}" != 1 ]; then + exit 0 +fi + +if [ [ "${LINK_MODE}" = transmission ] && [ "${LINK_TR_ALLOW_IPV6}" = 1 ] ] || [ [ "${LINK_MODE}" = qbittorrent ] && ["${LINK_QB_ALLOW_IPV6}" != 1 ] ]; then + + # get rule name + rule_name_v6=$(echo "${GENERAL_NAT_NAME}_v6_allow" | sed 's/[^a-zA-Z0-9]/_/g' | awk '{print tolower($0)}') + + # echo "rule_name_v6: $rule_name_v6" + # ipv6 allow + uci set firewall.$rule_name_v6=rule + uci set firewall.$rule_name_v6.name="$rule_name_v6" + uci set firewall.$rule_name_v6.src="$GENERAL_WAN_INTERFACE" + uci set firewall.$rule_name_v6.dest="$FORWOARD_TARGET_INTERFACE" + uci set firewall.$rule_name_v6.target='ACCEPT' + uci set firewall.$rule_name_v6.proto="$protocol" + uci set firewall.$rule_name_v6.family='ipv6' + uci set firewall.$rule_name_v6.dest_port="$final_forward_target_port" + + # check if dest_ip is already set with return code + if uci get firewall.$rule_name_v6.dest_ip >/dev/null 2>&1; then + uci del firewall.$rule_name_v6.dest_ip + fi + + # add dest_ip list + case "${LINK_MODE}" in + "transmission") + for ip in $LINK_TR_IPV6_ADDRESS; do + uci add_list firewall.$rule_name_v6.dest_ip="${ip}" + done + ;; + "qbittorrent") + for ip in $LINK_QB_IPV6_ADDRESS; do + uci add_list firewall.$rule_name_v6.dest_ip="${ip}" + done + ;; + esac +fi + +# reload +uci commit firewall +/etc/init.d/firewall reload diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-forward/ikuai-forward.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-forward/ikuai-forward.sh new file mode 100755 index 000000000..f2829aa08 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-forward/ikuai-forward.sh @@ -0,0 +1,222 @@ +#!/bin/bash +# ikuai_version=3.7.6 + +# natmap +outter_ip=$1 +outter_port=$2 +ip4p=$3 +inner_port=$4 +protocol=$5 + +## ikuai参数获取 +# lan_port +mapping_lan_port="" +if [ -z "${FORWARD_TARGET_PORT}" ] || [ "${FORWARD_TARGET_PORT}" -eq 0 ]; then + mapping_lan_port=$outter_port +else + mapping_lan_port=${FORWARD_TARGET_PORT} +fi + +# login api and call api +ikuai_login_api="/Action/login" +ikuai_call_api="/Action/call" +call_url="$(echo $FORWARD_IKUAI_WEB_URL | sed 's/\/$//')${ikuai_call_api}" +login_url="$(echo $FORWARD_IKUAI_WEB_URL | sed 's/\/$//')${ikuai_login_api}" + +# 浏览器headers +headers='{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36", + "Accept": "application/json", + "Content-type": "application/json;charset=utf-8", + "Accept-Language": "zh-CN"}' + +# 登录ikuai +# This function performs the login action +# Parameters: +# - login_username: username for login +# - login_password: password for login +function login_action() { + local login_username="$1" + local login_password="$2" + + # Calculate the MD5 hash value of the password and convert it to hexadecimal + local passwd=$(echo -n "$login_password" | openssl dgst -md5 -hex | awk '{print $2}') + + # Concatenate 'salt_11' and the password, and encode it using base64 + local pass=$(echo -n "salt_11$passwd" | openssl enc -base64) + + # Create the JSON payload for the login request + local login_params='{ + "username": "'"$login_username"'", + "passwd": "'"$passwd"'", + "pass": "'"$pass"'", + "remember_password": "" + }' + + # Send the login request, extract the session ID (cookie) from the response headers, and store it in a variable + local login_cookie=$(curl -s -D - -H "$headers" -X POST -d "$login_params" "$login_url" | awk -F' ' '/Set-Cookie:/ {print $2}') + + # echo the login_cookie + echo "$login_cookie" +} + +# 查询端口映射 +# Function to show the mapping action +# Parameters: +# - show_cookie: Cookie value for authentication +# - show_comment: Comment to filter the results +function show_mapping_action() { + local show_cookie="$1" + local show_comment="$2" + # Construct the payload for the API request + local show_payload='{ + "func_name": "dnat", + "action": "show", + "param": { + "FINDS": "lan_addr,lan_port,wan_port,comment", + "KEYWORDS": "'"$show_comment"'", + "TYPE": "total,data", + "limit": "0,20", + "ORDER_BY": "", + "ORDER": "" + } + }' + + # Send the API request and store the response in show_result variable + local show_result=$(curl -s -X POST -H "$headers" -b "$show_cookie" -d "$show_payload" "$call_url") + # Extract the show_ids from the response using jq + local show_ids=$(echo "$show_result" | jq -r '.Data.data[].id') + + # echo the show_ids + # echo "${show_ids[@]}" + for id in "${show_ids[@]}"; do + echo "$id" + done +} + +# 删除端口映射 +# Deletes a mapping action +# Arguments: +# - del_cookie: The cookie used for authentication +# - del_ids: An array of DNAT IDs to be deleted +function del_mapping_action() { + local del_cookie="$1" + local del_id="$2" + # Declare an empty array to store the delete response + local del_result="" + + # Construct the payload for the delete request. + local del_payload='{ + "func_name": "dnat", + "action": "del", + "param": { + "id": "'"$del_id"'" + } + }' + + # Send the delete request using cURL and store the response. + del_response=$(curl -s -X POST -H "$headers" -b "$del_cookie" -d "$del_payload" "$call_url") + # echo "del_ids: $del_ids" + # echo "del_response: $del_response" +} + +# 增加端口映射 +# Function to add a mapping action +# Parameters: +# - add_cookie - The cookie for authentication +# - add_comment - The comment for the mapping action +function add_mapping_action() { + local add_cookie="$1" + local add_comment="$2" + local enabled="yes" + + # Create the payload JSON object + local add_payload='{ + "func_name": "dnat", + "action": "add", + "param": { + "enabled": "'"$enabled"'", + "comment": "'"$add_comment"'", + "interface": "'"$FORWARD_IKUAI_MAPPING_WAN_INTERFACE"'", + "lan_addr": "'"$FORWARD_TARGET_IP"'", + "protocol": "'"$FORWARD_IKUAI_MAPPING_PROTOCOL"'", + "wan_port": "'"$GENERAL_BIND_PORT"'", + "lan_port": "'"$mapping_lan_port"'", + "src_addr": "" + } + }' + + # Send the POST request to the specified URL with the payload + local add_result=$(curl -s -X POST -H "$headers" -b "$add_cookie" -d "$add_payload" "$call_url") + + # Output the result + echo "$add_result" +} + +# 初始化参数 +# cookie +cookie="" +# 端口映射id +dnat_ids=() +# 端口映射备注,区分不同的端口映射,查询时使用,唯一,不可重复 +comment="natmap-${GENERAL_NAT_NAME}" + +# 默认重试次数为1,休眠时间为3s +max_retries=1 +sleep_time=3 +retry_count=0 + +# 判断是否开启高级功能 +if [ "${FORWARD_ADVANCED_ENABLE}" == 1 ] && [ -n "$FORWARD_ADVANCED_MAX_RETRIES" ] && [ -n "$FORWARD_ADVANCED_SLEEP_TIME" ]; then + # 获取最大重试次数 + max_retries=$((FORWARD_ADVANCED_MAX_RETRIES == "0" ? 1 : FORWARD_ADVANCED_MAX_RETRIES)) + # 获取休眠时间 + sleep_time=$((FORWARD_ADVANCED_SLEEP_TIME == "0" ? 3 : FORWARD_ADVANCED_SLEEP_TIME)) +fi + +# 端口映射处理开始 +for ((retry_count = 0; retry_count <= max_retries; retry_count++)); do + # 登录 + cookie=$(login_action "$FORWARD_IKUAI_USERNAME" "$FORWARD_IKUAI_PASSWORD") + # echo "cookie: $cookie" + + if [ -n "$cookie" ]; then + # echo "$GENERAL_NAT_NAME - $FORWARD_MODE 登录成功" + + # 查询端口映射id + dnat_ids=($(show_mapping_action "$cookie" "$comment")) + # echo "dnat_ids: ${dnat_ids[@]}" + + # 删除端口映射 + for dnat_id in "${dnat_ids[@]}"; do + del_mapping_action "$cookie" "$dnat_id" + done + + # 再次查询端口映射id + dnat_ids=($(show_mapping_action "$cookie" "$comment")) + + # 验证对应端口映射是否全部删除 + if [ ${#dnat_ids[@]} -eq 0 ]; then + # echo "$GENERAL_NAT_NAME - $FORWARD_MODE Port mapping deleted successfully" + + # 添加端口映射 + add_response=$(add_mapping_action "$cookie" "$comment") + # Check if the modification was successful + if [ "$(echo "$add_response" | jq -r '.ErrMsg')" = "Success" ]; then + echo "$GENERAL_NAT_NAME - $FORWARD_MODE Port mapping modified successfully" + break + else + echo "$GENERAL_NAT_NAME - $FORWARD_MODE Failed to modify the port mapping" + fi + else + echo "$GENERAL_NAT_NAME - $FORWARD_MODE Failed to delete the port mapping" + fi + fi + # echo "$FORWARD_MODE 修改失败,休眠$sleep_time秒" + sleep $sleep_time +done + +# Check if maximum retries reached +if [ $retry_count -eq $max_retries ]; then + echo "$GENERAL_NAT_NAME - $FORWARD_MODE 达到最大重试次数,无法修改" + exit 1 +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/cloudflare_origin_rule.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/cloudflare_origin_rule.sh new file mode 100755 index 000000000..2015ce7ba --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/cloudflare_origin_rule.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# NATMap +outter_ip=$1 +outter_port=$2 + +function get_current_rule() { + # Function to get the current rule + # + # Returns: + # string: The current rule + curl --request GET \ + --url https://api.cloudflare.com/client/v4/zones/$LINK_CLOUDFLARE_ZONE_ID/rulesets/phases/http_request_origin/entrypoint \ + --header "Authorization: Bearer $LINK_CLOUDFLARE_TOKEN" \ + --header 'Content-Type: application/json' +} + +# 默认重试次数为1,休眠时间为3s +max_retries=1 +sleep_time=3 + +# 判断是否开启高级功能 +if [ "$LINK_ADVANCED_ENABLE" == 1 ] && [ -n "$LINK_ADVANCED_MAX_RETRIES" ] && [ -n "$LINK_ADVANCED_SLEEP_TIME" ]; then + # 获取最大重试次数 + max_retries=$((LINK_ADVANCED_MAX_RETRIES == "0" ? 1 : LINK_ADVANCED_MAX_RETRIES)) + # 获取休眠时间 + sleep_time=$((LINK_ADVANCED_SLEEP_TIME == "0" ? 3 : LINK_ADVANCED_SLEEP_TIME)) +fi + +# 初始化参数 +# currrent_rule="" +retry_count=0 +# cloudflare_ruleset_id="" + +for ((retry_count = 0; retry_count < max_retries; retry_count++)); do + local currrent_rule=$(get_current_rule) + local cloudflare_ruleset_id=$(echo "$currrent_rule" | jq '.result.id' | sed 's/"//g') + + if [ -z "$cloudflare_ruleset_id" ]; then + # echo "$GENERAL_NAT_NAME - $LINK_MODE 登录失败,休眠$sleep_time秒" + sleep $sleep_time + else + echo "$GENERAL_NAT_NAME - $LINK_MODE 登录成功" + # 修改 + LINK_CLOUDFLARE_ORIGIN_RULE_NAME="\"$LINK_CLOUDFLARE_ORIGIN_RULE_NAME\"" + local new_rule=$(echo "$currrent_rule" | jq '.result.rules| to_entries | map(select(.value.description == '"$LINK_CLOUDFLARE_ORIGIN_RULE_NAME"')) | .[].key') + new_rule=$(echo "$currrent_rule" | jq '.result.rules['"$new_rule"'].action_parameters.origin.port = '"$outter_port"'') + + # delete last_updated + local body=$(echo "$new_rule" | jq '.result | del(.last_updated)') + curl --request PUT \ + --url https://api.cloudflare.com/client/v4/zones/$LINK_CLOUDFLARE_ZONE_ID/rulesets/$cloudflare_ruleset_id \ + --header "Authorization: Bearer $LINK_CLOUDFLARE_TOKEN" \ + --header 'Content-Type: application/json' \ + --data "$body" + break + fi +done + +# Check if maximum retries reached +if [ $retry_count -eq $max_retries ]; then + echo "$GENERAL_NAT_NAME - $LINK_MODE 达到最大重试次数,无法修改" + exit 1 +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/cloudflare_redirect_rule.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/cloudflare_redirect_rule.sh new file mode 100755 index 000000000..3f2775720 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/cloudflare_redirect_rule.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# NATMap +outter_ip=$1 +outter_port=$2 + +function get_current_rule() { + curl --request GET \ + --url https://api.cloudflare.com/client/v4/zones/$LINK_CLOUDFLARE_ZONE_ID/rulesets/phases/http_request_dynamic_redirect/entrypoint \ + --header "Authorization: Bearer $LINK_CLOUDFLARE_TOKEN" \ + --header 'Content-Type: application/json' +} + +# 默认重试次数为1,休眠时间为3s +max_retries=1 +sleep_time=3 + +# 判断是否开启高级功能 +if [ "$LINK_ADVANCED_ENABLE" == 1 ] && [ -n "$LINK_ADVANCED_MAX_RETRIES" ] && [ -n "$LINK_ADVANCED_SLEEP_TIME" ]; then + # 获取最大重试次数 + max_retries=$((LINK_ADVANCED_MAX_RETRIES == "0" ? 1 : LINK_ADVANCED_MAX_RETRIES)) + # 获取休眠时间 + sleep_time=$((LINK_ADVANCED_SLEEP_TIME == "0" ? 3 : LINK_ADVANCED_SLEEP_TIME)) +fi + +# 初始化参数 +# currrent_rule="" +retry_count=0 +# cloudflare_ruleset_id="" + +for ((retry_count = 0; retry_count < max_retries; retry_count++)); do + local currrent_rule=$(get_current_rule) + local cloudflare_ruleset_id=$(echo "$currrent_rule" | jq '.result.id' | sed 's/"//g') + + if [ -z "$cloudflare_ruleset_id" ]; then + # echo "$LINK_MODE 登录失败,休眠$sleep_time秒" + sleep $sleep_time + else + echo "$GENERAL_NAT_NAME - $LINK_MODE 登录成功" + + LINK_CLOUDFLARE_REDIRECT_RULE_NAME="\"$LINK_CLOUDFLARE_REDIRECT_RULE_NAME\"" + # replace NEW_PORT with outter_port + LINK_CLOUDFLARE_REDIRECT_RULE_TARGET_URL=$(echo $LINK_CLOUDFLARE_REDIRECT_RULE_TARGET_URL | sed 's/NEW_PORT/'"$outter_port"'/g') + local new_rule=$(echo "$currrent_rule" | jq '.result.rules| to_entries | map(select(.value.description == '"$LINK_CLOUDFLARE_REDIRECT_RULE_NAME"')) | .[].key') + new_rule=$(echo "$currrent_rule" | jq '.result.rules['"$new_rule"'].action_parameters.from_value.target_url.value = "'"$LINK_CLOUDFLARE_REDIRECT_RULE_TARGET_URL"'"') + + local body=$(echo "$new_rule" | jq '.result') + + # delete last_updated + body=$(echo "$body" | jq 'del(.last_updated)') + curl --request PUT \ + --url https://api.cloudflare.com/client/v4/zones/$LINK_CLOUDFLARE_ZONE_ID/rulesets/$cloudflare_ruleset_id \ + --header "Authorization: Bearer $LINK_CLOUDFLARE_TOKEN" \ + --header 'Content-Type: application/json' \ + --data "$body" + + break + fi +done + +# Check if maximum retries reached +if [ $retry_count -eq $max_retries ]; then + echo "$GENERAL_NAT_NAME - $LINK_MODE 达到最大重试次数,无法修改" + exit 1 +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/emby.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/emby.sh new file mode 100755 index 000000000..cd75ecc3b --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/emby.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# NATMap +outter_ip=$1 +outter_port=$2 +LINK_EMBY_URL=$(echo $LINK_EMBY_URL | sed 's/\/$//') + +# 默认重试次数为1,休眠时间为3s +max_retries=1 +sleep_time=3 + +# 判断是否开启高级功能 +if [ "$LINK_ADVANCED_ENABLE" == 1 ] && [ -n "$LINK_ADVANCED_MAX_RETRIES" ] && [ -n "$LINK_ADVANCED_SLEEP_TIME" ]; then + # 获取最大重试次数 + max_retries=$((LINK_ADVANCED_MAX_RETRIES == "0" ? 1 : LINK_ADVANCED_MAX_RETRIES)) + # 获取休眠时间 + sleep_time=$((LINK_ADVANCED_SLEEP_TIME == "0" ? 3 : LINK_ADVANCED_SLEEP_TIME)) +fi + +# 初始化参数 +current_cfg="" +retry_count=0 + +for ((retry_count = 0; retry_count < max_retries; retry_count++)); do + current_cfg=$(curl -v $LINK_EMBY_URL/emby/System/Configuration?api_key=$LINK_EMBY_API_KEY) + + if [ -z "$current_cfg" ]; then + # echo "$LINK_MODE 登录失败,休眠$sleep_time秒" + sleep $sleep_time + else + echo "$GENERAL_NAT_NAME - $LINK_MODE 登录成功" + new_cfg=$current_cfg + if [ ! -z $LINK_EMBY_USE_HTTPS ] && [ $LINK_EMBY_USE_HTTPS = '1' ]; then + new_cfg=$(echo $current_cfg | jq ".PublicHttpsPort = $outter_port") + else + new_cfg=$(echo $current_cfg | jq ".PublicPort = $outter_port") + fi + + if [ ! -z $LINK_EMBY_UPDATE_HOST_WITH_IP ] && [ $LINK_EMBY_UPDATE_HOST_WITH_IP = '1' ]; then + new_cfg=$(echo $new_cfg | jq ".WanDdns = \"$outter_ip\"") + fi + + curl -X POST "$LINK_EMBY_URL/emby/System/Configuration?api_key=$LINK_EMBY_API_KEY" -H "accept: */*" -H "Content-Type: application/json" -d "$new_cfg" + break + fi +done + +# Check if maximum retries reached +if [ $retry_count -eq $max_retries ]; then + echo "$GENERAL_NAT_NAME - $LINK_MODE 达到最大重试次数,无法修改" + exit 1 +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/qbittorrent.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/qbittorrent.sh new file mode 100644 index 000000000..e0cd34251 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/qbittorrent.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# NATMap +protocol=$5 +inner_port=$4 +outter_ip=$1 +outter_port=$2 +ip4p=$3 + +LINK_QB_WEB_URL=$(echo $LINK_QB_WEB_URL | sed 's/\/$//') + +# 默认重试次数为1,休眠时间为3s +max_retries=1 +sleep_time=3 + +# 判断是否开启高级功能 +if [ "$LINK_ADVANCED_ENABLE" == 1 ] && [ -n "$LINK_ADVANCED_MAX_RETRIES" ] && [ -n "$LINK_ADVANCED_SLEEP_TIME" ]; then + # 获取最大重试次数 + max_retries=$((LINK_ADVANCED_MAX_RETRIES == "0" ? 1 : LINK_ADVANCED_MAX_RETRIES)) + # 获取休眠时间 + sleep_time=$((LINK_ADVANCED_SLEEP_TIME == "0" ? 3 : LINK_ADVANCED_SLEEP_TIME)) +fi + +# 初始化参数 +# 获取qbcookie,直至重试次数用尽 +qbcookie="" +retry_count=0 + +for ((retry_count = 0; retry_count < max_retries; retry_count++)); do + # 获取qbcookie + qbcookie=$( + curl -Ssi -X POST \ + -d "username=$LINK_QB_USERNAME&password=$LINK_QB_PASSWORD" \ + "$LINK_QB_WEB_URL/api/v2/auth/login" | + sed -n 's/.*\(SID=.\{32\}\);.*/\1/p' + ) + + # 如果qbcookie为空,则重试 + if [ -z "$qbcookie" ]; then + # echo "$GENERAL_NAT_NAME - $LINK_MODE 登录失败,正在重试..." + sleep $sleep_time + else + echo "$GENERAL_NAT_NAME - $LINK_MODE 登录成功" + # 修改端口 + curl -s -X POST \ + -b "$qbcookie" \ + -d 'json={"listen_port":"'$outter_port'"}' \ + "$LINK_QB_WEB_URL/api/v2/app/setPreferences" + break + fi +done + +# Check if maximum retries reached +if [ $retry_count -eq $max_retries ]; then + echo "$GENERAL_NAT_NAME - $LINK_MODE 达到最大重试次数,无法修改" + exit 1 +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/transmission.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/transmission.sh new file mode 100644 index 000000000..2ef18aa87 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-link/transmission.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# NATMap +outter_ip=$1 +outter_port=$2 +inner_port=$4 +protocol=$5 + +LINK_TR_RPC_URL=$(echo $LINK_TR_RPC_URL | sed 's/\/$//') +url="$LINK_TR_RPC_URL/transmission/rpc" +# update port +trauth="-u $LINK_TR_USERNAME:$LINK_TR_PASSWORD" + +# 默认重试次数为1,休眠时间为3s +max_retries=1 +sleep_time=3 + +# 判断是否开启高级功能 +if [ "$LINK_ADVANCED_ENABLE" == 1 ] && [ -n "$LINK_ADVANCED_MAX_RETRIES" ] && [ -n "$LINK_ADVANCED_SLEEP_TIME" ]; then + # 获取最大重试次数 + max_retries=$((LINK_ADVANCED_MAX_RETRIES == "0" ? 1 : LINK_ADVANCED_MAX_RETRIES)) + # 获取休眠时间 + sleep_time=$((LINK_ADVANCED_SLEEP_TIME == "0" ? 3 : LINK_ADVANCED_SLEEP_TIME)) +fi + +# 初始化参数 +# # 获取trsid,直至重试次数用尽 +trsid="" +retry_count=0 + +for ((retry_count = 0; retry_count <= max_retries; retry_count++)); do + trsid=$(curl -s $trauth $url | sed 's/.*//g;s/<\/code>.*//g') + + # Check if the provided session ID contains the header "X-Transmission-Session-Id" + if [[ $trsid == *"X-Transmission-Session-Id"* ]]; then + echo "$GENERAL_NAT_NAME - $LINK_MODE 登录成功" + + # Modify the port using the Transmission API + tr_result=$(curl -s -X POST \ + -H "$trsid" $trauth \ + -d '{"method":"session-set","arguments":{"peer-port":'$outter_port'}}' \ + "$url") + + # Check if the port modification was successful + if [[ $(echo "$tr_result" | jq -r '.result') == "success" ]]; then + echo "transmission port modified successfully" + break + else + echo "transmission Failed to modify the port" + # Sleep for a specified amount of time + sleep $sleep_time + fi + else + # Sleep for a specified amount of time + sleep $sleep_time + fi + + # Sleep for a specified amount of time + sleep $sleep_time +done + +if [ $retry_count -eq $max_retries ]; then + echo "$GENERAL_NAT_NAME - $LINK_MODE 达到最大重试次数,无法修改" + exit 1 +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/gotify.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/gotify.sh new file mode 100644 index 000000000..6d46e8c35 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/gotify.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Define the Gotify URL, title, message, and priority +title="natmap - ${GENERAL_NAT_NAME} 更新" +message="$1" +gotify_url="${NOTIFY_GOTIFY_URL}" +priority="${NOTIFY_GOTIFY_PRIORITY:-5}" +token="${NOTIFY_GOTIFY_TOKEN}" + +# 默认重试次数为1,休眠时间为3s +max_retries=1 +sleep_time=3 + +# 判断是否开启高级功能 +if [ "${NOTIFY_ADVANCED_ENABLE}" == 1 ] && [ -n "$NOTIFY_ADVANCED_MAX_RETRIES" ] && [ -n "$NOTIFY_ADVANCED_SLEEP_TIME" ]; then + # 获取最大重试次数 + max_retries=$((NOTIFY_ADVANCED_MAX_RETRIES == "0" ? 1 : NOTIFY_ADVANCED_MAX_RETRIES)) + # 获取休眠时间 + sleep_time=$((NOTIFY_ADVANCED_SLEEP_TIME == "0" ? 3 : NOTIFY_ADVANCED_SLEEP_TIME)) +fi + +for ((retry_count = 1; retry_count <= max_retries; retry_count++)); do + # Send the message using curl + curl -s -X POST -H "Content-Type: multipart/form-data" -F "token=$token" -F "title=$title" -F "message=$message" -F "priority=$priority" "$gotify_url/message" + status=$? + if [ $status -eq 0 ]; then + echo "$GENERAL_NAT_NAME - $NOTIFY_MODE 发送成功" + break + else + # echo "$NOTIFY_MODE 登录失败,休眠$sleep_time秒" + sleep $sleep_time + fi +done + +# Check if maximum retries reached +if [ $retry_count -eq $max_retries ]; then + echo "$GENERAL_NAT_NAME - $NOTIFY_MODE 达到最大重试次数,无法通知" + break +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/pushplus.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/pushplus.sh new file mode 100755 index 000000000..38ffb7e65 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/pushplus.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +text="$1" +title="natmap - ${GENERAL_NAT_NAME} 更新" +token="${NOTIFY_PUSHPLUS_TOKEN}" + +# 默认重试次数为1,休眠时间为3s +max_retries=1 +sleep_time=3 + +# 判断是否开启高级功能 +if [ "${NOTIFY_ADVANCED_ENABLE}" == 1 ] && [ -n "$NOTIFY_ADVANCED_MAX_RETRIES" ] && [ -n "$NOTIFY_ADVANCED_SLEEP_TIME" ]; then + # 获取最大重试次数 + max_retries=$((NOTIFY_ADVANCED_MAX_RETRIES == "0" ? 1 : NOTIFY_ADVANCED_MAX_RETRIES)) + # 获取休眠时间 + sleep_time=$((NOTIFY_ADVANCED_SLEEP_TIME == "0" ? 3 : NOTIFY_ADVANCED_SLEEP_TIME)) +fi + +for ((retry_count = 1; retry_count <= max_retries; retry_count++)); do + curl -4 -Ss -X POST \ + -H 'Content-Type: application/json' \ + -d '{"token": "'"${token}"'", "content": "'"${text}"'", "title": "'"${title}"'"}' \ + "http://www.pushplus.plus/send" + status=$? + if [ $status -eq 0 ]; then + echo "$GENERAL_NAT_NAME - $NOTIFY_MODE 发送成功" + break + else + # echo "$NOTIFY_MODE 登录失败,休眠$sleep_time秒" + sleep $sleep_time + fi +done + +# Check if maximum retries reached +if [ $retry_count -eq $max_retries ]; then + echo "$GENERAL_NAT_NAME - $NOTIFY_MODE 达到最大重试次数,无法通知" + break +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/serverchan.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/serverchan.sh new file mode 100644 index 000000000..e6b39c1db --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/serverchan.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +title="natmap - ${GENERAL_NAT_NAME} 更新" +desp="$1" + +# 拼装post数据 +postdata="title=$title&desp=$desp" +message=( + "--header" "Content-type: application/x-www-form-urlencoded" + "--data" "$postdata" +) + +# 获取url +url="" +if [ "${NOTIFY_SERVERCHAN_ADVANCED_ENABLE}" == 1 ] && [ -n "$NOTIFY_SERVERCHAN_ADVANCED_URL" ]; then + url="$NOTIFY_SERVERCHAN_ADVANCED_URL/${NOTIFY_SERVERCHAN_SENDKEY}.send" +else + url="https://sctapi.ftqq.com/${NOTIFY_SERVERCHAN_SENDKEY}.send" +fi +# 默认重试次数为1,休眠时间为3s +max_retries=1 +sleep_time=3 + +# 判断是否开启高级功能 +if [ "${NOTIFY_ADVANCED_ENABLE}" == 1 ] && [ -n "$NOTIFY_ADVANCED_MAX_RETRIES" ] && [ -n "$NOTIFY_ADVANCED_SLEEP_TIME" ]; then + # 获取最大重试次数 + max_retries=$((NOTIFY_ADVANCED_MAX_RETRIES == "0" ? 1 : NOTIFY_ADVANCED_MAX_RETRIES)) + # 获取休眠时间 + sleep_time=$((NOTIFY_ADVANCED_SLEEP_TIME == "0" ? 3 : NOTIFY_ADVANCED_SLEEP_TIME)) +fi + +for ((retry_count = 1; retry_count <= max_retries; retry_count++)); do + result=$(curl -X POST -s -o /dev/null -w "%{http_code}" "$url" "${message[@]}") + if [ $result -eq 200 ]; then + echo "$GENERAL_NAT_NAME - $NOTIFY_MODE 发送成功" + break + else + # echo "$NOTIFY_MODE 登录失败,休眠$sleep_time秒" + sleep $sleep_time + fi +done + +# Check if maximum retries reached +if [ $retry_count -eq $max_retries ]; then + echo "$GENERAL_NAT_NAME - $NOTIFY_MODE 达到最大重试次数,无法通知" + break +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/telegram_bot.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/telegram_bot.sh new file mode 100755 index 000000000..faaf40978 --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/plugin-notify/telegram_bot.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +text="$1" +chat_id="${NOTIFY_TELEGRAM_BOT_CHAT_ID}" +token="${NOTIFY_TELEGRAM_BOT_TOKEN}" +title="natmap - ${GENERAL_NAT_NAME} 更新" + +function curl_proxy() { + if [ -z "$NOTIFY_TELEGRAM_BOT_PROXY" ]; then + curl "$@" + else + curl -x $NOTIFY_TELEGRAM_BOT_PROXY "$@" + fi +} + +# 默认重试次数为1,休眠时间为3s +max_retries=1 +sleep_time=3 + +# 判断是否开启高级功能 +if [ "${NOTIFY_ADVANCED_ENABLE}" == 1 ] && [ -n "$NOTIFY_ADVANCED_MAX_RETRIES" ] && [ -n "$NOTIFY_ADVANCED_SLEEP_TIME" ]; then + # 获取最大重试次数 + max_retries=$((NOTIFY_ADVANCED_MAX_RETRIES == "0" ? 1 : NOTIFY_ADVANCED_MAX_RETRIES)) + # 获取休眠时间 + sleep_time=$((NOTIFY_ADVANCED_SLEEP_TIME == "0" ? 3 : NOTIFY_ADVANCED_SLEEP_TIME)) +fi + +# # 判断是否开启高级功能 +# if [ "$NOTIFY_ADVANCED_ENABLE" == 1 ]; then +# # 获取最大重试次数 +# max_retries="${NOTIFY_ADVANCED_MAX_RETRIES%/:-$max_retries}" +# # 获取休眠时间 +# sleep_time="${NOTIFY_ADVANCED_SLEEP_TIME%/:-$sleep_time}" +# fi + +for ((retry_count = 1; retry_count <= max_retries; retry_count++)); do + curl_proxy -4 -Ss -o /dev/null -X POST \ + -H 'Content-Type: application/json' \ + -d '{"chat_id": "'"${chat_id}"'", "text": "'"${title}\n\n${text}"'", "parse_mode": "HTML", "disable_notification": "false"}' \ + "https://api.telegram.org/bot${token}/sendMessage" + status=$? + if [ $status -eq 0 ]; then + echo "$GENERAL_NAT_NAME - $NOTIFY_MODE 发送成功" + break + else + # echo "$NOTIFY_MODE 登录失败,休眠$sleep_time秒" + sleep $sleep_time + fi +done + +# Check if maximum retries reached +if [ $retry_count -eq $max_retries ]; then + echo "$GENERAL_NAT_NAME - $NOTIFY_MODE 达到最大重试次数,无法通知" + exit 1 +fi diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/update.sh b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/update.sh new file mode 100755 index 000000000..a8169fe5c --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/natmap/update.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +. /usr/share/libubox/jshn.sh + +( + json_init + json_add_string ip "$1" + json_add_int port "$2" + json_add_int inner_port "$4" + json_add_string protocol "$5" + json_add_string name "$GENERAL_NAT_NAME" + json_dump >/var/run/natmap/$PPID.json +) + +echo "natmap update json: $(cat /var/run/natmap/$PPID.json)" + +# link setting +[ "${LINK_ENABLE}" == 1 ] && source /usr/share/natmap/link.sh "$@" + +# forward setting +[ "${FORWARD_ENABLE}" == 1 ] && source /usr/share/natmap/forward.sh "$@" + +# notify setting +[ "${NOTIFY_ENABLE}" == 1 ] && source /usr/share/natmap/notify.sh "$@" + +# custom setting +[ "${CUSTOM_SCRIPT_ENABLE}" == 1 ] && [ -n "${CUSTOM_SCRIPT_PATH}" ] && { + export -n CUSTOM_SCRIPT_PATH + source "${CUSTOM_SCRIPT_PATH}" "$@" +} diff --git a/luci-app-natmap/luci-app-natmap/root/usr/share/rpcd/acl.d/luci-app-natmap.json b/luci-app-natmap/luci-app-natmap/root/usr/share/rpcd/acl.d/luci-app-natmap.json new file mode 100644 index 000000000..e961e269b --- /dev/null +++ b/luci-app-natmap/luci-app-natmap/root/usr/share/rpcd/acl.d/luci-app-natmap.json @@ -0,0 +1,17 @@ +{ + "luci-app-natmap": { + "description": "Grant access to LuCI app natmap", + "read": { + "file": { + "/var/run/natmap/*": ["read"] + }, + "ubus": { + "service": ["list"] + }, + "uci": ["natmap"] + }, + "write": { + "uci": ["natmap"] + } + } +} diff --git a/luci-app-natmap/natmap/Makefile b/luci-app-natmap/natmap/Makefile new file mode 100755 index 000000000..0257feca8 --- /dev/null +++ b/luci-app-natmap/natmap/Makefile @@ -0,0 +1,35 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=natmap +PKG_VERSION:=20240126 +PKG_RELEASE:=2 + +PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://github.com/heiher/natmap/releases/download/$(PKG_VERSION) +PKG_HASH:=7d8b3fe5c29dfe30271197b8ea1f78af292830483aefb1ccc21a3ec05f74627b + +PKG_MAINTAINER:=Richard Yu +PKG_LICENSE:=MIT +PKG_LICENSE_FILES:=License + +PKG_BUILD_FLAGS:=no-mips16 +PKG_BUILD_PARALLEL:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/natmap + SECTION:=net + CATEGORY:=Network + TITLE:=TCP/UDP port mapping tool for full cone NAT + URL:=https://github.com/heiher/natmap + DEPENDS:=+jq +curl +openssl-util +endef + +MAKE_FLAGS += REV_ID="$(PKG_VERSION)" + +define Package/${PKG_NAME}/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/bin/natmap $(1)/usr/bin/ +endef + +$(eval $(call BuildPackage,natmap)) diff --git a/luci-app-natmap/natmap/README.md b/luci-app-natmap/natmap/README.md new file mode 100644 index 000000000..f8aeb57f5 --- /dev/null +++ b/luci-app-natmap/natmap/README.md @@ -0,0 +1,69 @@ +### 新功能 + +1. 内置两种消息通知方式,Telegram Bot 和 PushPlus,可以为任何每一个打洞单独配置, PushPlus 支持推送到微信且不需要翻墙 +2. 内置多种配置模式,常用功能无需写脚本就能使用,详见内置模式 + +### 内置模式 + +#### qBittorrent + +打洞成功后,自动修改 qBittorrent 的端口号,并配置转发(可选) + +需要配置 qBittorrent 地址、账号、密码用于修改端口 +需要配置 qBittorrent 使用网卡的 IP 用于配置转发,端口填 0,会转发到修改后的端口 + +#### Transmission + +打洞成功后,自动修改 Transmission 的端口号,并配置转发(可选) + +需要配置 Transmission 地址、账号、密码用于修改端口 +需要配置 Transmission 使用网卡的 IP 用于配置转发,端口填 0,会转发到修改后的端口 + +#### Emby + +配合 Emby Connect 使用时,用户登录账号后,会从服务器获取最新的连接地址信息,此模式就是用于配置这些信息的 + +需要配置 Emby 地址和 API Key 用于修改连接地址信息 + +此模式必须配置转发 + +默认不更新「外部域」,如果有配置 DDNS,将 DDNS 域名填入外部域后将不需要再次修改 + +若没有域名,需要将 IP 填入外部域,可以勾选 「Update host with IP」 + +若对外提供的是 HTTPS 服务,需要勾选 「Update HTTPS Port」 + + +#### Cloudflare Origin Rules + +Cloudflare Origin Rules 可以设置回源端口,配合 DDNS 使用时,可以将 DDNS 域名指向 Cloudflare,然后将回源端口设置为打洞后的端口,这样就可以通过 Cloudflare 的 CDN 加速访问了 + +需要配置 Cloudflare 的 API Key,邮箱 和 Zone ID,Zone ID 可以在 Cloudflare 的域名首页找到 + +API Key 请访问 https://dash.cloudflare.com/profile/api-tokens 复制 Global API Key + +需要先在 Cloudflare 后台的 Rules - Origin Rules 下添加一个 Origin Rules,然后将 Origin Rules 的 Name 填入配置中 + +Name 请保持唯一,否则会出现奇怪的问题 + + +### 使用 + +添加软件源 + +``` +curl -fsSL https://github.com/ekkog/openwrt-dist/raw/master/add-feed.sh | sh +``` + +当前环境访问 GitHub 有问题时,可以使用 GitHub 镜像 + +``` +curl -fsSL https://ghproxy.com/https://github.com/EkkoG/openwrt-dist/blob/master/add-feed.sh | sh +``` + +更新软件源并安装 + +``` +opkg update +opkg install natmap +``` diff --git a/luci-app-natmap/patch.sh b/luci-app-natmap/patch.sh new file mode 100644 index 000000000..afdb890c3 --- /dev/null +++ b/luci-app-natmap/patch.sh @@ -0,0 +1,20 @@ +#!/bin/bash -e +function sed_wrapper() { + # if run in linux, use sed -i + # if run in macos, use sed -i '' + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + sed -i "$@" + elif [[ "$OSTYPE" == "darwin"* ]]; then + /usr/local/opt/gnu-sed/libexec/gnubin/sed -i "$@" + fi +} + +sed_wrapper '/luci.mk/ c\include $(TOPDIR)/feeds/luci/luci.mk' packages/luci-app-natmap/Makefile + +# remove the last \ +dep=${dep%\\} +if [[ $1 =~ '21.02'* ]]; then + dep="$dep +ip6tables-mod-nat +iptables-mod-extra +iptables-mod-tproxy" +else + dep="$dep +kmod-nft-tproxy" +fi diff --git a/luci-app-tinyfilemanager/LICENSE b/luci-app-tinyfilemanager/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/luci-app-tinyfilemanager/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/luci-app-tinyfilemanager/Makefile b/luci-app-tinyfilemanager/Makefile new file mode 100644 index 000000000..00f44ee5b --- /dev/null +++ b/luci-app-tinyfilemanager/Makefile @@ -0,0 +1,45 @@ +# Tiny File Manager by prasathmani +# Copyright (C) 2022-2023 muink +# +# This is free software, licensed under the GNU General Public License v3. +# See /LICENSE for more information. +# +include $(TOPDIR)/rules.mk + +LUCI_NAME:=luci-app-tinyfilemanager +PKG_VERSION:=2.5.3-20231119 +#PKG_RELEASE:=1 + +LUCI_TITLE:=LuCI Tiny File Manager +LUCI_DEPENDS:=+php8 +php8-cgi +php8-fastcgi +php8-fpm +php8-mod-session +php8-mod-ctype +php8-mod-fileinfo +php8-mod-zip +php8-mod-iconv +php8-mod-mbstring +coreutils-stat +zoneinfo-asia +bash +curl +tar + +LUCI_DESCRIPTION:=A Web based File Manager in PHP + +define Package/$(LUCI_NAME)/conffiles +/etc/config/tinyfilemanager +endef + +define Package/$(LUCI_NAME)/postinst +#!/bin/sh +mkdir -p "$${IPKG_INSTROOT}/www/tinyfilemanager" 2>/dev/null +[ ! -d "$${IPKG_INSTROOT}/www/tinyfilemanager/rootfs" ] && ln -s / "$${IPKG_INSTROOT}/www/tinyfilemanager/rootfs" +total_size_limit=5G #post_max_size = 8M +single_size_limit=2G #upload_max_filesize = 2M +otime_uploads_limit=200 #max_file_uploads = 20 +sed -Ei "s|^(post_max_size) *=.*$$|\1 = $$total_size_limit|; \ + s|^(upload_max_filesize) *=.*$$|\1 = $$single_size_limit|; \ + s|^(max_file_uploads) *=.*$$|\1 = $$otime_uploads_limit|" \ +"$${IPKG_INSTROOT}/etc/php.ini" +# unpack +tar -C "$${IPKG_INSTROOT}/www/tinyfilemanager" -xzf "$${IPKG_INSTROOT}/www/tinyfilemanager/index.tgz" +rm -f "$${IPKG_INSTROOT}/www/tinyfilemanager/index.tgz" +endef + +define Package/$(LUCI_NAME)/prerm +#!/bin/sh +if [ -d /www/tinyfilemanager ]; then rm -rf /www/tinyfilemanager; fi +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-tinyfilemanager/README.md b/luci-app-tinyfilemanager/README.md new file mode 100644 index 000000000..c5af1ff7f --- /dev/null +++ b/luci-app-tinyfilemanager/README.md @@ -0,0 +1,85 @@ +# LuCI Tiny File Manager +> [TinyFileManager][] is web based file manager and it is a simple, fast and small file manager with a single file, multi-language ready web application for storing, uploading, editing and managing files and folders online via web browser. The Application runs on PHP 5.5+, It allows the creation of multiple users and each user can have its own directory and a build-in support for managing text files with cloud9 IDE and it supports syntax highlighting for over 150+ languages and over 35+ themes. + +### Screenshots + +![demo](example/demo.png "demo") + +
Real installation + + + + +
+ +### How to install + +1. Goto ~~[releases](https://github.com/muink/luci-app-tinyfilemanager/tree/releases)~~ [here](https://fantastic-packages.github.io/packages/) +2. Download the latest version of ipk +3. Login router and goto **System --> Software** +4. Upload and install ipk +5. Reboot if the app is not automatically added in page +6. Goto **NAS --> Tiny File Manager** +7. Default username/password: admin/admin and user/12345. + +### Uploading limit + +**If you need to change the upload limit for Tiny File Manager** + +Edit [config.js](htdocs/luci-static/resources/view/tinyfilemanager/config.js) before build +Edit `/www/luci-static/resources/view/tinyfilemanager/config.js` in router +```javascript +o = s.option(form.Value, 'max_upload_size', _('Max upload size (MBytes)')); +o.datatype = "and(uinteger,max(2048))"; //limit to 2048MB +``` +And edit [Makefile](Makefile) before build +```makefile +total_size_limit=?? #Total size of multiple files +single_size_limit=?? #Max single file size +otime_uploads_limit=?? #Max count of simultaneous uploads +``` +And edit `/etc/php.ini` in router +```ini +post_max_size = ?? ;Total size of multiple files +upload_max_filesize = ?? ;Max single file size +max_file_uploads = ?? ;Max count of simultaneous uploads +``` + +### Build + +- Compile from OpenWrt/LEDE SDK + +``` +# Take the x86_64 platform as an example +tar xjf openwrt-sdk-21.02.3-x86-64_gcc-8.4.0_musl.Linux-x86_64.tar.xz +# Go to the SDK root dir +cd OpenWrt-sdk-*-x86_64_* +# First run to generate a .config file +make menuconfig +./scripts/feeds update -a +./scripts/feeds install -a +# Get Makefile +git clone --depth 1 --branch master --single-branch --no-checkout https://github.com/muink/luci-app-tinyfilemanager.git package/luci-app-tinyfilemanager +pushd package/luci-app-tinyfilemanager +umask 022 +git checkout +popd +# Select the package LuCI -> Applications -> luci-app-tinyfilemanager +make menuconfig +# Upgrade to new version Tiny File Manager (optional) +1. modify the tag VERSION='2.4.7' to new version in makenew.sh +2. run makenew.sh to upgrade current version (if it worked) +# Start compiling +make package/luci-app-tinyfilemanager/compile V=99 +``` + +### Contributors + +- [prasathmani](https://tinyfilemanager.github.io) +- [muink](https://github.com/muink) + +[TinyFileManager]: https://github.com/prasathmani/tinyfilemanager + +### License + +- This project is licensed under the [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.html) diff --git a/luci-app-tinyfilemanager/example/demo.png b/luci-app-tinyfilemanager/example/demo.png new file mode 100644 index 000000000..a1466950f Binary files /dev/null and b/luci-app-tinyfilemanager/example/demo.png differ diff --git a/luci-app-tinyfilemanager/example/localfeeds.png b/luci-app-tinyfilemanager/example/localfeeds.png new file mode 100644 index 000000000..a1eaa9db8 Binary files /dev/null and b/luci-app-tinyfilemanager/example/localfeeds.png differ diff --git a/luci-app-tinyfilemanager/example/pictute.png b/luci-app-tinyfilemanager/example/pictute.png new file mode 100644 index 000000000..dcfd4f4f4 Binary files /dev/null and b/luci-app-tinyfilemanager/example/pictute.png differ diff --git a/luci-app-tinyfilemanager/example/root.png b/luci-app-tinyfilemanager/example/root.png new file mode 100644 index 000000000..83c953c5c Binary files /dev/null and b/luci-app-tinyfilemanager/example/root.png differ diff --git a/luci-app-tinyfilemanager/example/video.png b/luci-app-tinyfilemanager/example/video.png new file mode 100644 index 000000000..81b6fe5a3 Binary files /dev/null and b/luci-app-tinyfilemanager/example/video.png differ diff --git a/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js b/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js new file mode 100644 index 000000000..8bfa096cf --- /dev/null +++ b/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js @@ -0,0 +1,198 @@ +'use strict'; +'require view'; +'require fs'; +'require uci'; +'require ui'; +'require form'; + +return view.extend({ +// handleSaveApply: null, +// handleSave: null, +// handleReset: null, + + load: function() { + return Promise.all([ + L.resolveDefault(fs.read('/var/tinyfilemanager/releaseslist'), null), + L.resolveDefault(fs.stat('/usr/libexec/tinyfilemanager-update'), {}), + L.resolveDefault(fs.stat('/etc/nginx/conf.d/php.locations'), {}), + uci.load('tinyfilemanager'), + ]); + }, + + render: function(res) { + var releaseslist = res[0] ? res[0].trim().split("\n") : [], + has_location = res[2].path, + pkgversion = '2.5.3'; + + var m, s, o; + + m = new form.Map('tinyfilemanager'); + + s = m.section(form.TypedSection, 'main'); + s.anonymous = true; + + o = s.option(form.Button, '_reload', _('Reload') + ' tinyfilemanager'); + o.inputtitle = _('Reload'); + o.inputstyle = 'apply'; + o.onclick = function() { + return fs.exec('/etc/init.d/tinyfilemanager', ['reload']) + .catch(function(e) { ui.addNotification(null, E('p', e.message), 'error') }); + }; + if (! has_location) + o.description = _('To enable SSL support, you may need to install %s
').format(['php-nginx']); + + o = s.option(form.Flag, 'use_auth', _('Enable Authentication')); + o.rmempty = false; + + o = s.option(form.DynamicList, 'auth_users', _('Login user name and passwd hash'), + _('You can generate new passwd in File Manager -> Admin -> Help -> Generate new or Here.').format('https://tinyfilemanager.github.io/docs/pwd.html')); + + o.datatype = "list(string)"; + o.placeholder = 'user:$2y$10$cFk8K5VQJr...'; + o.default = 'admin:$2y$10$BewzfQXrlnUihprEgGt7ROMB9NigZcZkkwssIRYznF9fwMuObIZoa'; + o.rmempty = false; + o.retain = true; + o.depends('use_auth', '1'); + + o = s.option(form.DynamicList, 'readonly_users', _('Readonly users')); + o.datatype = "list(string)"; + o.placeholder = 'user'; + o.default = 'user'; + o.rmempty = false; + o.retain = true; + o.depends('use_auth', '1'); + + o = s.option(form.Flag, 'global_readonly', _('Global Readonly')); + o.default = o.disabled; + o.rmempty = false; + + o = s.option(form.Value, 'root_path', _('Home path')); + o.datatype = 'directory'; + o.placeholder = '/var'; + o.optional = true; + o.rmempty = true; + + o = s.option(form.ListValue, 'date_format', _('Date format')); + o.value('d.m.o', _('DD.MM.YYYY')); + o.value('d-m-o', _('DD-MM-YYYY')); + o.value('d/m/o', _('DD/MM/YYYY')); + o.value('j.n.o', _('D.M.YYYY')); + o.value('j-n-o', _('D-M-YYYY')); + o.value('j/n/o', _('D/M/YYYY')); + o.value('o.m.d', _('YYYY.MM.DD')); + o.value('o-m-d', _('YYYY-MM-DD')); + o.value('o/m/d', _('YYYY/MM/DD')); + o.value('o.n.j', _('YYYY.M.D')); + o.value('o-n-j', _('YYYY-M-D')); + o.value('o/n/j', _('YYYY/M/D')); + o.default = 'd.m.o'; + o.rmempty = false; + + o = s.option(form.ListValue, 'time_format', _('Time format')); + o.value('H:i:s', _('HH:mm:ss')); + o.value('G:i:s', _('H:mm:ss')); + o.value('A h:i:s', _('TT hh:mm:ss')); + o.value('A g:i:s', _('TT h:mm:ss')); + o.value('h:i:s A', _('hh:mm:ss TT')); + o.value('g:i:s A', _('h:mm:ss TT')); + o.default = 'H:i:s'; + o.rmempty = false; + + o = s.option(form.Flag, 'show_second', _('Show seconds in time')); + o.default = o.disabled; + o.rmempty = false; + + o = s.option(form.Value, 'favicon_path', _('Favicon path')); + o.datatype = 'file'; + o.placeholder = '/etc/tinyfilemanager/favicon.png'; + o.optional = true; + o.rmempty = false; + o.retain = true; + + o = s.option(form.DynamicList, 'exclude_items', _('Exclude Files/Folders')); + o.datatype = "list(string)"; + o.optional = true; + o.rmempty = false; + o.retain = true; + + o = s.option(form.ListValue, 'online_viewer', _('Online Docs viewer'), + _('Requires running on open network')); + o.value('0', _('Disable')); + o.value('google', _('Google Docs')); + o.value('microsoft', _('Microsoft Web Apps')); + o.default = '0'; + o.rmempty = false; + + o = s.option(form.Value, 'max_upload_size', _('Max upload size (MBytes)')); + o.datatype = "and(uinteger,max(2048))"; + o.placeholder = '3'; + o.default = '25'; + o.rmempty = false; + + o = s.option(form.Flag, 'proxy_enabled', _('Enable proxy for updater')); + o.rmempty = true; + + o = s.option(form.ListValue, 'proxy_protocol', _('Proxy Protocol')); + o.value('http', 'HTTP'); + o.value('https', 'HTTPS'); + o.value('socks5', 'SOCKS5'); + o.value('socks5h', 'SOCKS5H'); + o.default = 'socks5'; + o.rmempty = false; + o.retain = true; + o.depends('proxy_enabled', '1'); + + o = s.option(form.Value, 'proxy_server', _('Proxy Server')); + o.datatype = "ipaddrport(1)"; + o.placeholder = '192.168.1.10:1080'; + o.rmempty = false; + o.retain = true; + o.depends('proxy_enabled', '1'); + + o = s.option(form.Button, '_check_update', _('Check update')); + o.inputtitle = _('Check update'); + o.inputstyle = 'apply'; + o.onclick = function() { + return fs.exec('/etc/init.d/tinyfilemanager', ['check']) + .then(function(res) { return window.location.reload() }) + .catch(function(e) { ui.addNotification(null, E('p', e.message), 'error') }); + }; + + if (releaseslist.length) { + o = s.option(form.ListValue, '_releaseslist', _('Releases list')); + //o.value(pkgversion); + o.default = pkgversion; + for (var i = 0; i < releaseslist.length; i++) + o.value(releaseslist[i]); + o.write = function() {}; + + o = s.option(form.Button, '_uprgade', _('Uprgade ') + _('Tiny File Manager')); + o.inputtitle = _('Uprgade'); + o.inputstyle = 'apply'; + o.onclick = L.bind(function(ev, section_id) { + var releasestag=document.getElementById('widget.' + this.cbid(section_id).match(/.+\./) + '_releaseslist').value; + //alert(releasestag); + return fs.exec_direct('/usr/libexec/tinyfilemanager-update', [releasestag]) + .catch(function(e) { ui.addNotification(null, E('p', e.message), 'error') }); + }, o) + }; + +// s = m.section(form.TypedSection, '_updater'); +// s.render = L.bind(function(view, section_id) { +// return E('div',{ 'class': 'cbi-section' }, [ +// E('button', { +// 'class': 'cbi-button cbi-button-action', +// 'click': ui.createHandlerFn(view, 'handleQueryVendor') +// }, _('Check update')), +// +// E('select', { 'class': 'cbi-input-select' }, [ +// E('option', { 'value': '2.4.7' }, '2.4.7'), +// E('option', { 'value': '2.4.3' }, '2.4.3'), +// E('option', { 'value': '2.4.1' }, '2.4.1') +// ]) +// ]); +// }, o, this); + + return m.render(); + } +}); diff --git a/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/main.js b/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/main.js new file mode 100644 index 000000000..fdc9ba28a --- /dev/null +++ b/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/main.js @@ -0,0 +1,18 @@ +'use strict'; +'require view'; + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + }, + + render: function() { + return E('iframe', { + src: window.location.protocol + '//' + window.location.hostname + '/tinyfilemanager/', + style: 'width: 100%; min-height: 100vh; border: none; border-radius: 3px;' + }); + } +}); diff --git a/luci-app-tinyfilemanager/htdocs/tinyfilemanager/index.tgz b/luci-app-tinyfilemanager/htdocs/tinyfilemanager/index.tgz new file mode 100644 index 000000000..d2b8626a7 Binary files /dev/null and b/luci-app-tinyfilemanager/htdocs/tinyfilemanager/index.tgz differ diff --git a/luci-app-tinyfilemanager/makenew.sh b/luci-app-tinyfilemanager/makenew.sh new file mode 100755 index 000000000..10ff35bfc --- /dev/null +++ b/luci-app-tinyfilemanager/makenew.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# dependent: curl tar +# +# LuCI Tiny File Manager +# Author: muink +# Github: https://github.com/muink/luci-app-tinyfilemanager +# + +# PKGInfo +REPOURL='https://github.com/prasathmani/tinyfilemanager' +PKGNAME='tinyfilemanager' +VERSION='2.5.3' +# +PKG_DIR=$PKGNAME-$VERSION +REF_DIR="assets" +# +INDEXPHP="tinyfilemanager.php" +#CFGSAMPl="config-sample.php" +LANGFILE="translation.json" + + +PROJDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # <-- +WORKDIR="$PROJDIR/htdocs/$PKGNAME" # <-- +mkdir -p "$WORKDIR" 2>/dev/null +cd $WORKDIR + + + +# Clear Old version +rm -rf * + +# Download Repository +curl -L ${REPOURL}/archive/refs/tags/${VERSION}.tar.gz | tar -xvz -C "$WORKDIR" + +# Check offline ? +[ -n "$(sed -En "/^\\\$external = array\(/,/^\);/{s,^(.+=\")(http(s)?://.+/)([^/]+\.(css|js))(\".+),\4,p}" "$PKG_DIR/$INDEXPHP")" ] && { + +# Preprocessing +sed -Ei "//dev/null +refurl=($(sed -En "/^\\\$external /,/^\);/{s,^.+=\"(http(s)?://.+\.(css|js))\".+,\1, p}" "$PKG_DIR/$INDEXPHP" | sort -u )) +ref= +url= +out= +type= + +for _i in $(seq 0 1 $[ ${#refurl[@]} -1 ]); do + eval "url=${refurl[$_i]}" + out=${url##*/} + type=${url##*.} + + curl -Lo $out $url + mkdir -p "$REF_DIR/$type" 2>/dev/null + mv --backup $out "$REF_DIR/$type/" +done + +ref=$(for _p in $(find * -type f ! -path "$PKG_DIR/*"); do \ + sed -E "s/(,|;)/\1\n/g" $_p | grep -E "\burl\([^\)]+\)" | grep -Ev "\burl\(\"data:image" >/dev/null; \ + [ "$?" == "0" ] && echo $_p; \ + done) + +for _i in $ref; do + suburl=($(sed -E "s/(,|;)/\1\n/g" $_i | grep -E "\burl\([^\)]+\)" | grep -Ev "\burl\(\"data:image" | sed -En "s|^[^']+'([^']+)'.+|\1| p")) + hosturl=$(for _ in "${refurl[@]}"; do echo "$_" | grep "${_i##*/}"; done) + + for _j in $(seq 0 1 $[ ${#suburl[@]} -1 ]); do + url="${suburl[$_j]}" + out=${url%%\?*} + type=${hosturl##*.} + + mkdir -p "$REF_DIR/$type/${out%/*}" 2>/dev/null + curl -Lo ${out##*/} "${hosturl%/*}/$url" + mv -f ${out##*/} "$REF_DIR/$type/$out" + done +done + +# Post-processing +sed -i "s|\$__highlightjs_style|' . \$highlightjs_style . '|" "$PKG_DIR/$INDEXPHP" + +# Hotfix + +# Migrating to Local Reference +sed -Ei "s,^(.+=\")(http(s)?://.+/)([^/]+\.(css|js))(\".+),\1$REF_DIR/\5/\4\6," "$PKG_DIR/$INDEXPHP" + +} + +# FixED +sed -Ei "/^if \(\\\$use_auth\) \{/,/^}/{/\/\/ Logging In/,/\/\/ Form/{s|(fm_redirect\().+|\1FM_SELF_URL);|g}}" "$PKG_DIR/$INDEXPHP" + +# Clean up and Done +[ -d "$PKG_DIR/$REF_DIR" ] && cp -rf "$PKG_DIR/$REF_DIR" . +mv -f "$PKG_DIR/$INDEXPHP" ./index.php +#mv -f "$PKG_DIR/$CFGSAMPl" . +mv -f "$PKG_DIR/$LANGFILE" . +rm -rf "$PKG_DIR" + +# Package +sed -Ei "/^VERSION=/{s|(VERSION:=)[^\}]*|\1$VERSION|}" "$PROJDIR/root/usr/libexec/tinyfilemanager-update" +sed -Ei "s|(VERSION=).*|\1'$VERSION'|" "$PROJDIR/root/etc/init.d/tinyfilemanager" +sed -Ei "s|(pkgversion =).*|\1 '$VERSION';|" "$PROJDIR/htdocs/luci-static/resources/view/tinyfilemanager/config.js" +sed -Ei "s|(PKG_VERSION:=)[^-]+|\1$VERSION|" "$PROJDIR/Makefile" +tar -czvf index.tgz * --owner=0 --group=0 --no-same-owner --no-same-permissions --remove-files diff --git a/luci-app-tinyfilemanager/po/templates/tinyfilemanager.pot b/luci-app-tinyfilemanager/po/templates/tinyfilemanager.pot new file mode 100644 index 000000000..620db2fb1 --- /dev/null +++ b/luci-app-tinyfilemanager/po/templates/tinyfilemanager.pot @@ -0,0 +1,197 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:150 +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:151 +msgid "Check update" +msgstr "" + +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:30 +msgid "Config" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:80 +msgid "D-M-YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:79 +msgid "D.M.YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:81 +msgid "D/M/YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:77 +msgid "DD-MM-YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:76 +msgid "DD.MM.YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:78 +msgid "DD/MM/YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:75 +msgid "Date format" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:118 +msgid "Disable" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:44 +msgid "Enable Authentication" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:130 +msgid "Enable proxy for updater" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:111 +msgid "Exclude Files/Folders" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:105 +msgid "Favicon path" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:65 +msgid "Global Readonly" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:119 +msgid "Google Docs" +msgstr "" + +#: applications/luci-app-tinyfilemanager/root/usr/share/rpcd/acl.d/luci-app-tinyfilemanager.json:3 +msgid "Grant access to tinyfilemanager procedures" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:93 +msgid "H:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:92 +msgid "HH:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:69 +msgid "Home path" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:47 +msgid "Login user name and passwd hash" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:124 +msgid "Max upload size (MBytes)" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:120 +msgid "Microsoft Web Apps" +msgstr "" + +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:3 +msgid "NAS" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:116 +msgid "Online Docs viewer" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:133 +msgid "Proxy Protocol" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:143 +msgid "Proxy Server" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:57 +msgid "Readonly users" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:163 +msgid "Releases list" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:34 +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:35 +msgid "Reload" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:117 +msgid "Requires running on open network" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:101 +msgid "Show seconds in time" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:95 +msgid "TT h:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:94 +msgid "TT hh:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:91 +msgid "Time format" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:170 +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:11 +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:22 +msgid "Tiny File Manager" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:42 +msgid "To enable SSL support, you may need to install %s
" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:170 +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:171 +msgid "Uprgade" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:86 +msgid "YYYY-M-D" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:83 +msgid "YYYY-MM-DD" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:85 +msgid "YYYY.M.D" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:82 +msgid "YYYY.MM.DD" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:87 +msgid "YYYY/M/D" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:84 +msgid "YYYY/MM/DD" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:48 +msgid "" +"You can generate new passwd in File Manager -> Admin -> Help -> Generate " +"new or Here." +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:97 +msgid "h:mm:ss TT" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:96 +msgid "hh:mm:ss TT" +msgstr "" diff --git a/luci-app-tinyfilemanager/po/zh-cn b/luci-app-tinyfilemanager/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-tinyfilemanager/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-tinyfilemanager/po/zh_Hans/tinyfilemanager.po b/luci-app-tinyfilemanager/po/zh_Hans/tinyfilemanager.po new file mode 100644 index 000000000..f1ccc22bd --- /dev/null +++ b/luci-app-tinyfilemanager/po/zh_Hans/tinyfilemanager.po @@ -0,0 +1,210 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"PO-Revision-Date: 2023-03-26 21:32+0100\n" +"Last-Translator: muink \n" +"Language-Team: Chinese (Simplified) \n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.12.1-dev\n" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:150 +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:151 +msgid "Check update" +msgstr "检查更新" + +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:30 +msgid "Config" +msgstr "配置" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:80 +msgid "D-M-YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:79 +msgid "D.M.YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:81 +msgid "D/M/YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:77 +msgid "DD-MM-YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:76 +msgid "DD.MM.YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:78 +msgid "DD/MM/YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:75 +msgid "Date format" +msgstr "日期格式" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:118 +msgid "Disable" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:44 +msgid "Enable Authentication" +msgstr "启用用户验证" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:130 +msgid "Enable proxy for updater" +msgstr "为更新器启用代理" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:111 +msgid "Exclude Files/Folders" +msgstr "排除 文件/目录" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:105 +msgid "Favicon path" +msgstr "Favicon 路径" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:65 +msgid "Global Readonly" +msgstr "全局只读" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:119 +msgid "Google Docs" +msgstr "" + +#: applications/luci-app-tinyfilemanager/root/usr/share/rpcd/acl.d/luci-app-tinyfilemanager.json:3 +msgid "Grant access to tinyfilemanager procedures" +msgstr "授予访问 LuCI 应用 tinyfilemanager 的权限" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:93 +msgid "H:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:92 +msgid "HH:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:69 +msgid "Home path" +msgstr "家目录" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:47 +msgid "Login user name and passwd hash" +msgstr "登入用户名和密码哈希" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:124 +msgid "Max upload size (MBytes)" +msgstr "上传大小限制 (MBytes)" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:120 +msgid "Microsoft Web Apps" +msgstr "" + +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:3 +msgid "NAS" +msgstr "存储" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:116 +msgid "Online Docs viewer" +msgstr "在线文档预览器" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:133 +msgid "Proxy Protocol" +msgstr "代理协议" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:143 +msgid "Proxy Server" +msgstr "代理服务器" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:57 +msgid "Readonly users" +msgstr "只读用户组" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:163 +msgid "Releases list" +msgstr "发布版列表" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:34 +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:35 +msgid "Reload" +msgstr "重载" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:117 +msgid "Requires running on open network" +msgstr "需要运行在开放网络" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:101 +msgid "Show seconds in time" +msgstr "显示秒" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:95 +msgid "TT h:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:94 +msgid "TT hh:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:91 +msgid "Time format" +msgstr "时间格式" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:170 +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:11 +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:22 +msgid "Tiny File Manager" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:42 +msgid "To enable SSL support, you may need to install %s
" +msgstr "要启用 SSL 支持可能需先安装 %s
" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:170 +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:171 +msgid "Uprgade" +msgstr "升级" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:86 +msgid "YYYY-M-D" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:83 +msgid "YYYY-MM-DD" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:85 +msgid "YYYY.M.D" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:82 +msgid "YYYY.MM.DD" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:87 +msgid "YYYY/M/D" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:84 +msgid "YYYY/MM/DD" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:48 +msgid "" +"You can generate new passwd in File Manager -> Admin -> Help -> Generate " +"new or Here." +msgstr "" +"你可以在 文件管理器 -> Admin -> 帮助 -> 生成新的 or " +"这里。" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:97 +msgid "h:mm:ss TT" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:96 +msgid "hh:mm:ss TT" +msgstr "" diff --git a/luci-app-tinyfilemanager/po/zh_Hant/tinyfilemanager.po b/luci-app-tinyfilemanager/po/zh_Hant/tinyfilemanager.po new file mode 100644 index 000000000..fb2986485 --- /dev/null +++ b/luci-app-tinyfilemanager/po/zh_Hant/tinyfilemanager.po @@ -0,0 +1,210 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"PO-Revision-Date: 2023-03-26 21:32+0100\n" +"Last-Translator: muink \n" +"Language-Team: Chinese (Traditional) \n" +"Language: zh_Hant\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.12.1-dev\n" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:150 +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:151 +msgid "Check update" +msgstr "檢查更新" + +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:30 +msgid "Config" +msgstr "組態" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:80 +msgid "D-M-YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:79 +msgid "D.M.YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:81 +msgid "D/M/YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:77 +msgid "DD-MM-YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:76 +msgid "DD.MM.YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:78 +msgid "DD/MM/YYYY" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:75 +msgid "Date format" +msgstr "日期格式" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:118 +msgid "Disable" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:44 +msgid "Enable Authentication" +msgstr "啟用用戶認證" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:130 +msgid "Enable proxy for updater" +msgstr "為更新器啟用代理" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:111 +msgid "Exclude Files/Folders" +msgstr "排除 文件/目錄" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:105 +msgid "Favicon path" +msgstr "Favicon 路徑" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:65 +msgid "Global Readonly" +msgstr "全局只讀" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:119 +msgid "Google Docs" +msgstr "" + +#: applications/luci-app-tinyfilemanager/root/usr/share/rpcd/acl.d/luci-app-tinyfilemanager.json:3 +msgid "Grant access to tinyfilemanager procedures" +msgstr "授予訪問 LuCI 應用 tinyfilemanager 的權限" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:93 +msgid "H:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:92 +msgid "HH:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:69 +msgid "Home path" +msgstr "家目錄" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:47 +msgid "Login user name and passwd hash" +msgstr "登入用戶名和密碼雜湊" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:124 +msgid "Max upload size (MBytes)" +msgstr "上傳大小限制 (MBytes)" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:120 +msgid "Microsoft Web Apps" +msgstr "" + +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:3 +msgid "NAS" +msgstr "存儲" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:116 +msgid "Online Docs viewer" +msgstr "在線文檔預覽器" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:133 +msgid "Proxy Protocol" +msgstr "代理協議" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:143 +msgid "Proxy Server" +msgstr "代理伺服器" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:57 +msgid "Readonly users" +msgstr "只讀用戶組" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:163 +msgid "Releases list" +msgstr "發佈版本列表" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:34 +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:35 +msgid "Reload" +msgstr "重載" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:117 +msgid "Requires running on open network" +msgstr "需要運行在開放網路" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:101 +msgid "Show seconds in time" +msgstr "顯示秒" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:95 +msgid "TT h:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:94 +msgid "TT hh:mm:ss" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:91 +msgid "Time format" +msgstr "時間格式" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:170 +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:11 +#: applications/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:22 +msgid "Tiny File Manager" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:42 +msgid "To enable SSL support, you may need to install %s
" +msgstr "要啟用 SSL 支援可能需先安裝 %s
" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:170 +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:171 +msgid "Uprgade" +msgstr "升級" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:86 +msgid "YYYY-M-D" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:83 +msgid "YYYY-MM-DD" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:85 +msgid "YYYY.M.D" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:82 +msgid "YYYY.MM.DD" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:87 +msgid "YYYY/M/D" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:84 +msgid "YYYY/MM/DD" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:48 +msgid "" +"You can generate new passwd in File Manager -> Admin -> Help -> Generate " +"new or Here." +msgstr "" +"你可以在 檔案管理器 -> Admin -> 幫助 -> 建立新的 or " +"這裡。" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:97 +msgid "h:mm:ss TT" +msgstr "" + +#: applications/luci-app-tinyfilemanager/htdocs/luci-static/resources/view/tinyfilemanager/config.js:96 +msgid "hh:mm:ss TT" +msgstr "" diff --git a/luci-app-tinyfilemanager/root/etc/config/tinyfilemanager b/luci-app-tinyfilemanager/root/etc/config/tinyfilemanager new file mode 100644 index 000000000..1e0fc4418 --- /dev/null +++ b/luci-app-tinyfilemanager/root/etc/config/tinyfilemanager @@ -0,0 +1,12 @@ + +config main + option use_auth '1' + list auth_users 'admin:$2y$10$BewzfQXrlnUihprEgGt7ROMB9NigZcZkkwssIRYznF9fwMuObIZoa' + list auth_users 'user:$2y$10$Fg6Dz8oH9fPoZ2jJan5tZuv6Z4Kp7avtQ9bDfrdRntXtPeiMAZyGO' + list readonly_users 'user' + option date_format 'd.m.o' + option time_format 'H:i:s' + option show_second '0' + option favicon_path '/etc/tinyfilemanager/favicon.png' + option online_viewer '0' + diff --git a/luci-app-tinyfilemanager/root/etc/init.d/tinyfilemanager b/luci-app-tinyfilemanager/root/etc/init.d/tinyfilemanager new file mode 100755 index 000000000..c4446e8e8 --- /dev/null +++ b/luci-app-tinyfilemanager/root/etc/init.d/tinyfilemanager @@ -0,0 +1,118 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2022-2023 muink + +. "${IPKG_INSTROOT}/lib/functions.sh" + +START=99 +USE_PROCD=1 +VERSION='2.5.3' + +EXTRA_COMMANDS="check" +EXTRA_HELP=\ +" check Check for version updates" + +CONFIG_NAME='tinyfilemanager' +TYPEDSECTION='main' + +WORKDIR="/www/tinyfilemanager" +VARDIR="/var/tinyfilemanager" +CONF="/var/etc/tinyfilemanager.conf" + +HOMEPATH="tinyfilemanager/rootfs" + +if [ "$(uci -q get $CONFIG_NAME.@$TYPEDSECTION[0].proxy_enabled)" == "1" ]; then + export ALL_PROXY=$(uci -q get $CONFIG_NAME.@$TYPEDSECTION[0].proxy_protocol)://$(uci -q get $CONFIG_NAME.@$TYPEDSECTION[0].proxy_server) +fi + + + +validate_section() { + uci_load_validate $CONFIG_NAME $TYPEDSECTION "$1" "$2" \ + 'use_auth:bool:0' \ + 'auth_users:list(string)' \ + 'readonly_users:list(string)' \ + 'global_readonly:bool:0' \ + 'root_path:directory' \ + 'date_format:or("d.m.o", "d-m-o", "d/m/o", "j.n.o", "j-n-o", "j/n/o", "o.m.d", "o-m-d", "o/m/d", "o.n.j", "o-n-j", "o/n/j"):d.m.o' \ + 'time_format:or("H\:i\:s", "G\:i\:s", "A h\:i\:s", "A g\:i\:s", "h\:i\:s A", "g\:i\:s A"):H\:i\:s' \ + 'show_second:bool:0' \ + 'favicon_path:file' \ + 'exclude_items:list(string)' \ + 'online_viewer:or("0", "google", "microsoft"):0' \ + 'max_upload_size:and(uinteger,max(2048))' +} + +init_config() { + sed -n '/\/\/ --- EDIT BELOW CONFIGURATION CAREFULLY ---/,/\/\/ --- EDIT BELOW CAREFULLY OR DO NOT EDIT AT ALL ---/{ \ + s|// --- EDIT BELOW CONFIGURATION CAREFULLY ---|\|; \ + /\/\/ if User has the external config file, try to use it to override the default config above/,/^}/d; \ + p \ + }' "$WORKDIR/index.php" > "$1" + + sed -Ei "s|(^\\\$root_path =)(.+)|\1 \\\$_SERVER['DOCUMENT_ROOT'].'/$HOMEPATH';|; \ + s|(^\\\$root_url =)(.+)|\1 '$HOMEPATH';|; \ + + s|(^\\\$default_timezone =)(.+)|\1 '$(uci -q get system.@system[0].zonename)';| \ + " "$1" +} + +apply_config() { + [ "$2" == "0" ] || { >&2 echo "section $1 validation failed"; return 1; } + + local _conf="$CONF" + + local auth_users=$(echo "$auth_users"|sed -E "s/('[^']+'|[^' ]+)/'\1'/g; s|:|' => '|g; s|''|'|g; s|' '|',\\\n '|g; s|^| |") + local readonly_users=$(echo "$readonly_users"|sed -E "s/('[^']+'|[^' ]+)/'\1'/g; s|''|'|g; s|' '|',\\\n '|g; s|^| |") + + local exclude_items=$(echo "$exclude_items"|sed -E "s/('[^']+'|[^' ]+)/'\1'/g; s|''|'|g; s|' '|',\\\n '|g; s|^| |") + +[ "$use_auth" -eq "1" ] && sed -Ei "s|(^\\\$use_auth =)(.+)|\1 true;|" "$_conf" || sed -Ei "s|(^\\\$use_auth =)(.+)|\1 false;|" "$_conf" +[ "$global_readonly" -eq "1" ] && sed -Ei "s|(^\\\$global_readonly =)(.+)|\1 true;|" "$_conf" || sed -Ei "s|(^\\\$global_readonly =)(.+)|\1 false;|" "$_conf" +sed -Ei "/^\\\$auth_users /,/\);/{/^ /d}" "$_conf" && sed -Ei "/^\\\$auth_users /a\ $auth_users" "$_conf" +sed -Ei "/^\\\$readonly_users /,/\);/{/^ /d}" "$_conf" && sed -Ei "/^\\\$readonly_users /a\ $readonly_users" "$_conf" +sed -Ei "/^\\\$exclude_items /{s|array\(\);|array(\n);|}" "$_conf" && sed -Ei "/^\\\$exclude_items /a\ $exclude_items" "$_conf" +sed -Ei "s|(^\\\$default_timezone =)(.+)|\1 '$(uci -q get system.@system[0].zonename)';|" "$_conf" +if [ -n "$root_path" ]; then + sed -Ei "s|(^\\\$root_path =)(.+)|\1 \\\$_SERVER['DOCUMENT_ROOT'].'/${HOMEPATH}$root_path';|; \ + s|(^\\\$root_url =)(.+)|\1 '${HOMEPATH}$root_path';| \ + " "$_conf" +fi +sed -Ei "s|(^\\\$datetime_format =)(.+)|\1 '$date_format $time_format';|" "$_conf" +[ "$show_second" -eq "0" ] && sed -Ei "/^\\\$datetime_format =/{s|:s||}" "$_conf" +sed -Ei "s|(^\\\$favicon_path =)(.+)|\1 '$favicon_path';|" "$_conf" +if [ "$online_viewer" == "0" ]; then sed -Ei "s|(^\\\$online_viewer =)(.+)|\1 false;|" "$_conf" +else sed -Ei "s|(^\\\$online_viewer =)(.+)|\1 '$online_viewer';|" "$_conf" +fi +if [ "$(( $max_upload_size +0 ))" == "0" ]; then sed -Ei "s|(^\\\$max_upload_size_bytes =)(.+)|\1 26214400;|" "$_conf" #25M +else sed -Ei "s|(^\\\$max_upload_size_bytes =)(.+)|\1 $(( $max_upload_size * 1024**2 ));|" "$_conf" +fi +} + +start_service() { + mkdir /var/etc 2>/dev/null + touch "$CONF" + ln -s "$CONF" "$WORKDIR/config.php" 2>/dev/null + init_config "$CONF" + + config_load "$CONFIG_NAME" + config_foreach validate_section "$TYPEDSECTION" apply_config +} + +service_triggers() { + procd_add_reload_trigger "$CONFIG_NAME" 'system' +} + +check() { + local owner='prasathmani' + local repo='tinyfilemanager' + mkdir $VARDIR 2>/dev/null + curl -sSL https://api.github.com/repos/$owner/$repo/releases \ + | grep '"tag_name":' | sed "/$VERSION/,\${/$VERSION/b;d}" \ + | sed -E 's|",||g; s|^.+"([^"]+)|\1|g' \ + > "$VARDIR/releaseslist" +} + +restart() { + start +} diff --git a/luci-app-tinyfilemanager/root/etc/nginx/conf.d/tinyfilemanager.locations b/luci-app-tinyfilemanager/root/etc/nginx/conf.d/tinyfilemanager.locations new file mode 100644 index 000000000..d580341ff --- /dev/null +++ b/luci-app-tinyfilemanager/root/etc/nginx/conf.d/tinyfilemanager.locations @@ -0,0 +1,33 @@ +location = /tinyfilemanager/ { + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + if (!-f $document_root$fastcgi_script_name) { + return 404; + } + + # Mitigate https://httpoxy.org/ vulnerabilities + fastcgi_param HTTP_PROXY ""; + + #error_log /dev/null; + fastcgi_connect_timeout 300s; + fastcgi_read_timeout 300s; + fastcgi_send_timeout 300s; + fastcgi_buffer_size 32k; + fastcgi_buffers 4 32k; + fastcgi_busy_buffers_size 32k; + fastcgi_temp_file_write_size 32k; + client_body_timeout 10s; + send_timeout 60s; # default, increase if experiencing a lot of timeouts. + output_buffers 1 32k; + fastcgi_index index.php; + include fastcgi_params; + + # Only throw it at PHP-FPM if file exists (prevents PHP exploits). + fastcgi_pass 127.0.0.1:1026; # or: unix:/var/run/php-fpm.sock; + # fastcgi_pass unix:/var/run/php8-fpm.sock; + + # SCRIPT_FILENAME parameter is used for PHP FPM determining + # the script name. If it is not set in fastcgi_params file, + # i.e. /etc/nginx/fastcgi_params or in the parent contexts, + # please comment off following line: + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; +} diff --git a/luci-app-tinyfilemanager/root/etc/tinyfilemanager/favicon.png b/luci-app-tinyfilemanager/root/etc/tinyfilemanager/favicon.png new file mode 100644 index 000000000..5b6aefe9f Binary files /dev/null and b/luci-app-tinyfilemanager/root/etc/tinyfilemanager/favicon.png differ diff --git a/luci-app-tinyfilemanager/root/etc/uci-defaults/99_luci-app-tinyfilemanager b/luci-app-tinyfilemanager/root/etc/uci-defaults/99_luci-app-tinyfilemanager new file mode 100644 index 000000000..3a280dac3 --- /dev/null +++ b/luci-app-tinyfilemanager/root/etc/uci-defaults/99_luci-app-tinyfilemanager @@ -0,0 +1,12 @@ +#!/bin/sh + +# uhttpd +uci -q get uhttpd.main.index_page|grep -i 'index.php' >/dev/null || uci add_list uhttpd.main.index_page='index.php' +uci -q get uhttpd.main.interpreter|grep -i '.php=/usr/bin/php-cgi' >/dev/null || uci add_list uhttpd.main.interpreter='.php=/usr/bin/php-cgi' +uci changes uhttpd|grep . >/dev/null && uci commit uhttpd +/etc/init.d/uhttpd reload + +# nginx +[ -n "$(command -v nginx)" ] && /etc/init.d/nginx reload + +exit 0 diff --git a/luci-app-tinyfilemanager/root/usr/libexec/tinyfilemanager-update b/luci-app-tinyfilemanager/root/usr/libexec/tinyfilemanager-update new file mode 100755 index 000000000..f5ce5146b --- /dev/null +++ b/luci-app-tinyfilemanager/root/usr/libexec/tinyfilemanager-update @@ -0,0 +1,115 @@ +#!/bin/bash +# dependent: curl tar +# +# LuCI Tiny File Manager +# Author: muink +# Github: https://github.com/muink/luci-app-tinyfilemanager +# + +# PKGInfo +REPOURL='https://github.com/prasathmani/tinyfilemanager' +PKGNAME='tinyfilemanager' +VERSION="$1"; VERSION="${VERSION:=2.5.3}" +# +PKG_DIR=$PKGNAME-$VERSION +REF_DIR="assets" +# +INDEXPHP="tinyfilemanager.php" +#CFGSAMPl="config-sample.php" +LANGFILE="translation.json" + + +WORKDIR="/www/$PKGNAME" +mkdir -p "$WORKDIR" 2>/dev/null +cd $WORKDIR + +if [ "$(uci -q get $PKGNAME.@main[0].proxy_enabled)" == "1" ]; then + export ALL_PROXY=$(uci -q get $PKGNAME.@main[0].proxy_protocol)://$(uci -q get $PKGNAME.@main[0].proxy_server) +fi + + + +# Clear Old version +rm -rf * + +# Download Repository +curl -sSL ${REPOURL}/archive/refs/tags/${VERSION}.tar.gz | tar -xvz -C /tmp + +# Check offline ? +[ -n "$(sed -En "/^\\\$external = array\(/,/^\);/{s,^(.+=\")(http(s)?://.+/)([^/]+\.(css|js))(\".+),\4,p}" /tmp/$PKG_DIR/$INDEXPHP)" ] && { + +# Preprocessing +sed -Ei "//dev/null +refurl=($(sed -En "/^\\\$external /,/^\);/{s,^.+=\"(http(s)?://.+\.(css|js))\".+,\1, p}" /tmp/$PKG_DIR/$INDEXPHP | sort -u )) +ref= +url= +out= +path= + +for _i in $(seq 0 1 $[ ${#refurl[@]} -1 ]); do + eval "url=${refurl[$_i]}" + out=${url##*/} + path="$REF_DIR/${url#http*://}" + + mkdir -p "${path%/*}" 2>/dev/null + curl -sSLo $out $url + mv -f $out "${path%/*}" +done + +ref=$(for _p in $(find * -type f); do \ + sed -E "s/(,|;)/\1\n/g" $_p | grep -E "\burl\([^\)]+\)" | grep -Ev "\burl\(\"data:image" >/dev/null; \ + [ "$?" == "0" ] && echo $_p; \ + done) + +for _i in $ref; do + suburl=($(sed -E "s/(,|;)/\1\n/g" $_i | grep -E "\burl\([^\)]+\)" | grep -Ev "\burl\(\"data:image" | sed -En "s|^[^']+'([^']+)'.+|\1| p")) + hosturl=$(for _ in "${refurl[@]}"; do echo "$_" | grep "${_i##*/}"; done) + + for _j in $(seq 0 1 $[ ${#suburl[@]} -1 ]); do + url="${suburl[$_j]}" + out=${url%%\?*} + path="$REF_DIR/${hosturl#http*://}" + + mkdir -p "${path%/*}/${out%/*}" 2>/dev/null + curl -sSLo ${out##*/} "${hosturl%/*}/$url" + mv -f ${out##*/} "${path%/*}/$out" + done +done + +# Post-processing +sed -i "s|\$__highlightjs_style|' . \$highlightjs_style . '|" /tmp/$PKG_DIR/$INDEXPHP + +# Hotfix + +# Migrating to Local Reference +sed -Ei "s,^(.+=\")(http(s)?://)(.+\.(css|js))(\".+),\1$REF_DIR/\4\6," /tmp/$PKG_DIR/$INDEXPHP + +} + +# FixED +sed -Ei "/^if \(\\\$use_auth\) \{/,/^}/{/\/\/ Logging In/,/\/\/ Form/{s|(fm_redirect\().+|\1FM_SELF_URL);|g}}" /tmp/$PKG_DIR/$INDEXPHP + +# Clean up and Done +[ -d "/tmp/$PKG_DIR/$REF_DIR" ] && cp -rf "/tmp/$PKG_DIR/$REF_DIR" . +mv -f /tmp/$PKG_DIR/$INDEXPHP ./index.php +#mv -f /tmp/$PKG_DIR/$CFGSAMPl . +mv -f /tmp/$PKG_DIR/$LANGFILE . +find * -type d -exec chmod 755 {} \; +find * -type f -exec chmod 644 {} \; +[ ! -d /www/tinyfilemanager/rootfs ] && ln -s / /www/tinyfilemanager/rootfs +/etc/init.d/tinyfilemanager start + + + +#$(INSTALL_DIR) $(1)/usr/libexec +#$(INSTALL_DIR) $(1)/www/$PKGNAME +#$(INSTALL_BIN) run.sh $(1)/usr/libexec/$PKGNAME +#$(CP) $PKG_DIR/$INDEXPHP $(1)/www/$PKGNAME/index.php +#$(CP) $PKG_DIR/$CFGSAMPl $(1)/www/$PKGNAME/$CFGSAMPl +#$(CP) $PKG_DIR/$LANGFILE $(1)/www/$PKGNAME/$LANGFILE +#$(CP) --parents -rf $REF_DIR/ $(1)/www/$PKGNAME/ diff --git a/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json b/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json new file mode 100644 index 000000000..8acb24f34 --- /dev/null +++ b/luci-app-tinyfilemanager/root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json @@ -0,0 +1,37 @@ +{ + "admin/nas": { + "title": "NAS", + "order": 44, + "action": { + "type": "firstchild", + "recurse": true + } + }, + "admin/nas/tinyfilemanager": { + "title": "Tiny File Manager", + "order": 10, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": [ "luci-app-tinyfilemanager" ], + "uci": { "tinyfilemanager": true } + } + }, + "admin/nas/tinyfilemanager/tinyfilemanager": { + "title": "Tiny File Manager", + "order": 1, + "action": { + "type": "view", + "path": "tinyfilemanager/main" + } + }, + "admin/nas/tinyfilemanager/config": { + "title": "Config", + "order": 2, + "action": { + "type": "view", + "path": "tinyfilemanager/config" + } + } +} diff --git a/luci-app-tinyfilemanager/root/usr/share/rpcd/acl.d/luci-app-tinyfilemanager.json b/luci-app-tinyfilemanager/root/usr/share/rpcd/acl.d/luci-app-tinyfilemanager.json new file mode 100644 index 000000000..fa85b5fe2 --- /dev/null +++ b/luci-app-tinyfilemanager/root/usr/share/rpcd/acl.d/luci-app-tinyfilemanager.json @@ -0,0 +1,17 @@ +{ + "luci-app-tinyfilemanager": { + "description": "Grant access to tinyfilemanager procedures", + "read": { + "file": { + "/usr/libexec/tinyfilemanager-update": [ "exec" ], + "/etc/init.d/tinyfilemanager check": [ "exec" ], + "/etc/init.d/tinyfilemanager reload": [ "exec" ], + "/var/tinyfilemanager/releaseslist": [ "read" ] + }, + "uci": [ "tinyfilemanager" ] + }, + "write": { + "uci": [ "tinyfilemanager" ] + } + } +} diff --git a/luci-app-torbp/LICENSE b/luci-app-torbp/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/luci-app-torbp/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/luci-app-torbp/Makefile b/luci-app-torbp/Makefile new file mode 100644 index 000000000..3a22bbf02 --- /dev/null +++ b/luci-app-torbp/Makefile @@ -0,0 +1,72 @@ +# Copyright 2018-2020 Alex D (https://gitlab.com/Nooblord/) +# Copyright 2022 ZeroChaos (https://github.com/zerolabnet/) +# This is free software, licensed under the GNU General Public License v3. + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-torbp +PKG_VERSION:=1.0 +PKG_RELEASE:=1 +PKG_MAINTAINER:=ZeroChaos + +include $(INCLUDE_DIR)/package.mk + +define Package/luci-app-torbp + SECTION:=luci + CATEGORY:=LuCI + DEPENDS:=+tor +tor-geoip +obfs4proxy + TITLE:=Tor bridges proxy + MAINTAINER:=ZeroChaos + URL:=https://zerolab.net + PKGARCH:=all +endef + +define Package/luci-app-torbp/description +Tor with SOCKS 5 proxy with a UI for the ability to add bridges +endef + +define Package/luci-app-torbp/conffiles +/etc/config/torbp +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/luci-app-torbp/install + # Copy config + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/torbp $(1)/etc/config/torbp + + # Copy LuCI Description and ACL + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./files/usr/share/luci/menu.d/luci-app-torbp.json \ + $(1)/usr/share/luci/menu.d/luci-app-torbp.json + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./files/usr/share/rpcd/acl.d/luci-app-torbp.json \ + $(1)/usr/share/rpcd/acl.d/luci-app-torbp.json + + # Copy web stuff + $(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller + $(INSTALL_DATA) ./files/usr/lib/lua/luci/controller/torbp.lua \ + $(1)/usr/lib/lua/luci/controller/torbp.lua + $(INSTALL_DIR) $(1)/usr/lib/lua/luci/model/cbi + $(INSTALL_DATA) ./files/usr/lib/lua/luci/model/cbi/torbp.lua \ + $(1)/usr/lib/lua/luci/model/cbi/torbp.lua + + # Copy translation + $(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n + $(INSTALL_DATA) ./files/usr/lib/lua/luci/i18n/torbp.ru.lmo $(1)/usr/lib/lua/luci/i18n/ +endef + +define Package/luci-app-torbp/postinst + #!/bin/sh + if [ -z "$${IPKG_INSTROOT}" ]; then + rm -f /tmp/luci-indexcache* 2>/dev/null + fi + exit 0 +endef + +$(eval $(call BuildPackage,luci-app-torbp)) diff --git a/luci-app-torbp/README.md b/luci-app-torbp/README.md new file mode 100644 index 000000000..916685adb --- /dev/null +++ b/luci-app-torbp/README.md @@ -0,0 +1,37 @@ +### Описание + +OpenWrt LuCI модуль для Tor с SOCKS 5 proxy сервером и возможностью работы с мостами. Без функции выходного узла, только SOCKS 5 proxy. + +

+ +

+ +### Установка зависимостей + +```bash +opkg install wget-ssl tor tor-geoip obfs4proxy +``` + +### Установка luci-app-torbp + +```bash +cd /tmp +wget https://github.com/zerolabnet/luci-app-torbp/releases/download/1.0/luci-app-torbp_1.0-1_all.ipk +opkg install luci-app-torbp_1.0-1_all.ipk +rm *.ipk +``` + +### Порты по умолчанию + +``` +9150 - порт SOCKS 5 proxy для трафика через сеть Tor +``` + +### Можем использовать в OpenClash: + +```yaml + - name: "Tor" + type: socks5 + server: ROUTER_IP + port: 9150 +``` diff --git a/luci-app-torbp/docs/01-scr.png b/luci-app-torbp/docs/01-scr.png new file mode 100644 index 000000000..6fa5ac864 Binary files /dev/null and b/luci-app-torbp/docs/01-scr.png differ diff --git a/luci-app-torbp/files/etc/config/torbp b/luci-app-torbp/files/etc/config/torbp new file mode 100644 index 000000000..83f7bad6b --- /dev/null +++ b/luci-app-torbp/files/etc/config/torbp @@ -0,0 +1 @@ +config torbp diff --git a/luci-app-torbp/files/usr/lib/lua/luci/controller/torbp.lua b/luci-app-torbp/files/usr/lib/lua/luci/controller/torbp.lua new file mode 100644 index 000000000..6a94415d4 --- /dev/null +++ b/luci-app-torbp/files/usr/lib/lua/luci/controller/torbp.lua @@ -0,0 +1,9 @@ +module("luci.controller.torbp", package.seeall) +function index() + if not nixio.fs.access("/etc/config/torbp") then + return + end + local page + page = entry({"admin", "services", "torbp"}, cbi("torbp"), _("Tor bridges proxy")) + page.dependent = true +end diff --git a/luci-app-torbp/files/usr/lib/lua/luci/i18n/torbp.ru.lmo b/luci-app-torbp/files/usr/lib/lua/luci/i18n/torbp.ru.lmo new file mode 100644 index 000000000..8950a1cf5 Binary files /dev/null and b/luci-app-torbp/files/usr/lib/lua/luci/i18n/torbp.ru.lmo differ diff --git a/luci-app-torbp/files/usr/lib/lua/luci/model/cbi/torbp.lua b/luci-app-torbp/files/usr/lib/lua/luci/model/cbi/torbp.lua new file mode 100644 index 000000000..a6222d82d --- /dev/null +++ b/luci-app-torbp/files/usr/lib/lua/luci/model/cbi/torbp.lua @@ -0,0 +1,162 @@ +-- Copyright 2018-2020 Alex D (https://gitlab.com/Nooblord/) +-- Copyright 2022 ZeroChaos (https://github.com/zerolabnet/) +-- This is free software, licensed under the GNU General Public License v3. + +-- [GLOBAL VARS] -------------------------------------------------------------- +local torrc = "/etc/tor/torrc" +local makeTorConfigButtonPressed = false +local torrcSampleConfig = 'User tor\n' .. + 'HardwareAccel 1\n' .. + 'Log notice syslog\n' .. + 'SocksPort 0.0.0.0:9150\n' .. + 'DataDirectory /var/lib/tor\n' .. + 'ExcludeExitNodes {us},{ca},{cn},{hk},{jp},{kr},{tw},{ru},{ua},{by},{kz},{in},{af},{aq},{ar},{au},{bs},{bh},{bb},{bz},{bo},{bw},{br},{bn},{bf},{bi},{kh},{cm},{cv},{ky},{cf},{td},{cl},{co},{km},{cg},{cd},{ck},{cr},{ci},{cu},{dj},{dm},{do},{ec},{eg},{sv},{gq},{et},{fk},{fo},{fj},{ga},{gm},{gh},{gi},{gl},{gd},{gp},{gu},{gt},{gn},{gw},{gy},{ht},{hn},{id},{ir},{iq},{il},{jm},{jo},{ke},{ki},{kp},{kg},{lb},{ls},{lr},{ly},{mo},{mg},{mw},{my},{mv},{ml},{mt},{mh},{mq},{mr},{mu},{yt},{mx},{fm},{mn},{ms},{ma},{mz},{mm},{na},{nr},{np},{nc},{nz},{ni},{ne},{ng},{nu},{nf},{mp},{om},{pk},{pw},{ps},{pa},{pg},{py},{pe},{ph},{pr},{qa},{re},{rw},{ws},{st},{sa},{sn},{sc},{sl},{sb},{so},{as},{za},{lk},{kn},{lc},{pm},{vc},{sd},{sr},{sz},{sy},{tj},{tz},{th},{tg},{tk},{to},{tt},{tn},{tr},{tm},{tc},{tv},{ug},{ae},{uy},{vu},{vn},{vi},{wf},{ye},{zm},{zw},{??}\n' .. + 'StrictNodes 1\n' .. + 'UseBridges 1\n' .. + 'ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy\n' .. + 'Bridge ' + +local fontred = "" +local fontgreen = "" +local endfont = "" +local bold = "" +local endbold = "" +local brtag ="
" +------------------------------------------------------------------------------- + +-- [VARS INITIALIZATION] ------------------------------------------------------ +-- Detect TOR +local torBinary = luci.util.exec("/usr/bin/which tor") + +if torBinary ~= "" then + local torPid = luci.util.exec("/usr/bin/pgrep tor") + torServiceStatus = luci.util.exec("/bin/ls /etc/rc.d/S??tor 2>/dev/null") + if torServiceStatus ~= "" then + torServiceStatusValue = fontgreen .. translate("ENABLED on boot") .. endfont + else + torServiceStatusValue = fontred .. translate("NOT ENABLED on boot") .. endfont + end + if torPid ~= "" then + torStatus = bold .. fontgreen .. translate("Tor is Running") .. endfont .. + " " .. translate("with PID") .. " " .. torPid .. " " .. + translate("and") .. " " .. torServiceStatusValue .. endbold + else + torStatus = bold .. fontred .. translate("Tor is not Running") .. endfont .. " " .. + translate("and") .. " " .. torServiceStatusValue .. endbold + end +else + torStatus = bold .. fontred .. translate("Tor is not Installed") .. endfont .. endbold +end +-- Detect TOR END +------------------------------------------------------------------------------- + +-- [SECTION INIT] ------------------------------------------------------------- +m = Map("torbp") +m.pageaction = false +m.title = translate("Tor bridges proxy") +m.description = translate("Tor with SOCKS 5 proxy with a UI for the ability to add bridges") +s = m:section(TypedSection, "torbp") +s.anonymous = true +s.addremove = false +------------------------------------------------------------------------------- + +-- [TOR CONFIGURATION TAB] ---------------------------------------------------- +s:tab("torConfig", translate("Tor configuration")) + +torrcStatus = s:taboption("torConfig",DummyValue, "torrcStatus", " ") +torrcStatus.rawhtml = true +function torrcStatus.cfgvalue(self, section) + return torStatus +end + +if torBinary ~= "" then + if torServiceStatus ~= "" then + torrcButtonDisable = s:taboption("torConfig",Button,"Stop & Disable start on boot"," ") + torrcButtonDisable.inputtitle=translate("Stop & Disable start on boot") + torrcButtonDisable.inputstyle="remove" + function torrcButtonDisable.write() + luci.sys.exec("/etc/init.d/tor stop") + luci.sys.exec("sleep 1") + luci.sys.exec("/etc/init.d/tor disable") + luci.sys.exec("sleep 1") + luci.http.redirect(luci.dispatcher.build_url("admin", "services", "torbp")) + end + else + torrcButtonEnable = s:taboption("torConfig",Button,translate("Start & Enable start on boot")," ") + torrcButtonEnable.inputtitle=translate("Start & Enable start on boot") + torrcButtonEnable.inputstyle="apply" + function torrcButtonEnable.write() + luci.sys.exec("/etc/init.d/tor start") + luci.sys.exec("sleep 1") + luci.sys.exec("/etc/init.d/tor enable") + luci.sys.exec("sleep 1") + luci.http.redirect(luci.dispatcher.build_url("admin", "services", "torbp")) + end + end + + if nixio.fs.access(torrc) then + torrcButtonConfig = s:taboption("torConfig",Button,translate("Make me Tor config")," ",translate("Create a sample Tor config")) + torrcButtonConfig.inputtitle=translate("Make me Tor config") + function torrcButtonConfig.write() + makeTorConfigButtonPressed = true + nixio.fs.writefile(torrc, torrcSampleConfig) + luci.http.redirect(luci.dispatcher.build_url("admin", "services", "torbp")) + end + end +end + +if nixio.fs.access(torrc) then + torrcConfig = s:taboption("torConfig",TextValue,"torrcConfig",translate("Edit torrc file")) + torrcConfig.optional = true + torrcConfig.rmempty=true + torrcConfig.rows=19 + torrcConfig.wrap = "off" + + function torrcConfig.cfgvalue(self, section) + if nixio.fs.access(torrc) then + return nixio.fs.readfile(torrc) + else + return "No torrc file." + end + end + + function torrcConfig.write(self, section, value) + if value == nil or value == '' then + elseif nixio.fs.access(torrc) then + value = value:gsub("\r\n?", "\n") + local old_value = nixio.fs.readfile(torrc) + if value ~= old_value and not makeTorConfigButtonPressed then + nixio.fs.writefile(torrc, value) + end + end + end + + torrcButtonRestart = s:taboption("torConfig",Button,"Apply & Restart Tor"," ") + torrcButtonRestart.inputtitle=translate("Apply & Restart Tor") + torrcButtonRestart.inputstyle="apply" + function torrcButtonRestart.write() + luci.sys.exec("/etc/init.d/tor restart") + luci.sys.exec("sleep 1") + luci.http.redirect(luci.dispatcher.build_url("admin", "services", "torbp")) + end +end +------------------------------------------------------------------------------- + +-- [ LOG TAB] ---------------------------------------------------------------- +s:tab("log",translate("Log")) + +logsTor = s:taboption("log",TextValue,"logsTor",translate("Tor log")) +logsTor.readonly = "readonly" +logsTor.rmempty=true +logsTor.rows=30 +logsTor.wrap = "on" + +function logsTor.cfgvalue(self, section) + return luci.util.exec("/sbin/logread -e Bootstrapped") +end + +function logsTor.write(self, section, value) +end +------------------------------------------------------------------------------- + +return m \ No newline at end of file diff --git a/luci-app-torbp/files/usr/share/luci/menu.d/luci-app-torbp.json b/luci-app-torbp/files/usr/share/luci/menu.d/luci-app-torbp.json new file mode 100644 index 000000000..8cab33887 --- /dev/null +++ b/luci-app-torbp/files/usr/share/luci/menu.d/luci-app-torbp.json @@ -0,0 +1,14 @@ +{ + "admin/services/torbp": { + "title": "Tor bridges proxy", + "action": { + "type": "cbi", + "path": "torbp", + "post": { "cbi.submit": true } + }, + "depends": { + "acl": [ "luci-app-torbp" ], + "uci": { "torbp": true } + } + } +} diff --git a/luci-app-torbp/files/usr/share/rpcd/acl.d/luci-app-torbp.json b/luci-app-torbp/files/usr/share/rpcd/acl.d/luci-app-torbp.json new file mode 100644 index 000000000..f132a6953 --- /dev/null +++ b/luci-app-torbp/files/usr/share/rpcd/acl.d/luci-app-torbp.json @@ -0,0 +1,19 @@ +{ + "luci-app-torbp": { + "description": "Grant UCI luci-app-torbp", + "read": { + "cgi-io": [ + "exec" + ], + + "uci": [ + "torbp" + ] + }, + "write": { + "uci": [ + "torbp" + ] + } + } +} \ No newline at end of file diff --git a/luci-app-torbp/lang/torbp.ru.po b/luci-app-torbp/lang/torbp.ru.po new file mode 100755 index 000000000..adf9e055a --- /dev/null +++ b/luci-app-torbp/lang/torbp.ru.po @@ -0,0 +1,56 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8\n" + +msgid "Tor bridges proxy" +msgstr "Tor прокси" + +msgid "Tor with SOCKS 5 proxy with a UI for the ability to add bridges" +msgstr "Tor SOCKS 5 прокси с интерфейсом, позволяющим добавлять мосты" + +msgid "Tor configuration" +msgstr "Настройки Tor" + +msgid "Stop & Disable start on boot" +msgstr "Остановить и Отключить автозапуск" + +msgid "Start & Enable start on boot" +msgstr "Запустить и Включить автозапуск" + +msgid "NOT ENABLED on boot" +msgstr "НЕ ЗАПУСКАЕТСЯ при загрузке" + +msgid "ENABLED on boot" +msgstr "ЗАПУСКАЕТСЯ при загрузке" + +msgid "Tor is Running" +msgstr "Tor запущен" + +msgid "Tor is not Installed" +msgstr "Tor не установлен" + +msgid "Tor is not Running" +msgstr "Tor не запущен" + +msgid "Make me Tor config" +msgstr "Сконфигурировать Tor" + +msgid "Create a sample Tor config" +msgstr "Создать пример конфигурации Tor" + +msgid "Edit torrc file" +msgstr "Редактирование torrc" + +msgid "Apply & Restart Tor" +msgstr "Применить и Перезапустить Tor" + +msgid "Log" +msgstr "Журнал" + +msgid "Tor log" +msgstr "Журнал Tor" + +msgid "and" +msgstr "и" + +msgid "with PID" +msgstr "c PID" diff --git a/luci-app-torbp/lang/torbp.zh-cn.po b/luci-app-torbp/lang/torbp.zh-cn.po new file mode 100644 index 000000000..875a1439e --- /dev/null +++ b/luci-app-torbp/lang/torbp.zh-cn.po @@ -0,0 +1,56 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8\n" + +msgid "Tor bridges proxy" +msgstr "Tor 桥接代理" + +msgid "Tor with SOCKS 5 proxy with a UI for the ability to add bridges" +msgstr "Tor Socks5 代理,具有一个gui界面来添加网桥" + +msgid "Tor configuration" +msgstr "Tor 设置" + +msgid "Stop & Disable start on boot" +msgstr "停止 & 禁止自动启动" + +msgid "Start & Enable start on boot" +msgstr "启动 & 启用自动启动" + +msgid "NOT ENABLED on boot" +msgstr "自动启动已禁用" + +msgid "ENABLED on boot" +msgstr "自动启动已启用" + +msgid "Tor is Running" +msgstr "Tor 正在运行" + +msgid "Tor is not Installed" +msgstr "Tor 未安装" + +msgid "Tor is not Running" +msgstr "Tor 未在运行" + +msgid "Make me Tor config" +msgstr "生成Tor配置" + +msgid "Create a sample Tor config" +msgstr "创建配置示例" + +msgid "Edit torrc file" +msgstr "编辑 torrc 文件" + +msgid "Apply & Restart Tor" +msgstr "应用 & 重启 Tor" + +msgid "Log" +msgstr "日志" + +msgid "Tor log" +msgstr "Tor 日志" + +msgid "and" +msgstr "和" + +msgid "with PID" +msgstr "带有 PID" diff --git a/luci-theme-kucat/Makefile b/luci-theme-kucat/Makefile new file mode 100644 index 000000000..7bdfac879 --- /dev/null +++ b/luci-theme-kucat/Makefile @@ -0,0 +1,25 @@ +# +# Copyright (C) 2019-2023 The Sirpdboy Team +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=Kucat Theme +PKG_NAME:=luci-theme-kucat +LUCI_DEPENDS:= +PKG_VERSION:=2.3.9 + +define Package/luci-theme-kucat/postinst +#!/bin/sh + +rm -Rf /var/luci-modulecache +rm -Rf /var/luci-indexcache +exit 0 + +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-theme-kucat/README.md b/luci-theme-kucat/README.md new file mode 100644 index 000000000..df879bd77 --- /dev/null +++ b/luci-theme-kucat/README.md @@ -0,0 +1,102 @@ +### 访问数:[![](https://visitor-badge.glitch.me/badge?page_id=sirpdboy-visitor-badge)] [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ) + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg) + +# 酷猫主题链接: https://github.com/sirpdboy/luci-theme-kucat +# 进队设置-酷猫主题设置下载链接: [https://github.com/sirpdboy/luci-app-advancedplus](https://github.com/sirpdboy/luci-app-advancedplus/releases) +- 开发时间:2021年12月 +- 发布时间:2023年2月 +- 开源时间:2023年4月 +- main 支持LEDE18.06和官方18.06分支 开源时间:2023年4月 版本:1.2.6 +- JS 支持官网19.07-23.05分支 开源时间:2023年9月 版本:2.0.12 + +# 目前最新版本,仅VIP固件中集成,开源升级时间在6个月以后。感谢大家支持与理解。 + +# 功能介绍 + +- 以酷为美,因动而生。 +- 带7种快捷键的工具栏。 +- 基本BUG,适应目前已经所有插件. +- 针对手机等做了大量优化. +- 有配套的主题设置工具,可调节快捷键、背景图片、颜色多种方案可供选择。 + +#【说在前面的话】此次酷猫主题研发借鉴:Opentopd主题、Jerryk大神argon主题、以及thinktip大神的neobird主题的部分灵感及参考借用部分代码,在这里表示感谢,感谢有你们珠玉在前! +#目前源码暂不开源,如果你们需要联系本人可以免费获取。 + +# 为什么叫酷猫 ? + +## 二个原因: + +## 一是这主题就是为酷而生,一切只为酷与简单!从此让你的主题不再单调!让主题活起来!按钮动起来!全面掌控主题颜色设置! + +## 二是因为这主题是在九个方案版本中测试研究后决定出来(正合猫有9条命),另外是本人(年后到现在)在无数个夜班熬呀熬,熬出来的,都熬成夜猫子了,所以定名:【英文名:KuCat】,【中文名:酷猫】。 + + +另外要说明一下就是本人弄固件不只是编译与搬运,是开发,是创新,更是爱好!请不要将本人与一些搬运工编译的固件来比较!那样会只会显得你很无知。 + +当然在OPENWRT的世界里比本人更优秀更专业的人士大有人在,本人能做的就是尽自己微薄浅湿的力量,尽量让固件更好用和更少的BUG。 + +不管是本人还是别人开发的插件,这里面的每一行代码,都凝聚着开发者在后面无数辛勤汗水和智慧的付出,请大家珍重他们的劳动成果与心血付出! + +也许某些功能某些要求还达不到你的预期要求,有道是一人难满百人意,有些可能是本身实现不了,有些可能是能力不足的原因实现不了. + +不管如何,如果有问题或者BUG请多给理解与支持【当然有能力的请给这些开发者们更多的物资支持】! + +在此要特别感谢VIP群里以及不在群里的所有为爱发电的好朋友们!是你们给本人无尽的力量!!谢谢!! + +## 界面 + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/kucat1.jpg) + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/kucat2.jpg) + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/kucat3.jpg) + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/kucat4.jpg) + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/kucat5.jpg) + + +## 使用与授权相关说明 + +- 本人开源的所有源码,任何引用需注明本处出处,如需修改二次发布必告之本人,未经许可不得做于任何商用用途。 + + +# My other project + +- 网络速度测试 :https://github.com/sirpdboy/NetSpeedTest + +- 定时设置插件 : https://github.com/sirpdboy/luci-app-autotimeset + +- 关机功能插件 : https://github.com/sirpdboy/luci-app-poweroffdevice + +- opentopd主题 : https://github.com/sirpdboy/luci-theme-opentopd + +- kucat 主题: https://github.com/sirpdboy/luci-theme-kucat + +- 家长控制: https://github.com/sirpdboy/luci-theme-parentcontrol + +- 系统高级设置 : 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-wizard + +- 分区扩容: 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) | + + + 图飞了😂 + + diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/css/dark.css b/luci-theme-kucat/htdocs/luci-static/kucat/css/dark.css new file mode 100644 index 000000000..4dc5a7f45 --- /dev/null +++ b/luci-theme-kucat/htdocs/luci-static/kucat/css/dark.css @@ -0,0 +1,769 @@ + +body { + font-family: "Microsoft Yahei", "Google Sans", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB"!important +} +body { + color: #98a6ad; +} + + +*::-webkit-scrollbar { + background: transparent +} + +*::-webkit-scrollbar-thumb { + background: #666; + height: 10px; + border-radius: 5px +} + +*::-webkit-scrollbar-thumb:hover { + background-color: #bbb +} + +*::-webkit-scrollbar-track { + background: transparent +} + +::selection { + background-color: #374564!important; + color: #ccc; +} + +a:link, +a:visited, +a:active { + color: #d0d7de; +} + +a { + color: #d0d7de; +} + + +header.bar-primary .container-bar-right, +header.bar-primary .container-bar-left { + background-color: rgba(var(--primary-rgbm), 1); + box-shadow: 0 0.5rem 1rem rgba(255, 255, 255, 0.15); + transition: all .2s ease; + transition: 0.3s ease-in-out +} + + +header.bar-primary .container-bar-right .labelbar:hover::before, +header.bar-primary .container-bar-left .labelbar:hover::before { + color: #d0d7de; +} + +header.bar-primary .container-bar-left .pdboy-closebar:before { + color: #d0d7de +} + +header.bar-primary .container-bar-right .pdboy-openbar:before { + color: #d0d7de +} + +header.bar-primary .container-bar-left .pdboy-gohome:before { + color: #d0d7de +} + +header.bar-primary .container-bar-left .pdboy-gossr:before { + color: #d0d7de +} + +header.bar-primary .container-bar-left .pdboy-gonet:before { + color: #d0d7de +} + +header.bar-primary .container-bar-left .pdboy-gouser:before { + color: #d0d7de +} + +header.bar-primary .container-bar-left .pdboy-gocontrol:before { + color: #d0d7de +} + +header.bar-primary .container-bar-left .pdboy-goadvanced:before { + color: #d0d7de +} + +#detail-bubble>div { + border: 1px solid rgba(255,255,255,0.3); + border-radius: 3px; + padding: 1px; + background-color: rgba(var(--primary-rgbbody), 1)!important; + +} + +#detail-bubble .head .dismiss { + color: #8391a2; +} + +#bubble-arrow { + border: 1px solid rgba(255,255,255,0.3); + background-color: rgba(var(--primary-rgbbody), 1); + +} + + +header.bg-primary { + box-shadow: 0 0.5rem 1rem rgba(255, 255, 255, 0.35); +} + +.uci_change_indicator.label.notice, +.pd-primary .fill .container .flex1 .showSide, +.pd-primary .fill .container .flex1 .brand, +.showWord::after, +.showWord::before , +header>.fill>.container>.showWord , +.showWord { + color: #e9eff3 +} + +header>.fill>.container>.brand { + font-family: "Microsoft Yahei", "Google Sans", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB"; + color: #e9eff3; +} +.pull-right a, +.pull-right span a { + color: #bbb; +} + +.pull-right a:hover, +.pull-right span a:hover { + background-color: rgba(255, 255, 255, 0.2); + color: #f8f8f8; +} + + +.pdboy-status_on::before { + color: #e9eff3 +} +.showSide { + color: #e9eff3; +} + + + +.notice { + color: #bbb +} + +.label { + color: #bbb; +} + +footer { + color: #bbb; +} + +footer a { + color: #bbb; +} + +.main .main-left { + -webkit-box-shadow: rgb(250 250 250 / 75%) 0 0 15px -5px; + box-shadow: 2px 2px 8px #666,-2px -2px 8px #bbb; +} + +.main .main-left::-webkit-scrollbar-thumb { + height: 8px; + background: #555; + border-radius: 5px +} + +.main .main-left::-webkit-scrollbar-thumb:hover, +.main .main-left::-webkit-scrollbar-thumb:active { + background: #bbb; + border: none +} + + + +.main .main-left .nav li.slide .menu { + color: #bbb +} + +.main .main-left .nav .slide .menu:after, +.main .main-left .nav li.slide .menu::after { + color: #bbb; +} + +.main .main-left .nav li.slide .menu:hover a::after, +.main .main-left .nav li.slide .menu.active a::after { + color: #bbb; +} + +.main>.main-left>.nav>.slide>.menu.active::before, +.main .main-left .nav li.slide .menu:hover::after, +.main .main-left .nav li.slide .menu.active::after { + color: #e9eff3 +} +.main .main-left .nav>li>a:hover, + .main .main-left .nav>li>a.active, +.main .main-left .nav li.slide .menu:hover, +.main .main-left .nav li.slide .menu.active { + background-color: rgba(255, 255, 255, 0.2); + color: #e9eff3 !important; +} + +.main .main-left .nav li.slide .menu:hover a, +.main .main-left .nav li.slide .menu.active a { + color: #e9eff3 +} + +.main .main-left .nav li.slide .menu:hover::before, +.main .main-left .nav li.slide .menu.active::before { + color: #e9eff3 +} + +.main .main-left .nav li.slide .slide-menu li a { + color: #bbb; +} + +.main .main-left .nav .slide .slide-menu .active a { + color: #e9eff3; +} + +.main .main-left .nav .slide .slide-menu > li.active { + color: #e9eff3; + background-color: rgba(255, 255, 255, 0.2); + border-left: 4px solid #fd8c73 +} + +.main .main-left .nav .slide .slide-menu li:hover { + background-color: rgba(255, 255, 255, 0.2); +} + +.main .main-left .nav .slide .slide-menu li:active a { + color: #e9eff3 +} + +.main .main-left .nav .slide .slide-menu li:hover a { + color: #e9eff3; +} + +.main .main-left .sidenav-header .brand { + font-family: "Microsoft Yahei",-apple-system, "WenQuanYi Micro Hei", "sans-serif"; + color: #ccc; +} + + + +.main .main-right #maincontent .container>div:nth-child(1).alert-message.warning>a { + color: #ccc; +} +.main .main-left .nav>li>a:first-child, +.main .main-left .nav li.slide .menu::before, +.main .main-left .nav>li>a:first-child::before, +.main .main-left .nav .slide .menu::before { + color: #bbb +} + + +select, +input { + font-family: "Microsoft Yahei", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB"; + +} + +input[type="checkbox"] { + background: rgba(255,255,255,0.1); +} + + +input:checked[type="checkbox"]:before { + transform: rotate(45deg); + width: 12px; + margin-left: 5px; + border-color: #f5f5f5; + border-width: 3px; + border-top-color: transparent; + border-left-color: transparent; + border-radius: 0 +} + + +.cbi-input-radio:checked { + background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'-4 -4 8 8\'%3e%3ccircle r=\'3\' fill=\'%23fff\'/%3e%3c/svg%3e') !important; + background-color: rgba(255,255,255,0.7) !important; + background-size: 70%; + background-repeat: no-repeat; + background-position: center +} + + +select:not([multiple="multiple"]):focus, input:not(.cbi-button):focus, .cbi-dropdown:focus { + + background-color: rgba(255,255,255,0.18); + box-shadow: 0 0.5rem 1rem rgba(255,255,255, 0.35); + -webkit-box-shadow: 0 0 6px rgba(255,255,255, 0.35); + -moz-box-shadow: 0 0 6px rgba(255,255,255, 0.35); +} + +.btn, button, select, input, .cbi-dropdown,.item::after { + color: #d0d7de; + border: 1px solid rgba(255,255,255,0.12)!important; + box-shadow: 0 0.5rem 1rem rgba(255, 255, 255, 0.12)!important; + +} +.btn, .cbi-button, .item::after { + color: #eee; +} + +.cbi-input-textarea, +textarea { + color: #8391a2; + background-color: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.12)!important; + box-shadow: 0 0.5rem 1rem rgba(255, 255, 255, 0.12)!important; +} + + +#diag-rc-output>pre { + color: #bbb +} + + +#swaptotal>div>div>div>small, +#swapfree>div>div>div>small, +#memfree>div>div>div>small, +#membuff>div>div>div>small, +#conns>div>div>div>small, +#memtotal>div>div>div>small { + color: #ccc !important; +} + + +.node-main-login .main .main-right #maincontent .alert-message p { + color: #eee; + font-weight: normal; +} +.node-main-login .errorbox { + color: #eee !important; +} + +.table { + color: #8391a2; +} + + +.table-titles .th { + color: #8391a2 +} + + +button:hover, +.btn:hover, +.cbi-button:hover, +.item:hover::after { + transform: scale(1.05) translate(0, -0.15rem); + + box-shadow: 0 0.5rem 1rem rgba(255,255,255, 0.35); + -webkit-box-shadow: 0 0 6px rgba(255,255,255, 0.35); + -moz-box-shadow: 0 0 6px rgba(255,255,255, 0.35); +} + +.btn:active , +.cbi-button:active , +button:active, +.item:hover::after { + transform: scale(1) translate(0, 0.15rem); + + box-shadow: 0 0.5rem 1rem rgba(255,255,255, 0.15); + -webkit-box-shadow: 0 0 6px rgba(255,255,255, 0.15); + -moz-box-shadow: 0 0 6px rgba(255,255,255, 0.15); +} + + +fieldset[id^="cbi-apply-"] { + box-shadow: 0 2px 2px 0 rgba(255, 255, 255, 0.14), 0 3px 1px -2px rgba(255, 255, 255, 0.32), 0 1px 5px 0 rgba(255, 255, 255, 0.2); +} + +.cbi-section>h3:first-child, +.panel-title { + color: #bbb +} + +table>tbody>tr>td, +table>tbody>tr>th, +table>tfoot>tr>td, +table>tfoot>tr>th, +table>thead>tr>td, +table>thead>tr>th, +.table>.tbody>.tr>.td, +.table>.tbody>.tr>.th, +.table>.tfoot>.tr>.td, +.table>.tfoot>.tr>.th, +.table>.thead>.tr>.td, +.table>.thead>.tr>.th { + color: #8391a2 +} + +.cbi-tabcontainer>.cbi-value:nth-of-type(4n+2):hover,.cbi-map>.cbi-section .cbi-value:nth-of-type(4n+2):hover,fieldset>table>tbody>tr:nth-of-type(4n+2):hover,table>tbody>tr:nth-of-type(4n+2):hover,div>.table>.tr:nth-of-type(4n+2):hover { + background-color: rgba(250,250,250,0.05); +} + +.cbi-tabcontainer>.cbi-value:nth-of-type(2n+1):hover,.cbi-map>.cbi-section .cbi-value:nth-of-type(2n+1):hover,fieldset>table>tbody>tr:nth-of-type(2n+1):hover,table>tbody>tr:nth-of-type(2n+1):hover,div>.table>.tr:nth-of-type(2n+1):hover { + background-color: rgba(250,250,250,0.05); +} + +.cbi-tabcontainer>.cbi-value:nth-of-type(4n):hover,.cbi-map>.cbi-section .cbi-value:nth-of-type(4n):hover,fieldset>table>tbody>tr:nth-of-type(4n):hover,table>tbody>tr:nth-of-type(4n):hover,div>.table>.tr:nth-of-type(4n):hover { + background-color: rgba(250,250,250,0.05); +} + + +.cbi-rowstyle-2 .cbi-button-up, .cbi-rowstyle-2 .cbi-button-down, body:not(.Interfaces) .cbi-rowstyle-2:first-child { + background-color: rgba(0,0,0,0, 0.2) !important; +} +.cbi-rowstyle-1 .cbi-button-up, .cbi-rowstyle-1 .cbi-button-down, body:not(.Interfaces) .cbi-rowstyle-1:first-child { + background-color: rgba(0,0,0,0, 0.1) !important; +} + +.status-bar { + font-size: var(--font-x); + color: #8391a2 !important; + background-color: rgba(var(--primary-rgbbody), 0.9) +} +.success { + color: #ddd; +} + +.danger { + color: #ddd; +} + +.errorbox { + color: #ccc; +} + + +h2 { + color: #bbb +} + + +h3 { + + color: #bbb; +} + + +.panel-title { + color: #bbb; +} + + +.tabs { + margin: 1rem; + color: #b4c9e3; +} + + +.tabs::-webkit-scrollbar-thumb { + background-color: #555 +} + +.tabs::-webkit-scrollbar-track { + background: transparent +} + +.tabs>li { + background: #343a40; + box-shadow: 0 0.5rem 1rem rgba(255, 255, 255, 0.15); +} + +.tabs li[class~="active"] { + border-right: 0.18751rem solid #fd8c73; + border-left: 0.18751rem solid #fd8c73; + box-shadow: 0 0.5rem 1rem rgba(255, 255, 255, 0.15); + background-color: rgba(255, 255, 255, 0.2); +} + +.tabs li[class~="hover"] { + background-color: rgba(255, 255, 255, 0.2); +} + +.tabs li:hover { + box-shadow: 0 0.5rem 1rem rgba(255, 255, 255, 0.35); + background-color: rgba(255, 255, 255, 0.2); +} + +.tabs li[class~="active"] a { + color: #bbb +} + +.tabs li:hover a { + color: #bbb +} + +.tabs li a { + color: #8391a2 +} + +.cbi-tabmenu { + color: #8391a2; +} + +.cbi-tabmenu::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +.cbi-tabmenu::-webkit-scrollbar-thumb { + background-color: #555; +} + + +.cbi-tabmenu li { + background: #343a40; + font-size: 1rem; + border-radius: 0.25rem; + + box-shadow: 0 0.5rem 1rem rgba(255, 255, 255, 0.15); +} + +.cbi-tabmenu li a { + color: ##8391a2; +} + +.cbi-tabmenu li:hover { + color: #bbb; + box-shadow: 0 0.5rem 1rem rgba(255, 255, 255, 0.35); + background-color: rgba(255, 255, 255, 0.2); +} + +.cbi-tabmenu li:hover a { + color: #bbb +} + +.cbi-tabmenu li[class~="cbi-tab"] { + border-right: 0.18751rem solid #fd8c73; + border-left: 0.18751rem solid #fd8c73; + + transform: scale(1) translate(0, 0.15rem); + box-shadow: 0 0.5rem 1rem rgba(255, 255, 255, 0.15); + background-color: rgba(255, 255, 255, 0.2); +} + +.cbi-tabmenu li[class~="cbi-tab"] a { + color: #bbb +} + +.cbi-tab-descr { + color: #8391a2; +} + + +.cbi-dropdown>.open { + background: #333 +} +.cbi-dropdown>.more, +.cbi-dropdown>ul>li[placeholder] { + text-shadow: 1px 1px 0 #ccc; +} + + +.cbi-dropdown[open]>ul.dropdown { + background: #ccc; +} + + +.cbi-dropdown[open]>ul.dropdown>li[selected] { + background-color: #8391a2; + color: #fff; +} + +.cbi-progressbar { + color: #8391a2; +} +.cbi-value-title { + color: #8391a2; +} + +.cbi-section-descr { + color: #8391a2 +} + +.cbi-map-descr { + color: #8391a2; +} + +#content_syslog{ + box-shadow: 0 0 1px rgba(255, 255, 255, 0.12); +} +#syslog { + color: #8391a2; + font-family: "Microsoft Yahei",-apple-system, "WenQuanYi Micro Hei", "sans-serif"; + background-color: rgba(0,0,0,0.1); +} +.request>.requestBody, +.response .markdown-body, +.markdown-body { +color: #8391a2!important; +background-color: rgba(255,255,255,0)!important; +} +.bottom_wrapper .message_input_wrapper .message_input_text{ + background-color: rgba(255,255,255,0.35)!important; + color: #d0d7de!important; +} +#chatlog .response .markdown-body>pre { +background-color: rgba(0,0,0,0.15)!important; +} +#chatlog .response { + background-color: rgba(255,255,255,0.03)!important; +} +.ifacebox-head { + color: #8391a2 +} + +.zonebadge { + + color: #8391a2; +} + +.zonebadge strong { + color: #8391a2 +} + + +.Reboot>.main>.main-right #maincontent .container p { + color: #8391a2; +} + + +.node-services-vssr .block h4 { + color: #8391a2 +} + +.node-services-vssr .status-bar { + color: #8391a2; + box-shadow: 0 0 .5rem 0 rgba(0, 0, 0, 0.35); + background-color: rgba(0, 0, 0, 0.1) !important; +} + +.node-services-vssr .block, +.node-services-shadowsocksr>.block, +.node-services-ssrpro>.block, +.block, +.node-services-bypass>.main .block, +.node-services-vssr>.main .block, +.main .block { + color: #8391a2; + background-color: rgba(255, 255, 255, 0.08) !important; +} + +.node-services-shadowsocksr>.block:active, +.node-services-ssrpro>.block:active, +.block:active, +.node-services-bypass>.main .block:active, +.main .block:active, +.node-services-shadowsocksr>.block:hover, +.node-services-ssrpro>.block:hover, +.block:hover, +.node-services-bypass>.main .block:hover, +.main .block:hover { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 4px 6px rgba(0, 0, 0, 0.35); + background-color: rgba(250, 250, 250, 0.1) +} + +/* log */ +#cbi-openclash .CodeMirror.cm-s-idea.CodeMirror-wrap { + background: rgba(0,0,0,0.1)!important; + color: #adbcc9!important; +} + +/* set modi file */ +#cbi-openclash .cm-s-material.CodeMirror +{ + background-color: #1b4c53!important; + color: #31b9c1!important; +} + +/* card */ +#cbi-openclash .card +{ + background: linear-gradient(#337ab7, #a8bfcf)!important; + box-shadow: 0 8px 16px -8px rgba(255,255,255,0.5)!important; +} + +.node-services-adguardhome>.main .cbi-value .cbi-input-textarea { + box-shadow: 0 0 1px rgba(255, 255, 255, 0.3)!important; +} + +.Software >.main table tr td:nth-last-child(1) , +.node-system-packages>.main table tr td:nth-last-child(1) { + color: #8391a2 +} + +.node-system-packages>.main .cbi-value>pre { + background-color: rgba(0, 0, 0, 0.22) ; +} + +.Software >.main .cbi-section-node:first-child .cbi-value-last>div, +.node-system-packages>.main .cbi-section-node:first-child .cbi-value-last>div { + border-color: rgba(255, 255, 255, 0.3) !important; + background-color: rgba(255, 255, 255,0.22) !important; +} + +.node-system-packages>.main .cbi-section-node:first-child .cbi-value-last>div>div { + background-color: rgba(var(--primary-rgbm), 1)!important; + +} + +.cbi-section { + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.08); +} + +.cbi-section em { + color: #8391a2; +} + + +.node-status-realtime table>tbody>tr>td, +.node-status-realtime table>tfoot>tr>td, +.node-status-realtime table>thead>tr>td { + color: #8391a2; + border-bottom: 1px solid rgba(255,255,255,0.3); +} + +.cbi-dynlist > .item > span +{ +color: #aaa; +} + +.cbi-dropdown[open]>ul.dropdown { + background-color: rgba(var(--primary-rgbbody), 1); +} +.ifacebox { + color: #bbb; +} +.ifacebadge { + background-color: #797d7f; + color: #bbb; +} + +.cbi-dropdown > .open, +.cbi-dropdown > .more { + background-color: rgba(255,255,255,0)!important; +} + +@media screen and (max-width:992px) { + + + .main .main-left { + -webkit-box-shadow: rgba(250 250 250, 0.75) 0 0 20px -5px; + box-shadow: rgba(250 250 250, 0.75) 0 0 20px -5px; + } + + .showSide:hover { + background-color: rgba(255, 255, 255, 0.2); + } + + table>tbody>tr>td, + table>tfoot>tr>td, + table>thead>tr>td { + color: #8391a2; + } + +} diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/css/fonts.css b/luci-theme-kucat/htdocs/luci-static/kucat/css/fonts.css new file mode 100644 index 000000000..2bfda3280 --- /dev/null +++ b/luci-theme-kucat/htdocs/luci-static/kucat/css/fonts.css @@ -0,0 +1,253 @@ +/** + * kucat is a clean HTML5 theme for LuCI. It is based on luci-theme-material + * + * luci-theme-material + * Copyright 2015 Lutty Yang + * + * Copyright 2019-2023 sirpdboy + * + * Licensed to the public under the Apache License 2.0 + */ +@font-face { + font-family: 'sirpdboy-kucat'; + src: url('../fonts/sirpdboy-kucat.eot?9qz8zf'); + src: url('../fonts/sirpdboy-kucat.eot?9qz8zf#iefix') format('embedded-opentype'), + url('../fonts/sirpdboy-kucat.ttf?9qz8zf') format('truetype'), + url('../fonts/sirpdboy-kucat.woff?9qz8zf') format('woff'), + url('../fonts/sirpdboy-kucat.svg?9qz8zf#sirpdboy-kucat') format('svg'); + font-weight: normal; + font-style: normal; + font-display: block; +} + +[class^="icon-"], [class*=" icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'sirpdboy-kucat' !important; + speak: never; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-brightness_low:before { + content: "\e926"; +} +.icon-brightness_4:before { + content: "\e92e"; +} +.icon-sync_disabled:before { + content: "\e932"; +} +.icon-sync_problem:before { + content: "\e933"; +} +.icon-power_settings_new:before { + content: "\e934"; +} +.icon-logout:before { + content: "\e935"; +} +.icon-menu_open:before { + content: "\e92f"; +} +.icon-menu_open1:before { + content: "\e930"; +} +.icon-published_with_changes:before { + content: "\e936"; +} +.icon-update_disabled:before { + content: "\e937"; +} +.icon-arrow-left:before { + content: "\e938"; +} +.icon-arrow-right:before { + content: "\e939"; +} +.icon-chevrons-right:before { + content: "\e931"; +} +.icon-download:before { + content: "\e904"; +} +.icon-eye:before { + content: "\e92b"; +} +.icon-eye-off:before { + content: "\e92c"; +} +.icon-fast-forward:before { + content: "\e93a"; +} +.icon-gitlab:before { + content: "\e93d"; +} +.icon-heart:before { + content: "\e927"; +} +.icon-key:before { + content: "\e93b"; +} +.icon-message-square:before { + content: "\e91f"; +} +.icon-more-horizontal:before { + content: "\e928"; +} +.icon-navigation-2:before { + content: "\e93e"; +} +.icon-rewind:before { + content: "\e921"; +} +.icon-save:before { + content: "\e923"; +} +.icon-slash:before { + content: "\e92d"; +} +.icon-thumbs-up:before { + content: "\e924"; +} +.icon-twitch:before { + content: "\e925"; +} +.icon-unlock:before { + content: "\e93c"; +} +.icon-upload-cloud:before { + content: "\e929"; +} +.icon-user-plus:before { + content: "\e92a"; +} +.icon-airplay:before { + content: "\e900"; +} +.icon-align-justify:before { + content: "\e922"; +} +.icon-archive:before { + content: "\e901"; +} +.icon-box:before { + content: "\e902"; +} +.icon-chevron-down:before { + content: "\e20b"; +} +.icon-chevron-right:before { + content: "\e920"; +} +.icon-chevron-up:before { + content: "\e20a"; +} +.icon-chevrons-left:before { + content: "\e903"; +} +.icon-clock:before { + content: "\e905"; +} +.icon-codesandbox:before { + content: "\e906"; +} +.icon-database:before { + content: "\e907"; +} +.icon-download-cloud:before { + content: "\e908"; +} +.icon-gift:before { + content: "\e909"; +} +.icon-globe:before { + content: "\e90a"; +} +.icon-grid:before { + content: "\e90b"; +} +.icon-help-circle:before { + content: "\f059"; +} +.icon-home:before { + content: "\e90c"; +} +.icon-layers:before { + content: "\e90d"; +} +.icon-lock:before { + content: "\e90e"; +} +.icon-log-out:before { + content: "\e90f"; +} +.icon-menu:before { + content: "\e20e"; +} +.icon-minus-circle:before { + content: "\e910"; +} +.icon-monitor:before { + content: "\e911"; +} +.icon-moon:before { + content: "\e912"; +} +.icon-pie-chart:before { + content: "\e913"; +} +.icon-plus-circle:before { + content: "\e914"; +} +.icon-power:before { + content: "\e915"; +} +.icon-refresh-cw:before { + content: "\e916"; +} +.icon-send:before { + content: "\e917"; +} +.icon-server:before { + content: "\e918"; +} +.icon-settings:before { + content: "\e919"; +} +.icon-sliders:before { + content: "\e91a"; +} +.icon-sun:before { + content: "\e91b"; +} +.icon-tool:before { + content: "\e91c"; +} +.icon-user:before { + content: "\e91d"; +} +.icon-wifi:before { + content: "\e91e"; +} +.icon-spinner3:before { + content: "\e602"; +} +.icon-spinner6:before { + content: "\e603"; +} +.icon-github:before { + content: "\eab0"; +} +.icon-appleinc:before { + content: "\eabe"; +} +.icon-eye1:before { + content: "\e9ce"; +} diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/css/pure-min.css b/luci-theme-kucat/htdocs/luci-static/kucat/css/pure-min.css new file mode 100644 index 000000000..81dbb1509 --- /dev/null +++ b/luci-theme-kucat/htdocs/luci-static/kucat/css/pure-min.css @@ -0,0 +1,11 @@ +/*! +Pure v2.0.3 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/pure-css/pure/blob/master/LICENSE.md +*/ +/*! +normalize.css v | MIT License | git.io/normalize +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:-webkit-gradient(linear,left top,left bottom,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129fea;outline:1px auto #129fea}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-disabled,.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} \ No newline at end of file diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/css/style.css b/luci-theme-kucat/htdocs/luci-static/kucat/css/style.css new file mode 100644 index 000000000..96e9db7b4 --- /dev/null +++ b/luci-theme-kucat/htdocs/luci-static/kucat/css/style.css @@ -0,0 +1,3412 @@ +/* + * KuCat is a clean HTML5 theme for LuCI. It is based on luci-theme-kucat + * + * Copyright (C) 2019-2023 The Sirpdboy Team + * Copyright 2015 Lutty Yang + * + * Licensed to the public under the Apache License 2.0 + */ + +@import url("fonts.css?v=3"); +:root { + --primary-solid: #fd8c73; + --primarytextcolor: #6c757d; + --primary-title-color: #555583; + --menu-color: #f5f5f5f5; + --menu-hover-color: #fff; + --menu-item-color: #6c757d; + --menu-item-titlebg-color: #e4eaec; + --menu-item-hover-bgcolor: rgba(255, 255, 255, 0.2); + --menu-item-active-color: #d0d7de; + --body-text-color: #3c4655; + --body-color: #6c757d; + --inputbg-color: rgba(255,255,255,0.1); + --inputtext-color: #4d5256; + --inputborder-color: rgba(0,0,0,0.15); + --input-hover-color: #fff; + --input-hover-bgcolor: rgba(255,255,255,0.2); + --font-apple: -apple-system,"Google Sans","Microsoft Yahei","WenQuanYi Micro Hei","sans-serif"; + --font-sans: "Microsoft Yahei",-apple-system, "Google Sans","WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB"; +} +[data-theme="dark"] { + --primary-solid: #fd8c73; + --primarytextcolor: #8391a2; + --primary-title-color: #bbb; + --primary-ufilter: none; + --menu-color: #bbb; + --menu-hover-color: #e9eff3; + --menu-item-color: #b4c9e3; + --menu-item-titlebg-color: #343a40; + --menu-item-hover-bgcolor: rgba(255, 255, 255, 0.2); + --menu-item-active-color: #d0d7de; + --body-text-color: #8391a2; + --body-color: #8391a2; + --inputbg-color: rgba(255,255,255,0.1); + --inputtext-color: #bbb; + --inputborder-color: rgba(255,255,255,0.3); + --input-hover-color: #ccc; + --input-hover-bgcolor: rgba(255,255,255,0.2); + --font-apple: -apple-system, "Microsoft Yahei", "WenQuanYi Micro Hei", "sans-serif"; + --font-sans: "Microsoft Yahei", -apple-system,"Google Sans", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB"; +} +.tr { + display: table-row; +} +.thead { + display: table-header-group; +} +.tbody { + display: table-row-group; +} +.tfoot { + display: table-footer-group; +} +.td, .th { + line-height: normal; + display: table-cell; + padding: 0.5em; + text-align: center; + vertical-align: middle; + white-space: nowrap; +} +.th { + font-weight: bold; + white-space: nowrap; +} +.tr.placeholder { + height: 2rem; +} +.tr.placeholder > .td { + position: absolute; + left: 0; + right: 0; + text-align: center; + background: inherit +} +.td[width="33%"], .td[width="33%"]~.td { + padding: 14px; +} +.table[width="33%"], .th[width="33%"], .td[width="33%"] { + width: 33%; +} +.table[width="100%"], .th[width="100%"], .td[width="100%"] { + width: 100%; +} +.col-1 { + flex: 1 1 30px !important; + -webkit-flex: 1 1 30px !important +} +.col-2 { + flex: 2 2 60px !important; + -webkit-flex: 2 2 60px !important +} +.col-3 { + flex: 3 3 90px !important; + -webkit-flex: 3 3 90px !important +} +.col-4 { + flex: 4 4 120px !important; + -webkit-flex: 4 4 120px !important +} +.col-5 { + flex: 5 5 150px !important; + -webkit-flex: 5 5 150px !important +} +.col-6 { + flex: 6 6 180px !important; + -webkit-flex: 6 6 180px !important +} +.col-7 { + flex: 7 7 210px !important; + -webkit-flex: 7 7 210px !important +} +.col-8 { + flex: 8 8 240px !important; + -webkit-flex: 8 8 240px !important +} +.col-9 { + flex: 9 9 270px !important; + -webkit-flex: 9 9 270px !important +} +.col-10 { + flex: 10 10 300px !important; + -webkit-flex: 10 10 300px !important; + white-space: inherit +} +.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { + font-family: inherit; + font-weight: normal; + line-height: 1.1 !important; + color: inherit; + clear: inherit; + text-transform: capitalize; +} + +::-webkit-scrollbar-track { + box-shadow: inset 0 0 0px rgba(240, 240, 240, .5); + background-color: rgb(255 255 255 / 0%); +} +::-webkit-scrollbar { + width: 8px; + height: 10px; +} + +::-webkit-scrollbar-thumb { + height: 15px; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; + background-clip: content-box; + background: #ccc; +} +::-webkit-scrollbar-thumb:hover { + background-color: #999 +} +::selection { + background-color: #7686d1!important; + color: #f8f8f8 +} +html { + margin: 0px; + padding: 0px; + height: 100%; + background-color: rgba(var(--primary-rgbbody), 1); + background: var(--theme-background) no-repeat center center fixed; + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; + background-repeat: no-repeat; +} +html { + -webkit-text-size-adjust: 100%; + overflow-y: hidden; + -ms-text-size-adjust: 100% +} +html, body { + font-family: var(--font-sans); + + height: 100%; + margin: 0; + padding: 0; +} +body { + letter-spacing: 0.050em; + font-size: var(--font-x); + color: var(--body-color); +} +body div { + line-height: 120% +} + +a { + color: var(--primary-title-color); + font-size: var(--font-z); + background-color: transparent; + text-decoration: none +} +a:hover { + font-size: var(--font-z); + text-decoration: underline +} +em { + font-style: normal !important; + line-height: 1.5; + padding-left: 10px; +} +* { + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-tap-highlight-color: transparent +} + +button, input { + overflow: visible; +} +button, input, optgroup, select, textarea { + font-family: inherit; + font-size: 100%; + line-height: 1.15; + border-radius: 0.25rem; + margin: 0; +} +select, input { + padding: 0 20px 0 10px; + margin: 0.2rem 0.2rem 0.2rem 0; +} + +select { + overflow: hidden !important; + width: 100%; + min-width: 8rem; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-size: 24px 16px; + background-repeat: no-repeat; +background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABGCAYAAAA6hjFpAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDcuMS1jMDAwIDc5LmRhYmFjYmIsIDIwMjEvMDQvMTQtMDA6Mzk6NDQgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjhDNzA0QUE0NjE2QTExRUNCMjJFQkQyRkIyNURDNjE3IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjhDNzA0QUEzNjE2QTExRUNCMjJFQkQyRkIyNURDNjE3IiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCAyMi41IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QzNBNTYzNTY2MTUxMTFFQ0IyMkVCRDJGQjI1REM2MTciIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QzNBNTYzNTc2MTUxMTFFQ0IyMkVCRDJGQjI1REM2MTciLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6YjzxlAAACsklEQVR42uyaXUsVURSG51T0U/obfUvhhYF0U0HQhZQVIWVJCAkiSUXfRRcaBFbXQVBBRCASFCJBpQQipw+RREkpCa9aG95gs0DMOp6Z7TwvvAxnzczaa6/nzDnzVTndfSVDxdE6WgAQBBCAIIAABAEEIAggAEEAQQABCAIIQBBAAIIAAhAEEAQQgCCAAAStGSAbzA/N+wvSiwPmB6or16bkoY2afLN5r+q4l2MfDpr7zetV2z7zYpmOkAHByNSE0IxDOdUSxr2rOjLVNVC2n6z77hsYmtFnbqlzHS0aN+7DouorFZBH+ibGUCrmO+YjdaqhVeNVHIxm1Ve6P/XH5ibzLwfllvn4Ko8d8t90MEIde1RXaU97n6oJCw7KNXPbKo3ZpvwxjAXV8YTrkCx7piPFQ7lsbq/xWO3K62E0qQ4uDKXn5kbzTxe/YO6o0Rgdypc5GI0anyt1p5fm3eYfLn7e3PmfuTuVJ1YYZ5fG5Up9CQ2qSfMu3m3u+secXdo/1rzGGeTWyfIaUrPmXPycuWeFuXq0X6w55R8q4NwLe3PxlXmn+buLnzX3/mWOXm0fK+RrUP4MICvTG/MO86yLnzFfWmbfi9ou1qzyvS7wnAt/+31YTZxx8ZPmq+709c/pcoifcvEZ5Rku+HyTeB4yYt5unnbxE+YbEZSwvK54rGntP5LAXJN5QPXWvNX8zcWPmm9rHmF5zK0P22/T/hlAaqv35i3mKRc/bB7VMtaUIL5LaI7JPcIdVZMnXXyT+zyp7T4kNr8kn6mPqdlfllj/VevHEpxbsi85fNTP1ycX/2zerPUZQOqrcUGp6nNVMMYTnlPyrwFNCMILLScSn0++r7zUSFVdZ6wJ8aIcQBBAAIIAAhAEEIAggAAEAQQBBCAIIABBAAEIAghAEEAQQJLQbwEGAK/reX2gh5gQAAAAAElFTkSuQmCC'); +background-position: right center; + +} + +select, .cbi-dropdown { + width: inherit; +} +select option { + background-color: rgba(var(--primary-rgbbody), 0.8); + width: 100%!important; + height: 2rem; + overflow: hidden; +} + +.center { + + width: 100%; + transform: translate(-50, -50%); + text-align: center +} +input[type="checkbox"] { + position: relative; + width: 25px !important; + height: 25px !important; + padding: 5px!important; + background: rgba(var(--primary-rgbbody), 0.7); + -webkit-appearance: none; + outline: none; + border-radius: 0.25rem; + transition: 0.1s +} + +input[type="checkbox"]:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 3px solid rgb(255, 255, 255, 0); + box-sizing: border-box; + transition: 0.15s +} +input:checked[type="checkbox"]:before { + transform: rotate(45deg); + width: 12px; + margin-left: 5px; + border-color: rgba(var(--primary-rgbm), 1); + border-width: 3px; + border-top-color: transparent; + border-left-color: transparent; + border-radius: 0 +} +input[type='file'] { + border: none; + background: none; + height: auto; + line-height: 1.5rem +} +input[type='radio'] { + height: 1.5rem; + width: 1.5rem; + appearance: none !important; + -webkit-appearance: none !important; + background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'-4 -4 8 8\'%3e%3ccircle r=\'3\' fill=\'%23ccc\'/%3e%3c/svg%3e'); + background-color: rgba(var(--primary-rgbbody), var(--primary-rgbm-ts)); + box-shadow: inset 0 2px 1px rgba(255, 255, 255, .6); + padding: 0; + border-radius: 70%; + cursor: pointer; + transition: all .1s; + margin: .25rem 0 +} + +input[type='radio']:checked { + background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'-4 -4 8 8\'%3e%3ccircle r=\'3\' fill=\'%23fff\'/%3e%3c/svg%3e')!important; + background-color: rgba(var(--primary-rgbm),1) !important; + background-size: 90%; + background-repeat: no-repeat; + background-position: center; + +} +select:not([multiple="multiple"]):focus, input:not(.cbi-button):focus, .cbi-dropdown:focus { + background-color: var(--input-hover-bgcolor); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.22); + -webkit-box-shadow: 0 0 6px rgba(0, 0, 0, 0.22); + -moz-box-shadow: 0 0 6px rgba(0, 0, 0, 0.22) +} +.cbi-dropdown, select[multiple="multiple"] { + height: auto; +} +pre { + overflow: auto; +} +code { + font-size: var(--font-z); + font-size-adjust: .35; + padding: 1px 3px; + color: #101010; + border-radius: 0.25rem; + background: #ddd; +} +abbr { + cursor: help; + text-decoration: underline; + color: #005470; +} +hr { + margin: 1rem 0; + opacity: .1; + border-color: #eee; +} +footer { + text-align: right; + padding: 1rem 1rem 2rem 0; + color: var(--primary-title-color); + font-size: var(--font-x); + -webkit-backdrop-filter: var(--primary-ufilter); + backdrop-filter: var(--primary-ufilter); + background-color: rgba(var(--primary-rgbbody), var(--primary-rgbm-ts)); +} +footer > a { + color: var(--primary-title-color); + text-decoration: none +} +small { + font-size: 90%; + line-height: 1.42857143; + white-space: normal; +} +.main { + position: relative; + top: 0; + bottom: 0; + height: 100vh; + width: 100%; +} +.bar-primary { + bottom: 10px; + right: 0; + height: 50px; + z-index: 101; + box-shadow: none !important; + position: absolute; +} +.bar-primary .container-bar-right, .bar-primary .container-bar-left { + position: fixed; + height: 50px; + line-height: 50px; + text-align: center; + right: 0; + margin: 0; + padding: 0; + border-radius: .375rem; + background-color: #51c291; + background-color: rgba(var(--primary-rgbm), 0.9); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + background-image: var(--bgqs-image); + background-image: -webkit-linear-gradient(135deg,rgba(255,255,255,0.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.2) 50%,rgba(255,255,255,0.2) 75%,transparent 75%,transparent) !important; + transition: all .1s ease; + -moz-osx-font-smoothing: grayscale; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + transition: 0.1s ease-in-out +} +.container-bar-left a { + text-decoration: none; + width: 16.6% !important +} +.container-bar-right a { + text-decoration: none +} +.container-bar-left .labelbar:hover, .container-bar-right .labelbar:hover { + background-color: rgba(255,255,255,0.3); + border-radius: .375rem; + text-decoration:none +} +.container-bar-right .labelbar:hover::before, .container-bar-left .labelbar:hover::before { + color: #fff; + text-decoration: none +} + +.container-bar-right .labelbar::before,.container-bar-left .labelbar::before { + color: #f5f5f5f5; + font-size: 2rem !important; + text-decoration: none +} + +.container-bar-left .pdboy-closebar:before { + content: "\e931"; +} +.container-bar-right .pdboy-openbar:before { + content: "\e903"; +} +.container-bar-left .pdboy-gohome:before { + content: "\e90c"; +} +.container-bar-left .pdboy-gossr:before { + content: "\e917"; +} +.container-bar-left .pdboy-gonet:before { + content: "\e91e"; +} +.container-bar-left .pdboy-gouser:before { + content: "\e93d"; +} +.container-bar-left .pdboy-gocontrol:before { + content: "\e905"; +} +.container-bar-left .pdboy-goadvanced:before { + content: "\e91c"; +} +.container-bar-left .pdboy-gopoweroff:before { + content: "\e915"; + color: #ff5722 +} +.labelbar { + font-size: 2rem !important; + display: inline-block; + text-decoration: none; + text-align: center; + line-height: 30px; + padding: 10px; + height: 100%; + transition: 0.1s; + float: left +} +.main > .loading { + position: fixed; + top: 0; + opacity: 1; + visibility: visible; + align-items: center; + justify-content: center; + background-color: rgba(255,255,255,0); +} +.main-left { + float: left; + width: calc(0% + 17rem); + height: 100%; + -webkit-backdrop-filter: var(--primary-ufilter); + backdrop-filter: var(--primary-ufilter); + background-color: var(--bgqs-color); + background-image: var(--bgqs-image); + box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.14), 0 -2px 3px 1px rgba(0, 0, 0, 0.32), 0 0px 5px 2px rgba(0, 0, 0, 0.2); + overflow-x: auto; + position: fixed; + z-index: 100 +} +.main .main-left .sidenav-header { + text-align: center; + height: 4.8rem; + margin: 0 10px; + background-color: rgba(255, 255, 255, 0); + padding: 2rem; + background-clip: padding-box; + border-bottom: 1px solid #ddd +} +.main-right { + width: calc(100% - 17rem); + float: right; + height: 100vh; +} +.main-right > #maincontent { + position: relative; + z-index: 50 +} +.pull-left { + float: left; +} +.nowrap:not(.td) { + white-space: nowrap; +} +[disabled="disabled"] { + pointer-events: none; +} + +header.pd-primary { + -webkit-backdrop-filter: var(--primary-ufilter); + backdrop-filter: var(--primary-ufilter); + background-color: rgba(var(--primary-rgbm), var(--primary-rgbm-ts)); + background-image: -webkit-linear-gradient(135deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent) !important; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35); +} + +header .fill { + padding: 1rem 0; + border-bottom: 0 solid rgba(255,255,255,0.08) !important; + display: flex; +} +.pd-primary .fill .container { + height: 2.8rem; + padding: 0 1.25rem; + display: flex; + align-items: center; + width: 100%; + background-color: rgba(255, 255, 255, 0) !important +} +header .fill .container .flex1 { + flex: 1; +} +header .fill .container .flex1 .showSide { + display: none; +} +header .fill .container .flex1 .brand { + color: var(--menu-hover-color); + font-family: var(--font-sans); + font-weight: bold; + font-size: 1.5rem; + text-decoration: none; + cursor: default; + display: none; + +} +.label, [data-indicator] { + padding: .3rem .6rem; + text-decoration: none; + font-size: var(--font-z); + border-radius: 0.25rem; + background-color: #f8f8f8; + text-shadow: none; +} +.status > span { + text-shadow: none; + display: inline-block; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 2px 0 rgba(0,0,0,0.12); + margin: 0 0.6rem; +} +span[data-indicator="poll-status"] { + font-size: 0.7rem; + display: inline-block; + cursor: pointer; + line-height: 0.9; + border-radius: 1rem; + padding: 0.5rem 0rem!important; + background-repeat: no-repeat; + background-position: center; + color: transparent !important; + background-image: url(data:image/svg+xml;base64,PCEtLSBHZW5lcmF0ZWQgYnkgSWNvTW9vbi5pbyAtLT4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9Ijc2OCIgaGVpZ2h0PSI3NjgiIHZpZXdCb3g9IjAgMCA3NjggNzY4Ij4KPHRpdGxlPjwvdGl0bGU+CjxnIGlkPSJpY29tb29uLWlnbm9yZSI+CjwvZz4KPHBhdGggZD0iTTE1NC41NzMgMzAyLjk3M2MyMi40MDUtNjMuMzIzIDY4LjMwOS0xMTEuNTM3IDEyNC41MTgtMTM4LjM4czEyMi41NzItMzIuMTkzIDE4NS44NjQtOS43ODljMzYuMDU0IDEyLjc2OCA2Ny4wNjIgMzMuMDc1IDkwLjY4MyA1Ny4xNTFsODYuMDMyIDgwLjg2M2gtMTA1LjY3Yy0xNi43OCAwLTMwLjQgMTMuNjE5LTMwLjQgMzAuNHMxMy42MTkgMzAuNCAzMC40IDMwLjRoMTgyLjMwOGMwLjE1MiAwIDAuMjc0IDAgMC40MjUgMCA0LjE2NS0wLjAzMSA4LjE0Ny0wLjk0MyAxMS43MzQtMi40OTMgMy42MTctMS41NSA2Ljk2Mi0zLjgzIDkuODUtNi44NCAwLjM2NC0wLjM5NSAwLjczLTAuNzkgMS4wOTQtMS4xODYgMi4yOC0yLjY0NSA0LjA0My01LjU2NCA1LjI1OS04LjY2NHMxLjk0Ni02LjQxNSAyLjA5OC05LjkxYzAuMDMxLTAuNDU2IDAuMDMxLTAuODgxIDAuMDMxLTEuMzA3di0xODIuMzk5YzAtMTYuNzgxLTEzLjYxOS0zMC40LTMwLjQtMzAuNHMtMzAuNCAxMy42MTktMzAuNCAzMC40djExMi4xNDVsLTg4Ljk1LTgzLjU2OWMtMzAuMTU2LTMwLjcwNC02OS4wMzgtNTYuMDI3LTExMy43ODYtNzEuODY1LTc5LjEzLTI3Ljk5OC0xNjIuMTgzLTIxLjI1LTIzMi4zNDUgMTIuMjIxcy0xMjcuNjQ5IDkzLjgxNC0xNTUuNjQ3IDE3Mi45NDVjLTUuNjI0IDE1LjgwOCAyLjY3NSAzMy4xNjYgMTguNDgzIDM4Ljc5czMzLjE5Ni0yLjY3NSAzOC43OS0xOC41MTR6TTgwLjAwMSA1MTUuMTAybDg5LjgzMSA4NC40MmM1OC42NDEgNTguNzAyIDEzNi40OTUgODguNDAzIDIxNC4yNTggODguNDMzczE1NS42MTctMjkuNjQgMjE0Ljk4Ny04OC45OGMzMy41NjEtMzMuNTYxIDU3Ljc5LTczLjE3MiA3MS43NzQtMTEzLjg0OCA1LjQ3Mi0xNS44NjgtMi45NzktMzMuMTY2LTE4Ljg3OC0zOC42MzhzLTMzLjE2NiAyLjk3OS0zOC42MzggMTguODc4Yy0xMC45NzUgMzEuOTItMzAuMTg3IDYzLjU2Ni01Ny4yNDIgOTAuNjIyLTQ3LjUxNSA0Ny40ODQtMTA5LjcxMyA3MS4xOTYtMTcxLjk3MSA3MS4xOTZzLTEyNC40NTctMjMuNzczLTE3Mi42NC03MS45MjZsLTg1LjEyLTgwLjA0MmgxMDUuNjRjMTYuNzgxIDAgMzAuNC0xMy42MTkgMzAuNC0zMC40cy0xMy42MTktMzAuNC0zMC40LTMwLjRoLTE4Mi4zMDhjLTAuMTUxIDAtMC4yNzQgMC0wLjQyNSAwLTQuMTY1IDAuMDMxLTguMTQ3IDAuOTQzLTExLjczNCAyLjQ5My0zLjYxNyAxLjU1LTYuOTYxIDMuODMtOS44NSA2Ljg0LTAuMzY0IDAuMzk1LTAuNzMgMC43OTEtMS4wOTQgMS4xODYtMi4yOCAyLjY0NC00LjA0MyA1LjU2My01LjI1OSA4LjY2NHMtMS45NDUgNi40MTUtMi4wOTggOS45MWMtMC4wMzEgMC40NTYtMC4wMzEgMC44ODEtMC4wMzEgMS4zMDd2MTgyLjM5OWMwIDE2Ljc4IDEzLjYxOSAzMC40IDMwLjQgMzAuNHMzMC40LTEzLjYxOSAzMC40LTMwLjR6Ij48L3BhdGg+Cjwvc3ZnPgo=); + -webkit-appearance: none; + -moz-appearance: none; + transition: all .3s; +} + + +.pdboy-qlogout:hover, +.showSide:hover { + background-color: var(--menu-item-hover-bgcolor); + border-radius: 0.25rem; + text-decoration: none +} +.pdboy-qlogout { + line-height: 1.1; + font-size: 1.4rem; + padding: 0.9rem; + display: inline-block; +} +span[data-indicator="uci-changes"] { + cursor: pointer; +} +.pdboy-qlogout:before { + font-size: 1.5rem !important; + content: "\e935"; + color: red +} + + +.showWord:before { + + padding: 1rem; + font-size: 1.1rem; + line-height: 1.2; + content: "\e925"; + color: var(--menu-color); + text-decoration: none +} +.showWord:after { + padding: 1rem; + font-size: 1.1rem; + line-height: 1.2; + content: "\e927"; + color: var(--menu-color); + text-decoration: none +} +.showWord { + font-family: var(--font-sans); + color: var(--menu-color); + font-size: 1.1rem!important; + line-height: 1.2; + flex: auto; + cursor: pointer; + width: calc(100% - 20rem); + display: inline-block; + text-decoration: none!important; + vertical-align: text-bottom; +} + +.showWord:hover,.showWord:focus { +text-decoration: none!important; +} + +.showWord::before, +.showWord::after, +[class^="pdboy-"]:before, [class*=" pdboy-"]:before, [class^="pdboy-"]:after, [class*=" pdboy-"]:after { + font-family: 'sirpdboy-kucat' !important; + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; + text-transform: none !important; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} +.danger { + background-color: #d9534f!important; + color: #eee; +} +.warning { + background-color: #b98413 !important; + margin: 0 0 0.5rem 0; + color: #eee; +} +.success { + background-color: #1a8361!important; + color: #eee; + width: 12rem!important; +} +#log_textarea { + box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.16), 0 0 1px 0 rgba(0, 0, 0, 0.12); +} +[data-indicator]:not([data-style="inactive"]) { + background-color: #f8f8f8 !important; + background-size: 23px 23px; +} + +[data-indicator]:not([data-style="active"]) { + background-color: #9f9f9f !important; + background-size: 20px 20px; +background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzY4IiBoZWlnaHQ9Ijc2OCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiPgogPHRpdGxlLz4KCiA8Zz4KICA8dGl0bGU+TGF5ZXIgMTwvdGl0bGU+CiAgPGcgaWQ9Imljb21vb24taWdub3JlIi8+CiAgPHBhdGggZmlsbD0iIzY2NjY2NiIgaWQ9InN2Z18xIiBkPSJtMTU0LjU3MywzMDIuOTczYzIyLjQwNSwtNjMuMzIzIDY4LjMwOSwtMTExLjUzNyAxMjQuNTE4LC0xMzguMzhzMTIyLjU3MiwtMzIuMTkzIDE4NS44NjQsLTkuNzg5YzM2LjA1NCwxMi43NjggNjcuMDYyLDMzLjA3NSA5MC42ODMsNTcuMTUxbDg2LjAzMiw4MC44NjNsLTEwNS42NywwYy0xNi43OCwwIC0zMC40LDEzLjYxOSAtMzAuNCwzMC40czEzLjYxOSwzMC40IDMwLjQsMzAuNGwxODIuMzA4LDBjMC4xNTIsMCAwLjI3NCwwIDAuNDI1LDBjNC4xNjUsLTAuMDMxIDguMTQ3LC0wLjk0MyAxMS43MzQsLTIuNDkzYzMuNjE3LC0xLjU1IDYuOTYyLC0zLjgzIDkuODUsLTYuODRjMC4zNjQsLTAuMzk1IDAuNzMsLTAuNzkgMS4wOTQsLTEuMTg2YzIuMjgsLTIuNjQ1IDQuMDQzLC01LjU2NCA1LjI1OSwtOC42NjRzMS45NDYsLTYuNDE1IDIuMDk4LC05LjkxYzAuMDMxLC0wLjQ1NiAwLjAzMSwtMC44ODEgMC4wMzEsLTEuMzA3bDAsLTE4Mi4zOTljMCwtMTYuNzgxIC0xMy42MTksLTMwLjQgLTMwLjQsLTMwLjRzLTMwLjQsMTMuNjE5IC0zMC40LDMwLjRsMCwxMTIuMTQ1bC04OC45NSwtODMuNTY5Yy0zMC4xNTYsLTMwLjcwNCAtNjkuMDM4LC01Ni4wMjcgLTExMy43ODYsLTcxLjg2NWMtNzkuMTMsLTI3Ljk5OCAtMTYyLjE4MywtMjEuMjUgLTIzMi4zNDUsMTIuMjIxcy0xMjcuNjQ5LDkzLjgxNCAtMTU1LjY0NywxNzIuOTQ1Yy01LjYyNCwxNS44MDggMi42NzUsMzMuMTY2IDE4LjQ4MywzOC43OXMzMy4xOTYsLTIuNjc1IDM4Ljc5LC0xOC41MTRsMC4wMjksMC4wMDF6bS03NC41NzIsMjEyLjEyOWw4OS44MzEsODQuNDJjNTguNjQxLDU4LjcwMiAxMzYuNDk1LDg4LjQwMyAyMTQuMjU4LDg4LjQzM3MxNTUuNjE3LC0yOS42NCAyMTQuOTg3LC04OC45OGMzMy41NjEsLTMzLjU2MSA1Ny43OSwtNzMuMTcyIDcxLjc3NCwtMTEzLjg0OGM1LjQ3MiwtMTUuODY4IC0yLjk3OSwtMzMuMTY2IC0xOC44NzgsLTM4LjYzOHMtMzMuMTY2LDIuOTc5IC0zOC42MzgsMTguODc4Yy0xMC45NzUsMzEuOTIgLTMwLjE4Nyw2My41NjYgLTU3LjI0Miw5MC42MjJjLTQ3LjUxNSw0Ny40ODQgLTEwOS43MTMsNzEuMTk2IC0xNzEuOTcxLDcxLjE5NnMtMTI0LjQ1NywtMjMuNzczIC0xNzIuNjQsLTcxLjkyNmwtODUuMTIsLTgwLjA0MmwxMDUuNjQsMGMxNi43ODEsMCAzMC40LC0xMy42MTkgMzAuNCwtMzAuNHMtMTMuNjE5LC0zMC40IC0zMC40LC0zMC40bC0xODIuMzA4LDBjLTAuMTUxLDAgLTAuMjc0LDAgLTAuNDI1LDBjLTQuMTY1LDAuMDMxIC04LjE0NywwLjk0MyAtMTEuNzM0LDIuNDkzYy0zLjYxNywxLjU1IC02Ljk2MSwzLjgzIC05Ljg1LDYuODRjLTAuMzY0LDAuMzk1IC0wLjczLDAuNzkxIC0xLjA5NCwxLjE4NmMtMi4yOCwyLjY0NCAtNC4wNDMsNS41NjMgLTUuMjU5LDguNjY0cy0xLjk0NSw2LjQxNSAtMi4wOTgsOS45MWMtMC4wMzEsMC40NTYgLTAuMDMxLDAuODgxIC0wLjAzMSwxLjMwN2wwLDE4Mi4zOTljMCwxNi43OCAxMy42MTksMzAuNCAzMC40LDMwLjRzMzAuNCwtMTMuNjE5IDMwLjQsLTMwLjRsLTAuMDAyLC0xMTIuMTE0eiIvPgogPC9nPgo8L3N2Zz4="); +} + +.error { + color: #f00; +} +.alert, .alert-message { + padding: 1rem; + border: 0; + font-weight: normal; + font-style: normal; + line-height: 1.6em; + font-family: inherit; + min-width: inherit; + overflow: unset; + border-radius: 0.25rem; + background-color: rgba(var(--primary-rgbbody), var(--primary-rgbm-ts)); + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12) +} +.alert-message > * { + margin: .5rem 0; +} +.alert-message > h4 { + margin: 0.5rem; + color: red; + padding: 0.5rem 1rem; + font-weight: bold; +} + +.errorbox { + color: #f8f8f8; + background-color: #f0ad4e; + border-color: #eea236 +} +.container .alert, .container .alert-message { + margin-top: 1rem; +} +/* menu */ + +.main .main-left .nav { + font-size: var(--font-d); + margin-top: .5rem +} +.slide-menu { + overflow: hidden +} +.main .main-left .nav>li a { + display: block; + font-size: var(--font-d); + color: var(--menu-color) +} +.main .main-left .nav>li>a:first-child { + display: block; + font-size: var(--font-d); + color: var(--menu-color); + padding: 1rem 0rem 1rem 3rem; + margin-bottom: 0.1rem; + text-decoration: none; + cursor: default; + transition: all .2s; + position: relative; +} +.main .main-left .nav>li { + padding: 0rem; + cursor: pointer +} +.main .main-left .nav .slide ul { + display: none; + list-style: dotted +} +.main .main-left .nav .slide.active ul { + display: block; +} +.main .main-left .nav>li>a { + display: block; + padding: 1rem 0rem 1rem 3rem; + margin-bottom: .1rem; + text-decoration: none; + font-size: var(--font-d); + transition: all .1s; + position: relative +} +.main .main-left .nav>li>a:first-child.active::before { + color: var(--menu-hover-color); +} +.main .main-left .nav>li>a:first-child::before { + font-family: 'sirpdboy-kucat' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + position: absolute; + left: .8rem; + content: "\e91a"; + color: var(--menu-color) +} +.main .main-left .nav>.slide>a::before, .main .main-left .nav>li>a::before { + font-family: 'sirpdboy-kucat' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + position: absolute; + left: .8rem; + content: "\e909"; + color: var(--menu-color) +} +.main .main-left .nav>.slide>a::after, .main .main-left>.nav>li>a::after { + position: absolute; + right: .6rem; + top: 1rem; + font-family: 'sirpdboy-kucat' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + color: var(--menu-color); + content: '\e20b'; + transition: all .1s ease; + -moz-osx-font-smoothing: grayscale; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + transition: all .1s +} +.main .main-left .nav>li>a:hover a::after, .main .main-left>.nav>li>a.active a::after { + cursor: pointer; + color: var(--menu-color); + width: 100% +} +.main>.main-left .nav>.slide>a.active::before, .main .main-left .nav>li>a:hover::after, .main .main-left .nav>li>a.active::after { + cursor: pointer; + color: var(--menu-hover-color) +} +.main .main-left .nav>li>a.active::after { + -ms-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + transform: rotate(180deg) +} + +.main .main-left .nav>li>a:hover, .main .main-left .nav>li>a.active { + cursor: pointer; + background-color: var(--menu-item-hover-bgcolor); + color: var(--menu-hover-color) +} +.main .main-left .nav>li>a:hover a, .main .main-left .nav>li>a.active a { + color: var(--menu-hover-color) +} +.main .main-left .nav>li>a:hover::before, .main .main-left .nav>li>a.active::before { + color: var(--menu-hover-color) +} +.main .main-left .nav>li>a[data-title=Status]:before { + content: "\e90c" +} +.main .main-left .nav>li>a[data-title=System]:before { + content: "\e919" +} +.main .main-left .nav>li>a[data-title=Services]:before { + content: "\e92a" +} +.main .main-left .nav>li>a[data-title=NAS]:before { + content: "\e907" +} +.main .main-left .nav>li>a[data-title=VPN]:before { + content: "\e917" +} +.main .main-left .nav>li>a[data-title=Network]:before { + content: "\e90a" +} +.main .main-left .nav>li>a[data-title=Bandwidth_Monitor]:before { + content: "\e913" +} +.main .main-left .nav>li>a[data-title=iStore]:before { + content: "\e908" +} +.main .main-left .nav>li>a[data-title=Inital_Setup]:before { + content: "\e93a" +} +.main .main-left .nav>li>a[data-title=Wizard]:before { + content: "\e90b" +} +.main .main-left .nav>li>a[data-title=Docker]:before { + content: "\e902" +} +.main .main-left .nav>li>a[data-title=Statistics]:before { + content: "\e918" +} + .main .main-left .nav>li>a[data -title=control]:before, + .main .main-left .nav>li>a[data -title=Control]:before { + content: "\e91a" +} +.main .main-left .nav>li>a[data-title=Asterisk]:before { + content: "\e91c" +} +.main .main-left .nav>li>a[data-title=Logout]:before, +.main .main-left .nav>li>a[data-title=Log_out]:before { + content: "\e935" +} +.main .main-left .nav>li>a[data-title=Wizard]:after, +.main .main-left .nav>li>a[data-title=iStore]:after, +.main .main-left .nav>li>a[data-title=Logout]:after , +.main .main-left .nav>li>a[data-title=Log_out]:after{ + content: "" +} + +.main .main-left .nav>li [data-title=Logout], .main .main-left .nav>li>a[data-title=Logout], +.main .main-left .nav>li [data-title=Log_out], .main .main-left .nav>li>a[data-title=Log_out] { + display: none +} +.main .main-left .nav>li .slide-menu li a { + position: relative; + margin: 0rem 0rem .1rem 3rem; + padding: .7rem 0; + text-decoration: none; + white-space: nowrap; + color: var(--menu-color); + transition: all .1s; + border-left: 4px solid rgba(255, 255, 255, 0); + font-size: var(--font-z); +} +.main .main-left .nav .slide .slide-menu .active a { + color: var(--menu-hover-color); + font-size: var(--font-d) +} +.main .main-left .nav .slide .slide-menu > li.active { + color: var(--menu-hover-color); + background-color: var(--menu-item-hover-bgcolor); + border-left: 4px solid var(--primary-solid) +} +.main .main-left .nav .slide .slide-menu li:hover { + background-color: var(--menu-item-hover-bgcolor); +} +.main .main-left .nav .slide .slide-menu li:active a { + color: var(--menu-hover-color) +} +.main .main-left .nav .slide .slide-menu li:hover a { + color: var(--menu-hover-color); + font-size: var(--font-d) +} +.main .main-left .sidenav-header .brand { + color: var(--menu-hover-color); + font-family: var(--font-sans); + font-weight: bold; + font-size: 1.5rem; + padding-left: 0px; + text-decoration: none; + /*letter-spacing: 3px;*/ + font-weight: bolder; + cursor: default; + vertical-align: text-bottom; + white-space: nowrap; + text-align: center; + align-items: center; + justify-content: center; +} +#maincontent > .container { + background-color: rgba(var(--primary-rgbbody), var(--primary-rgbm-ts)); + -webkit-backdrop-filter: var(--primary-ufilter); + backdrop-filter: var(--primary-ufilter); + padding: 1rem 1.25rem 3rem 1.25rem; +} +ul { + line-height: inherit; +} +li { + list-style-type: none; +} +h1 { + font-size: var(--font-d); + padding-bottom: 10px +} +h2 { + font-size: var(--font-d); + padding: 0.5rem 1.5rem 0.2rem; + font-weight: bold; + text-transform: capitalize; + color: var(--primary-title-color) +} +h3 { + font-size: var(--font-d); + display: block; + margin: 1rem 0; + color: var(--primary-title-color); + font-weight: bold; + letter-spacing: .1rem; + background-color: rgba(255, 255, 255, 0) +} +h4 { + margin: 0; + padding: 0.75rem 1.25rem; + font-weight: 600; + font-size: var(--font-z); + color: var(--primary-title-color); + padding-bottom: 10px; +} + +h4>span { + font-size: 85%; +} +h5 { + font-size: var(--font-z); + color: var(--primary-title-color); + margin: 2rem 0 0 0; + padding-bottom: 10px; +} +#cbi-dropbear h2, #cbi-dropbear .cbi-map-descr, #cbi-dropbear .cbi-map-descr abbr, #cbi-rc h2, #cbi-rc .cbi-map-descr, #cbi-distfeedconf h2, #cbi-distfeedconf .cbi-map-descr, #cbi-customfeedconf h2, #cbi-customfeedconf .cbi-map-descr, #cbi-download h2, #cbi-filelist h2 { + font-weight: bold; +} +.cbi-section, .cbi-section-error, #iptables, .Firewall form, #cbi-network > .cbi-section-node, #cbi-wireless > .cbi-section-node, #cbi-wireless > #wifi_assoclist_table, [data-tab-title], [data-page="admin-system-opkg"] #maincontent > .container { + font-family: inherit; + font-weight: normal; + font-style: normal; + line-height: normal; + min-width: inherit; + line-height: 1; + overflow-x: auto; + overflow-y: hidden; + border: 0; + border-radius: 0; +} +.cbi-section { + padding: 0; + border: 0px solid rgba(0, 0, 0, 0); + box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.12); +} +.cbi-modal .cbi-section, .cbi-section .cbi-section { + padding: 0; + box-shadow: none; +} +.cbi-modal .cbi-tabmenu { + margin-left: 0; +} +.cbi-map-descr, .cbi-section-descr { + font-size: small; + line-height: 1.42857143; + margin: 0 1.5rem 1rem 1.5rem; + color: #999; +} +.cbi-section-table-descr { + display:none; +} +fieldset { + margin: 0; + padding: 0; + font-weight: normal; + font-style: normal; + line-height: 1; + font-family: inherit; + min-width: inherit; + overflow-x: auto; + overflow-y: hidden +} +.cbi-section > legend { + display: none !important; +} +.cbi-map-descr+fieldset { + margin-top: 1rem +} +fieldset>fieldset { + margin: 0; + padding: 0; + border: none; + box-shadow: none +} +.panel-title { + padding: 1rem 1.5rem; +} +.cbi-section > h3:first-child, .panel-title { + width: 100%; + display: block; + padding: 20px; + color: var(--primary-title-color); + font-size: var(--font-d); + font-weight: bold; + margin: 0rem; + letter-spacing: .1rem +} +.cbi-section > h4:first-child, .cbi-section > p:first-child, [data-tab-title] > h3:first-child, [data-tab-title] > h4:first-child, [data-tab-title] > p:first-child { + padding-top: 1rem; +} +.table { + position: relative; + display: table; +} +table, .table { + /*overflow-y: hidden;*/ + border-spacing: 0; + border-collapse: collapse; + border: 0px solid #eee; + font-size: var(--font-x); + width: 100%; +} +table > tbody > tr > td, table > tbody > tr > th, table > tfoot > tr > td, table > tfoot > tr > th, table > thead > tr > td, table > thead > tr > th, .table > .tbody > .tr > .td, .table > .tbody > .tr > .th, .table > .tfoot > .tr > .td, .table > .tfoot > .tr > .th, .table > .thead > .tr > .td, .table > .thead > .tr > .th { + font-size: var(--font-x); + padding: 1.2rem 1.5rem; + line-height: 1.2rem; + letter-spacing: 1px; + color: var(--body-color); + white-space: nowrap; +} +table .tr > .td.cbi-value-field, table .tr > .th.cbi-section-table-cell { + font-size: var(--font-x); + padding: 0.3rem 0.8rem; + letter-spacing: 1px; + display: table-cell!important; + color: var(--body-color); + white-space: nowrap; +} +.cbi-section-table-row .td { + text-align: center; +} + +table .tr > .td.cbi-value-field>[id*="ifc-description"] { + text-align: left ; + font-weight: normal!important +} +table>tbody>tr:first-child>td, table>tfoot>tr:first-child>td, table>thead>tr:first-child>td { + border-top: 0px solid #eee +} +.container > .cbi-section:first-of-type > .table[width="100%"] > .tr > .td { + padding: .6rem; +} +.cbi-section-table-cell { + line-height: 1.1; + align-self: flex-end; + flex: 1 1 auto; +} +#cbi-wireless .td, #cbi-network .tr:first-child > .td, .table[width="100%"] > .tr:first-child > .td, [data-page="admin-network-diagnostics"] .tr > .td, .tr.table-titles > .th, .tr.cbi-section-table-titles > .th { + border-top: 0 !important; + line-height: 1; +} + [data-page="admin-network-diagnostics"] .tr > .td { + white-space: normal; + } +[data-page="admin-network-diagnostics"] .cbi-dropdown > ul{ + overflow-y: hidden; + } +.table[width="100%"] > .tr:first-child > .td { + margin: auto 0; +} +.cbi-section-table-row { + text-align: center +} + +/* button style */ + +.btn, button, select, input, .cbi-dropdown { + height: 2.4rem; + outline: 0; + background-color: var(--inputbg-color); + color: var(--inputtext-color); + border: 1px solid var(--inputborder-color); + border-radius: 0.25rem; + transition: box-shadow .15s ease; +} +.btn, button, .cbi-button, .item::after { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + text-transform: uppercase; + line-height: 1; + display: inline-block; + padding: 0.6rem 1rem; + -ms-touch-action: manipulation; + touch-action: manipulation; + vertical-align: middle; + text-align: center; + white-space: nowrap; + font-size: var(--font-z); + color: #f8f8f8; + background-color: #008b89; + background-image: var(--bgqs-image); + box-shadow: 0 0.3rem 0.8rem rgba(0,0,0,0.22); + transform: translate(1) translate(0,0); + transition: transform 225ms,box-shadow 225ms; + border-radius: 0.25rem; +} +.cbi-section-table-row:last-child { + margin-bottom: 0; +} + +.cbi-value-field .cbi-dropdown, .cbi-value-field .cbi-input-select, .cbi-value input[type="text"], .cbi-value input[type="password"] { + min-width: 8rem; + width: 18rem; + padding: 0 10px 0 10px; +} +.cbi-section-table-row > .cbi-value-field .cbi-dropdown, .cbi-section-table-row > .cbi-value-field .cbi-input-select, .cbi-section-table-row > .cbi-value-field .cbi-input-text, .cbi-section-table-row > .cbi-value-field .cbi-input-password { + width: 8rem; +} +.cbi-section-table-row > .cbi-value-field [data-dynlist] > input, .cbi-section-table-row > .cbi-value-field input.cbi-input-password { + width: calc(100% - 1.5rem); +} + +.cbi-tabcontainer>.cbi-value:nth-of-type(4n+2), .cbi-map>.cbi-section .cbi-value:nth-of-type(4n+2), fieldset>table>tbody>tr:nth-of-type(4n+2), table>tbody>tr:nth-of-type(4n+2), div>.table>.tr:nth-of-type(4n+2) { + background-color: rgba(var(--primary-rgbm), var(--primary-rgbs-ts)); + background-image: var(--bgqs-image); +} +.cbi-tabcontainer>.cbi-value:nth-of-type(4n), .cbi-map>.cbi-section .cbi-value:nth-of-type(4n), fieldset>table>tbody>tr:nth-of-type(4n), table>tbody>tr:nth-of-type(4n), div>.table>.tr:nth-of-type(4n) { + background-color: rgba(var(--primary-rgbs), var(--primary-rgbs-ts)); + background-image: var(--bgqs-image); +} +.cbi-tabcontainer>.cbi-value:nth-of-type(4n+2):hover, .cbi-map>.cbi-section .cbi-value:nth-of-type(4n+2):hover, fieldset>table>tbody>tr:nth-of-type(4n+2):hover, table>tbody>tr:nth-of-type(4n+2):hover, div>.table>.tr:nth-of-type(4n+2):hover { + background-color: rgba(50,50,50,0.05); + background-image: var(--bgqs-image); + +} +.cbi-tabcontainer>.cbi-value:nth-of-type(4n):hover, .cbi-map>.cbi-section .cbi-value:nth-of-type(4n):hover, fieldset>table>tbody>tr:nth-of-type(4n):hover, table>tbody>tr:nth-of-type(4n):hover, div>.table>.tr:nth-of-type(4n):hover { + background-color: rgba(50,50,50,0.05); + background-image: var(--bgqs-image); +} + +.cbi-tabcontainer>.cbi-value:nth-of-type(2n+1):hover,.cbi-map>.cbi-section .cbi-value:nth-of-type(2n+1):hover,fieldset>table>tbody>tr:nth-of-type(2n+1):hover,table>tbody>tr:nth-of-type(2n+1):hover,div>.table>.tr:nth-of-type(2n+1):hover { + background-color: rgba(50,50,50,0.05); +} +/* fix multiple table */ +table table, .table .table, .cbi-value-field table, .cbi-value-field .table, td > table > tbody > tr > td, .td > .table > .tbody > .tr > .td, .cbi-value-field > table > tbody > tr > td, .cbi-value-field > .table > .tbody > .tr > .td { + border: 0; +} +.cbi-button-up { + font-size: 1.2rem; + display: inline-block; + cursor: pointer; +background-image: url("data:image/svg+xml;base64,PCEtLSBHZW5lcmF0ZWQgYnkgSWNvTW9vbi5pbyAtLT4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9Ijc2OCIgaGVpZ2h0PSI3NjgiIHZpZXdCb3g9IjAgMCA3NjggNzY4Ij4KPHRpdGxlPjwvdGl0bGU+CjxnIGlkPSJpY29tb29uLWlnbm9yZSI+CjwvZz4KPHBhdGggZD0iTTU5OC42MyA0NTcuMzc1bC0xOTItMTkyYy0xMi41MTItMTIuNTEyLTMyLjc2OC0xMi41MTItNDUuMjQ4IDBsLTE5MiAxOTJjLTEyLjUxMiAxMi41MTItMTIuNTEyIDMyLjc2OCAwIDQ1LjI0OHMzMi43NjggMTIuNTEyIDQ1LjI0OCAwbDE2OS4zNzYtMTY5LjM3NiAxNjkuMzc2IDE2OS4zNzZjMTIuNTEyIDEyLjUxMiAzMi43NjggMTIuNTEyIDQ1LjI0OCAwczEyLjUxMi0zMi43NjggMC00NS4yNDh6Ij48L3BhdGg+Cjwvc3ZnPgo="); + background-repeat: no-repeat; + background-position: center; + color: transparent !important; + -webkit-appearance: none; + -moz-appearance: none; + background-size: 25px 30px; +} +.cbi-button-down { + font-size: 1.2rem; + display: inline-block; + cursor: pointer; +background-image: url("data:image/svg+xml;base64,PCEtLSBHZW5lcmF0ZWQgYnkgSWNvTW9vbi5pbyAtLT4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9Ijc2OCIgaGVpZ2h0PSI3NjgiIHZpZXdCb3g9IjAgMCA3NjggNzY4Ij4KPHRpdGxlPjwvdGl0bGU+CjxnIGlkPSJpY29tb29uLWlnbm9yZSI+CjwvZz4KPHBhdGggZD0iTTE2OS4zNyAzMTAuNjI0bDE5MiAxOTJjMTIuNTEyIDEyLjUxMiAzMi43NjggMTIuNTEyIDQ1LjI0OCAwbDE5Mi0xOTJjMTIuNTEyLTEyLjUxMiAxMi41MTItMzIuNzY4IDAtNDUuMjQ4cy0zMi43NjgtMTIuNTEyLTQ1LjI0OCAwbC0xNjkuMzc2IDE2OS4zNzYtMTY5LjM3Ni0xNjkuMzc2Yy0xMi41MTItMTIuNTEyLTMyLjc2OC0xMi41MTItNDUuMjQ4IDBzLTEyLjUxMiAzMi43NjggMCA0NS4yNDh6Ij48L3BhdGg+Cjwvc3ZnPgo="); + background-repeat: no-repeat; + background-position: center; + color: transparent !important; + -webkit-appearance: none; + -moz-appearance: none; + background-size: 25px 30px; +} + +.cbi-button-up, .cbi-button-down, .cbi-value-helpicon, .showSide, .main > .loading > span { + font-family: 'sirpdboy-kucat' !important; + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; + text-transform: none !important; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} +.td.cbi-section-actions>*>*, .td.cbi-section-actions>*>form>* { + margin: 0 5px; + display: flex; + align-items: center; +} +.node-admin-network-network #view .td.cbi-section-actions > * , +.node-admin-network-network #view .td.cbi-section-actions>*>* { + display: unset; +} + +.cbi-rowstyle-2 .cbi-button-up, .cbi-rowstyle-2 .cbi-button-down, body:not(.Interfaces) .cbi-rowstyle-2:first-child { + background-color: rgba(var(--primary-rgbm), 0.5) !important; +} +.cbi-rowstyle-1 .cbi-button-up, .cbi-rowstyle-1 .cbi-button-down, body:not(.Interfaces) .cbi-rowstyle-1:first-child { + background-color: #ddd !important; +} + +.btn:hover, .btn:focus, .btn:active, .cbi-button:hover, .cbi-button:focus, .cbi-button:active, .item:hover::after, .item:focus::after, .item:active::after { + outline: 0; + text-decoration: none +} +.btn:focus, .cbi-button:focus, .item:hover::after, .item:focus::after { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + -webkit-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); +} +button:hover, .btn:hover, .cbi-button:hover ,.item:hover{ + transform: scale(1.05) translate(0, -0.15rem); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35); + -webkit-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35); + -moz-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35); + +} +.btn:active, .cbi-button:active, button:active,.item:active { + transform: scale(1) translate(0, 0.15rem); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); + -webkit-box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); +} +.btn:disabled, .cbi-button:disabled { + cursor: not-allowed; + pointer-events: none; + opacity: .5; + box-shadow: none; +} + +.primary { + line-height: 2; +} +.cbi-button:not(select) { + appearance: none !important; +} +/* gray */ +.alert-message [class="btn"], .modal div[class="btn"], .cbi-button-find, .cbi-button-link, .cbi-button-neutral, .cbi-button[name="zero"], .cbi-button[name="restart"], .cbi-button[onclick="hide_empty(this)"] { + font-weight: bold; + border: thin solid #1b4f0e; + background-color: #25770b; +} +/* dark blue */ +.btn.primary, .cbi-page-actions .cbi-button-save, .cbi-page-actions .cbi-button-apply + .cbi-button-save, .cbi-button-add, .cbi-button-save, .cbi-button-positive, .cbi-button-link, .cbi-button[value="Enable"], .cbi-button[value="Scan"], .cbi-button[value^="Back"], .cbi-button-neutral[onclick="handleConfig(event)"] { + font-weight: normal; + border: thin solid #2e6da4; + background-color: #337ab7; +} +/* light blue */ +.cbi-page-actions .cbi-button-apply, .cbi-section-actions .cbi-button-edit, .cbi-button-edit, .cbi-button-apply, .cbi-button-reload, .cbi-button-action, .cbi-button[value="Submit"], .cbi-button[value="Upload"], .cbi-button[value$="Apply"], .cbi-button[onclick="addKey(event)"] { + font-weight: normal; + border-color: rgba(var(--primary-rgbm), 1); + border: thin solid rgba(var(--primary-rgbm), 1); + background-color: rgba(var(--primary-rgbm), 0.9); +} + + + +/* red */ +.btn.danger, .cbi-section-remove > .cbi-button, .cbi-button-remove, .cbi-button-reset, .cbi-button-negative, .cbi-button[value="Stop"], .cbi-button[value="Kill"], .cbi-button[onclick="reboot(this)"], .cbi-button-neutral[value="Restart"] { + font-weight: normal; + border: thin solid #d43f3a; + background-color: #d9534f; +} +/* yellow */ +.btn[value="Dismiss"], .cbi-button[value="Terminate"], .cbi-button[value="Reset"], .cbi-button[value="Disabled"], .cbi-button[onclick^="iface_reconnect"], .cbi-button[onclick="handleReset(event)"], .cbi-button-neutral[value="Disable"] { + font-weight: normal; + border: thin solid #eea236; + background-color: #f0ad4e; +} +/* green */ +.cbi-button-success, .cbi-button-download, .cbi-button[name="backup"], .cbi-button[value="Download"], .cbi-button[value="Save mtdblock"] { + font-weight: normal; + border: thin solid #4cae4c; + background-color: #5cb85c; +} +.btn, .cbi-button, .item::after { + color: #f8f8f8; +} + +.cbi-page-actions .cbi-button-link:first-child { + float: left; +} +.a-to-btn { + text-decoration: none; +} +.cbi-value-field .cbi-button-add { + font-weight: bold; + margin: 9px 0 4px 3px; + padding: 1px 6px; +} +.tabs li[class~="active"] { + border-right: 0.18751rem solid var(--primary-solid)!important; + border-left: 0.18751rem solid var(--primary-solid)!important; + /*transform: scale(1) translate(0, 0.15rem); */ + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4); + background-color: rgba(var(--primary-rgbm), 0.3); +} +.tabs > li:hover { + background: rgba(var(--primary-rgbm), 0.3); + transform: scale(1.05) translate(0, -0.15rem); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35); +} +.tabs li[class~="active"] a { + color: var(--primary-title-color) +} +.tabs li:hover a { + color: var(--primary-title-color) +} +.tabs li a { + padding: 0.3rem 1rem; + text-decoration: none; + color: var(--primarytextcolor) +} +.tabs, .cbi-tabmenu { + border: none; + color: var(--primarytextcolor); + border-right: 0.18751rem solid rgba(255, 255, 255, 0); + letter-spacing: 1px; + padding: 0.3rem 1rem; + margin-bottom: 0.5rem; + overflow-x: auto; +} +.tabs > li, .cbi-tabmenu li { + display: inline-block; + font-size: var(--font-z); + border-left: 0.18751rem solid rgba(255, 255, 255, 0)!important; + border-right: 0.18751rem solid rgba(255, 255, 255, 0)!important; + border-radius: 0.25rem; + padding: 0.5rem 0rem; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + transform: translate(1) translate(0, 0); + transition: transform 225ms, box-shadow 225ms; + background: var(--menu-item-titlebg-color); + margin: 0.2rem 0rem 0.2rem 0.4rem !important; +} +.cbi-tabmenu li a { + text-decoration: none; + padding: 0.7rem 1rem; + color: var(--primarytextcolor); +} +.cbi-tabmenu > li:hover { + color: var(--primary-title-color); + background: rgba(var(--primary-rgbm), 0.3); + transform: scale(1.05) translate(0, -0.15rem); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35); + margin-bottom: 0 +} +.cbi-tabmenu li:hover a { + color: var(--primary-title-color) +} +.cbi-tabmenu > li[class~="cbi-tab"] { + border-right: 0.18751rem solid var(--primary-solid) !important; + border-left: 0.18751rem solid var(--primary-solid) !important; + /*transform: scale(1) translate(0, 0.15rem); */ + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + background-color: rgba(var(--primary-rgbm), 0.3); +} +[data-tab-title] { + overflow: hidden; + height: 0; + opacity: 0; + margin: 0; +} +[data-tab-active="true"] { + overflow: auto; + height: auto; + opacity: 1; + margin: inherit !important; + display: block; + border-radius: 0; +} +.cbi-section-node-tabbed { + margin-top: 0; + padding: 0; + border-top: 0; +} +.cbi-value-field, .cbi-value-description { + line-height: 1.25; + display: table-cell; +} +.cbi-value-description { + font-size: small; + + padding: 0 0 1rem 0 ; + opacity: .7; +} +.cbi-value-title { + display: table-cell; + float: left; + width: 23rem; + padding-right: 2rem; + text-align: right; + word-wrap: break-word; +} +.cbi-value { + display: inline-block; + width: 100%; + padding: 0.2rem 0.8rem; + line-height: 2.4rem; +} +.cbi-value.cbi-value-last { + padding: 0 0.8rem; +} +.cbi-value:first-child { + padding-top: 1rem; +} +.cbi-value:last-child { + border-bottom: 1rem; +} +.cbi-value-field { + display: table-cell; +} +.cbi-value ul { + line-height: 1.25; +} +.cbi-value strong { + font-weight: normal; +} +#cbi-firewall-zone .cbi-input-select, #cbi-network-switch_vlan .cbi-input-select { + min-width: 11rem; +} +#cbi-network-switch_vlan .cbi-input-text { + max-width: 3rem; +} +.cbi-input-invalid { + color: #f00; + border-bottom-color: #f00; +} +.cbi-section-error { + padding: 1.5rem; + color: #fb6340; + font-weight: 600 +} +.cbi-section-error ul { + margin: 0 0 0 20px; +} +.cbi-section-error ul li { + font-weight: bold; + color: #f00; +} +.td[data-title]::before { + font-weight: bold; + display: none; + padding: .25rem 0; + content: attr(data-title) ":\20"; + text-align: left; + white-space: nowrap; +} +.tr.placeholder .td[data-title]::before { + display: none; +} +.tr[data-title]::before, .tr.cbi-section-table-titles.named::before { + font-weight: bold; + display: table-cell; + align-self: center; + flex: 1 1 5%; + padding: .25rem; + content: attr(data-title) "\20"; + text-align: center; + vertical-align: middle; + white-space: normal; + word-wrap: break-word; +} +.cbi-section-table .cbi-section-table-titles .cbi-section-table-cell { + width: auto !important; +} +.td.cbi-section-actions { + text-align: right !important; + vertical-align: middle; +} +.td.cbi-section-actions > * { + display: inline-flex; + align-items: center; +} +.td.cbi-section-actions > * > *, .td.cbi-section-actions > * > form > * { + margin: 0 5px; +} +.td.cbi-section-actions > * > form { + display: inline-flex; + margin: 0; +} +/* lists */ +.cbi-dynlist { + line-height: 1.3; + flex-direction: column; + min-height: 30px; + cursor: text; +} +.cbi-dynlist > .item { + display: inline-flex; + flex-wrap: nowrap; + position: relative; + max-width: 25rem; + pointer-events: none; + outline: 0; + margin-top: 0.2rem; + color: #666; +} +.cbi-dynlist[name="sshkeys"] > .item { + max-width: none; +} +.cbi-dynlist>.item::after { + background-image: none; + box-shadow: none; + content: "\00D7"; + pointer-events: auto; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + font-weight: bold; + font-size: 1.5rem; + line-height: 1; + width: 2.5rem !important; + padding: 5px!important; + color: #f8f8f8; + border: 0px solid var(--inputborder-color); + border-radius: 0.25rem; + outline: 0; + background-color: #d9534f; + box-sizing: border-box; +} + +.cbi-dynlist > .item > span { + display: block; + padding: 0.5rem 0.75rem; + min-width: 15.5rem; + width: 15.5rem; + transition: box-shadow .15s ease; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-break: break-word; + font-size: var(--font-x); + font-weight: normal; + line-height: 24px; + background-image: none; + color: var(--inputtext-color); + font-family: var(--font-sans); + border: 1px solid var(--inputborder-color); + background-color: var(--inputbg-color); + border-radius: 0.25rem 0 0 0.25rem; + outline: 0; + box-shadow: none; + box-sizing: border-box; +} +.cbi-dynlist>.add-item { + display: inline-flex; + align-items: center; + width: 100%; + min-width: 16rem; + margin-top: 0.2rem; + gap: 0; + flex-wrap: nowrap; +} +.cbi-dynlist>.add-item input { + display: block; + color: var(--inputtext-color); + font-size: var(--font-z); + font-size: .875rem; + background-image: none; + box-shadow: none; + font-family: var(--font-sans); + border: 1px solid var(--inputborder-color); + background-color: var(--inputbg-color); + margin: 0; + padding: 0.5rem 0.75rem; + box-sizing: border-box; + min-width: 15.5rem; + width: 15.5rem; + transition: box-shadow .15s ease; + white-space: nowrap; + word-break: break-word; + line-height: 1.5rem; + border-radius: 0.25rem 0 0 0.25rem; + border-right-width: 0; + outline: 0; +} +.cbi-dynlist > .add-item:not([ondrop]) > input { + overflow: hidden; + min-width: 15.5rem; + width: 15.5rem; + white-space: nowrap; + text-overflow: ellipsis; +} +.cbi-dynlist > .add-item[ondrop] > input { + min-width: 13rem; +} +.cbi-dynlist>.add-item .cbi-button-add { + width: 2.5rem !important; + padding: 5px!important; + font-weight: bold; + font-size: 1.5rem; + line-height: 1; + margin: 5px 5px 5px 0; + color: #fff; + background-color: rgba(var(--primary-rgbm), 0.9); + border: 1px solid rgba(var(--primary-rgbm), 1); +} + +.cbi-dynlist, .cbi-dropdown { + position: relative; + display: inline-flex; + line-height: 1; +} +.cbi-dropdown[placeholder*="select"] { + max-width: 25rem; + height: auto; + margin-top: -3px; +} +.cbi-dropdown > ul { + display: flex; + overflow-x: hidden; + overflow-y: auto; + width: 100%; + margin: 0 !important; + padding: 0; + list-style: none; + outline: 0; +} +.cbi-dropdown > ul.preview { + display: none; +} +.cbi-dropdown > .open { + flex-basis: 0px; + font-size: 1.5rem; + +} +.cbi-dropdown > .open, .cbi-dropdown > .more { + font-weight: bolder; + background-color: rgba(255,255,255,0)!important; + display: flex; + flex-direction: column; + justify-content: center; +} +.cbi-dropdown > .more { + font-size: var(--font-z); +} +.cbi-dropdown.btn > .open { + padding: 0.5rem; + margin: 0; +} +.cbi-dropdown.btn > div { + margin: 0px; +} +.cbi-dropdown > .more, .cbi-dropdown > ul > li[placeholder] { + font-weight: bold; + display: none; + color: #777; + text-shadow: 1px 1px 0 #ddd; +} +.cbi-dropdown > ul > li { + display: none; + overflow: hidden; + align-items: center; + align-self: center; + flex-grow: 1; + flex-shrink: 1; + min-height: 30px; + padding: 0 10px 0 10px; + white-space: nowrap; + text-overflow: ellipsis; +} +.cbi-dropdown > ul > li .hide-open { + display: initial; +} +.cbi-dropdown > ul > li .hide-close { + display: none; +} +.cbi-dropdown > ul > li[display]:not([display="0"]) { + border-left: thin solid #ccc; +} +.cbi-dropdown[empty] > ul { + max-width: 1px; +} +.cbi-dropdown > ul > li > form { + display: none; + margin: 0; + padding: 0; + pointer-events: none; +} +.cbi-dropdown > ul > li img { + margin-right: .25em; + vertical-align: middle; +} +.cbi-dropdown > ul > li > form > input[type="checkbox"] { + height: auto; + margin: 0; +} +.cbi-dropdown > ul > li input[type="text"] { + height: 20px; +} +.cbi-dropdown[open] > ul.dropdown { + position: absolute; + z-index: 1100; + display: block; + width: auto; + min-width: 100%; + max-width: none; + max-height: 160px !important; + border: 0px solid var(--inputborder-color); + background-color: rgba(var(--primary-rgbbody), 1); + box-shadow: 0 0 4px #918e8c; + color: var(--inputtext-color); +} + +.cbi-dropdown > ul > li[display], .cbi-dropdown[open] > ul.preview, .cbi-dropdown[open] > ul.dropdown > li, .cbi-dropdown[multiple] > ul > li > label, .cbi-dropdown[multiple][open] > ul.dropdown > li, .cbi-dropdown[multiple][more] > .more, .cbi-dropdown[multiple][empty] > .more { + display: flex; + align-items: center; + padding: 0 10px 0 10px; + flex-grow: 1; +} +.cbi-dropdown > ul > li[display], .cbi-dropdown[open] > ul.preview { + padding: 0px; +} +.cbi-dropdown[empty] > ul > li, .cbi-dropdown[optional][open] > ul.dropdown > li[placeholder], .cbi-dropdown[multiple][open] > ul.dropdown > li > form { + display: block; +} +.cbi-dropdown[open] > ul.dropdown > li .hide-open { + display: none; +} + +.cbi-dropdown[open] > ul.dropdown > li .hide-close { + display: initial; +} +.cbi-dropdown[open] > ul.dropdown > li[selected] { + background-color: rgba(0, 0, 0, 0.12); + margin-bottom: 1px; + padding-left: 0.5rem; + +} +.cbi-dropdown[open] > ul.dropdown > li.focus { + background: linear-gradient(90deg, #3867075e 0%, #84aad9 100%); + color:#fff; +} +.cbi-dropdown[open] > ul.dropdown > li:last-child { + margin-bottom: 0; + padding-left: 0.5rem; + border-bottom: 0; +} +.cbi-dropdown[open] > ul.dropdown > li[unselectable] { + opacity: .7; +} +.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { + width: 100%; +} +.cbi-dropdown[disabled] { + pointer-events: none; + opacity: .6; +} +.cbi-dropdown .zonebadge { + width: 100%; +} +.cbi-dropdown[open] .zonebadge { + width: auto; +} +/* progressbar */ +.cbi-progressbar { + position: relative; + height: 26px; + margin: 2px 0; + border-radius: 0.25rem!important; + border-color: rgba(0,0,0,0.4)!important; + background-color: rgba(0,0,0,0.3)!important; + color: #eee; +} + +.cbi-progressbar > div { + width: 0; + height: 100%; + transition: width .25s ease-in; + border-radius: 0.25rem!important; + background-color: rgba(var(--primary-rgbm),0.7); + background-image: var(--bgqs-image); +} +.cbi-progressbar::after { + font-size: var(--font-x); + font-size-adjust: .38; + line-height: normal; + position: absolute; + top: 2px; + right: 0; + bottom: 2px; + left: 0; + overflow: hidden; + content: attr(title); + text-align: center; + white-space: pre; + text-overflow: ellipsis; +} +#modal_overlay { + position: fixed; + z-index: 900; + top: 0px; + display: none; + bottom: 0; + overflow-y: scroll; + transition: opacity .1s ease-in; + opacity: 0; + background: rgba(0, 0, 0, .7); + -webkit-overflow-scrolling: touch; +} +.modal { + display: flex; + align-items: center; + flex-wrap: wrap; + width: 90%; + min-width: 270px; + max-width: 800px; + min-height: 32px; + margin: 5em auto; + padding: 1em; + border-radius: 0.25rem!important; + background-color: rgba(var(--primary-rgbbody), 1); + box-shadow: 0 2px 10px 0px rgba(255, 255, 255, .16), 0 0 10px 0 rgba(255, 255, 255, .12); +} +#modal_overlay .cbi-section, +.modal .cbi-section{ + overflow: visible; +} +/* +.modal .right>.btn { + padding: 0 0.8rem +} +*/ +.modal .right>.primary { + padding: .3rem .6rem; +} + +.modal [data-tab-active="true"] { + overflow: visible; +} +.modal .cbi-value-title { + width: 15rem; + padding-right: 1rem; +} +.modal > * { + line-height: normal; + flex-basis: 100%; + margin-bottom: .5em; + max-width: 100%; +} +.modal > pre, .modal > textarea { + font-size: var(--font-z); + font-size-adjust: .35; + overflow: auto; + margin-bottom: .5em; + padding: 8.5px; + cursor: auto; + white-space: pre-wrap; + color: #eee; + outline: 0; + background-color: #101010; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .16), 0 0 2px 0 rgba(0, 0, 0, .12); +} +.modal > h4 { + margin: .5em 0; +} +.modal ul { + margin-left: 2.2em; +} +.modal li { + list-style-type: square; + color: #808080; +} +.modal p { + padding-left: .25rem; + word-break: break-word; +} + +.notice { + background-color: #b98413 !important; + background-color: rgba(var(--primary-rgbm), 1)!important; + color: #eee; +} + +#modal_overlay .modal>p ,.modal>p { + font-size: var(--font-x); +} +.modal .label { + font-size: var(--font-x); + font-weight: normal; + padding: .1rem .3rem; + padding-bottom: 0; + cursor: default; + border-radius: 0; +} +.modal .label.warning { + background-color: #f0ad4e !important; +} + +[data-page="admin-system-flash"] .modal>p>.btn { + white-space: normal !important; + font-size: var(--font-x); + margin: 0; + line-height: 1; + color: var(--inputtext-color); + padding: 0.3rem 1rem; + text-align: left; +} + +[data-page="admin-system-flash"] .modal>.right,[data-page="admin-system-flash"] .modal>.right::before { +margin: 3rem 5px 0 5px; +} + +[data-page="admin-system-flash"] .modal>.right>.btn { +color: #f8f8f8; +} +.modal.cbi-modal { + max-width: 90%; + max-height: none; +} +body.modal-overlay-active { + overflow: hidden; + height: 100vh; +} +body.modal-overlay-active #modal_overlay { + right: 0; + left: 0; + opacity: 1; + display: inline-block; +} +.spinning { + position: relative; + font-size: 1rem; + padding-left: 32px !important; +} +.spinning::before { + position: absolute; + top: 0; + bottom: 0; + left: .2em; + width: 32px; + content: ""; + background: url(../../resources/icons/loading.gif) no-repeat center; + background-size: 20px; + text-align: center; + -webkit-box-align: center; + -ms-flex-align: center; + -webkit-box-pack: center; + -ms-flex-pack: center; +} +/* luci */ +.hidden { + display: none; +} +.left, .left::before { + text-align: left !important; + margin-left: 5px; +} +.right .cbi-button { + line-height: 1; + margin: 0; +} +.right, .right::before { + text-align: right !important; + margin-right: 5px; + z-index: 9999; + margin-bottom: 5px; +} + +.center, .center::before { + text-align: center !important; +} +.top { + align-self: flex-start !important; + vertical-align: top !important; +} +.bottom { + align-self: flex-end !important; + vertical-align: bottom !important; +} +.inline { + display: inline; +} +[data-page="admin-system-system"] .control-group { + margin-top: 0.5rem; +} + +.cbi-page-actions { + padding: 1rem 0 2rem 0; + padding: 1rem; + text-align: center; + justify-content: center; +} +.cbi-page-actions > form[method="post"] { + display: inline-block; +} +.th[data-type="button"], .td[data-type="button"], .th[data-type="fvalue"], .td[data-type="fvalue"] { + flex: 1 1 2em; + text-align: center; +} +.ifacebadge { + display: inline-flex; + align-items: center; + gap: 0.2rem; + padding: 0.4rem 0.8rem; + box-shadow: inset 0 1px 0px rgba(255, 255, 255, .4), 0 1px 1px rgba(0, 0, 0, .2); + background-color: rgba(255, 255, 255, .1); + border-radius: 0.25rem; +} + +.ifacebadge > em, .ifacebadge > img { + display: inline-block; + align-self: flex-start; + float: right; + width: 20px!important; + height: 20px!important; + margin: 0 0.7rem; +} +.ifacebadge > img + img { + margin: 0 .2rem 0 0; +} +.nft-rules .ifacebadge { + margin: 0.125em; + box-shadow: none; + background-color: rgba(255, 255, 255, 0); +} +.network-status-table { + display: flex; + flex-wrap: wrap; +} +.network-status-table .ifacebox { + flex-grow: 1; + margin: .5em; +} +.network-status-table .ifacebox-body { + display: flex; + flex-direction: column; + height: 100%; +} +.network-status-table .ifacebox-body > span { + flex: 10 10 auto; + height: 100%; +} +.network-status-table .ifacebox-body > div { + display: flex; + flex-wrap: wrap; +} +.network-status-table .ifacebox-body .ifacebadge { + align-items: center; + flex: 1 1 auto; + margin: .5em .25em 0 .25em; + padding: .5em; +} +/* textarea */ + +.cbi-input-textarea, textarea { + padding: 10px; + outline-style: none; + line-height: normal; + font-size: var(--font-z); + font-family: var(--font-apple); + transition: color 100ms ease, border-color 100ms ease, opacity 100ms ease; + -webkit-transition: color 100ms ease, border-color 100ms ease, opacity 100ms ease; + vertical-align: baseline; + border: none; + outline: none; + color: var(--body-text-color); + border-radius: 0.25rem; + background-color: rgba(255,255,255,0.1); + border: 1px solid var(--inputborder-color)!important; + word-wrap: break-word; + white-space: pre-wrap; + min-width: 18rem; + margin: 0rem +} + +/* config changes */ +.uci-change-list { + font-family: var(--font-apple); +} +.uci-change-list ins, .uci-change-legend-label ins { + display: block; + padding: 2px; + text-decoration: none; + background-color: #cfc; +} +.uci-change-list del, .uci-change-legend-label del { + font-style: normal; + display: block; + padding: 2px; + text-decoration: none; + /* border: thin solid #f00; */ + background-color: #fcc; +} +.uci-change-list var, .uci-change-legend-label var { + font-style: normal; + display: block; + padding: 2px; + text-decoration: none; + /* border: thin solid #ccc; */ + background-color: #eee; +} +.uci-change-list var ins, .uci-change-list var del { + font-style: normal; + padding: 0; + white-space: pre; + border: 0; +} +.uci-change-legend { + padding: 5px; +} +.uci-change-legend-label { + float: left; + width: 150px; +} +.uci-change-legend-label > ins, .uci-change-legend-label > del, .uci-change-legend-label > var { + display: block; + float: left; + width: 15px; + height: 15px; + margin: 2px; +} +.uci-change-legend-label var ins, .uci-change-legend-label var del { + line-height: .4; + border: 0; +} +.uci-change-list var, .uci-change-list del, .uci-change-list ins { + padding: .5rem; +} +/* other fix */ +#iwsvg,#iwsvg2,#bwsvg { + border: none !important; + border-radius: 0.25rem; + background-color: rgba(255,255,255,0.1)!important; + box-shadow: 0 0 2rem 0 rgba(136,152,170,0.15); + overflow: hidden +} + +#iwsvg, [data-page="admin-status-realtime-bandwidth"] #bwsvg { + border-top: 0 !important; +} +.ifacebox { + border-radius: 0.375rem; + display: inline-flex; + flex-direction: column; + color: var(--body-color); + font-size: var(--font-x); + overflow: hidden; + border: 1px solid var(--inputborder-color); + background-color: var(--inputbg-color); + align-items: center; +} +.ifacebox-head { + padding: 1rem; + min-width: 7rem; + background-color: rgba(var(--primary-rgbm), 0.2); +} +.ifacebox-head.active { + background: #5bc0de; + background-color: rgba(var(--primary-rgbm), 0.1); +} +.ifacebox-body { + padding: .5em +} +.cbi-image-button { + margin-left: .5rem; +} +.zonebadge { + padding: .2rem .5rem!important; + display: inline-block; + padding: .2rem .5rem; +} +.zonebadge .ifacebadge { + margin: .1rem .2rem; + padding: .2rem 1rem; + border: thin solid #6c6c6c; +} +.zonebadge > input[type="text"] { + background-color: rgba(var(--primary-rgbbody), 1); + border: 1px solid #bbb; + min-width: 8rem; + margin-top: .3rem; + padding: .16rem 1rem; +} +.zonebadge > em, .zonebadge > strong { + display: inline-block; + margin: 0 .2rem; +} +.cbi-value-field .cbi-input-checkbox, .cbi-value-field .cbi-input-radio { + margin-top: .1rem; +} +.cbi-value-field > ul > li { + display: flex; +} +.cbi-value-field > ul > li > label { + margin-top: .5rem; +} +.cbi-value-field > ul > li .ifacebadge { + margin-top: -.5rem; + margin-left: .4rem; + background-color: #eee; +} +.cbi-section-table-row>.cbi-value-field .cbi-input-text, .cbi-section-table-row>.cbi-value-field .cbi-input-password, .cbi-section-table-row > .cbi-value-field .cbi-dropdown { + min-width: 7rem; +} +.cbi-section-create { + display: inline-flex; + align-items: center; + margin: .5rem -3px; +} +.cbi-section-create > * { + margin: .5rem; +} +.cbi-section-remove { + padding: .5rem; +} +div.cbi-value var, td.cbi-value-field var, .td.cbi-value-field var { + font-style: italic; + color: #0069d6; +} +.cbi-optionals { + padding: 1rem 1rem 0 1rem; + border-top: thin solid #ccc; +} +.cbi-dropdown-container { + position: relative; +} +.cbi-tooltip-container, span[data-tooltip], span[data-tooltip] .label { + cursor: help !important; +} +.cbi-tooltip { + position: absolute; + z-index: 1000; + left: -1000px; + padding: 2px 5px; + transition: opacity .25s ease-out; + white-space: pre; + pointer-events: none; + opacity: 0; + border-radius: 0.25rem; + background: #fff; + box-shadow: 0 0 2px #444; +} +.cbi-tooltip-container:hover .cbi-tooltip { + left: auto; + transition: opacity .25s ease-in; + opacity: 1; +} +.zonebadge .cbi-tooltip { + margin: -1.5rem 0 0 -.5rem; + padding: .25rem; + background: inherit; +} +.zonebadge-empty { + color: #404040; + background: repeating-linear-gradient(45deg, rgba(204, 204, 204, .5), rgba(204, 204, 204, .5) 5px, rgba(255, 255, 255, .5) 5px, rgba(255, 255, 255, .5) 10px); +} +.zone-forwards { + display: flex; + min-width: 10rem; +} +.zone-forwards > * { + flex: 1 1 45%; +} +.zone-forwards > span { + flex-basis: 10%; + padding: 0 .25rem; + text-align: center; +} +.zone-forwards .zone-src, .zone-forwards .zone-dest { + display: flex; + flex-direction: column; +} +label > input[type="checkbox"], label > input[type="radio"] { + margin-right: 0.8rem; + position: relative; + margin-bottom: 0.8rem; + vertical-align: -webkit-baseline-middle; +} + +label[data-index][data-depends] { + padding-right: 2em; +} +.showSide { + display: none; + cursor: default; + color: var(--menu-hover-color); + line-height: 1.1; + position: inherit; + cursor: pointer; + padding: 0.9rem; + text-decoration: none + +} +.darkMask { + position: fixed; + z-index: 99; + width: 100%; + height: 100%; + content: ""; + top: 0; + background-color: rgba(0, 0, 0, .6); + transition: opacity 200ms, visibility 200ms; + visibility: hidden; + opacity: 0; +} +/* diagnostics */ +#diag-rc-output > pre, #command-rc-output > pre, [data-page="admin-services-wol"] .notice code { + font-size: var(--font-x); + font-size-adjust: .35; + line-height: normal; + display: block; + overflow-y: hidden; + width: 100%; + padding: 8.5px; + white-space: pre; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .16), 0 0 2px 0 rgba(0, 0, 0, .12); +} +[data-page="admin-network-diagnostics"] .table { + box-shadow: none; +} +input[name="ping"], input[name="traceroute"], input[name="nslookup"] { + width: 80%; +} +/* fix Main Login */ +.node-main-login > .main > .main-left { + display: none; +} +.node-main-login > .main .cbi-page-actions{ + padding: 1rem 0 1rem 0; +} +.node-main-login > .main .cbi-map { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + align-content: center; +} +.node-main-login > .main fieldset { + display: inline; + overflow: hidden; + margin-bottom: 1rem; + padding: .5rem; + border: 0; + background: none; + box-shadow: none; +} +.node-main-login > .main .cbi-value-title { + width: 9.5rem; +} +body.lang_pl.node-main-login .cbi-value-title { + width: 12rem +} +.node-main-login > .main #maincontent { + text-align: center; +} +.node-main-login > .main .container { + display: inline-block; + margin-top: 2rem !important; + padding: 1rem 3.5rem 2rem; + text-align: left; + background-color: #f8f8f8; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .16), 0 0 2px 0 rgba(0, 0, 0, .12); +} +.node-main-login>.main>.main-right>.pd-primary>.fill>.container { + margin: 2rem auto +} +.node-main-login>.main>.main-right>.pd-primary>.fill>.container>.flex1>.brand { + display: none +} +.node-main-login>.main { + top: 0; + height: 100% !important +} +.node-main-login>.main>.main-right { + width: 100%; +} +.node-main-login .main .main-right .login-bg { + display: block; + position: fixed; + width: 100%; + height: 100%; + left: 0px; + top: 0px; + background-size: cover; + background-position: top center; + transition: all 0.1s; + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; + background-repeat: no-repeat; +} +.node-main-login>.main>.main-right>.login-bg.blur { + transform: scale(1.05); + -webkit-filter: blur(5px); + filter: blur(5px) +} +.node-main-login>.main .container .cbi-section-node .cbi-value:hover { + background-image: none !important; +} +.node-main-login>.main fieldset, .node-main-login>.main .cbi-section, .node-main-login>.main .cbi-section>.cbi-section-node { + padding: 0rem; + padding-top: 0rem; + background: none; + border: none; + box-shadow: none; + float: center; + text-align: center; + overflow: hidden +} +.node-main-login>.main #maincontent { + text-align: center; + -webkit-box-align: center; + -ms-flex-align: center; + display: flex; + height: auto; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + align-content: center; + flex-wrap: nowrap; + flex-direction: column; + justify-content: center; +} + +.node-main-login>.main>.main-right #maincontent .container { + display: inline-block; + position: relative; + text-align: center; + margin: 0; + border-radius: 10px; + -webkit-backdrop-filter: blur(6px)!important; + backdrop-filter: blur(6px)!important; + background-color: rgba(var(--primary-rgbbody),0.5)!important; + border: none; + -webkit-box-shadow: 3px 7px 12px rgba(0,0,0,0.22); + box-shadow: 3px 7px 12px rgba(0,0,0,0.22); + -webkit-transition: all 0.2s; + transition: all 0.2s; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + padding: 1rem 1rem 3rem 1rem; + user-select: none; + font-weight: bolder; + overflow-x: hidden; + overflow-y: hidden; +} + +.node-main-login>.main .container:active { + -webkit-transform: scale(0.95) rotateZ(1.2deg); + -ms-transform: scale(0.95) rotate(1.2deg); + transform: scale(0.95) rotateZ(1.2deg) +} +.node-main-login>.main .container h2 { + width: 80px; + height: 80px; + background-size: 80px; + display: block; + margin: 1rem 1rem 3rem 1rem; + text-indent: -500px; + overflow: hidden; + background-image: url(../logo.png); + background-color: rgba(250,250,250,0); + background-repeat: no-repeat; + background-position: 0; + border-top: 0rem; +} +.node-main-login>.main .container .cbi-section-node { + padding: 0.2rem; + text-align: center +} +.node-main-login>.main .container .cbi-value-field input:focus { + color: #f8f8f8; + text-shadow: 0px 1px 3px #222; + outline: 0; + background-color: rgba(255, 255, 255, 0.2) !important; + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, .1) +} +.node-main-login>.main .container input.cbi-button { + font-size: var(--font-z); + position: relative; + transition: all .15s ease; + letter-spacing: .2em; + text-transform: none; + will-change: transform; + border-radius: .375rem; + min-width: 14rem +} +.node-main-login>.bar-primary .container-bar-left, .node-main-login>.bar-primary .container-bar-right, .node-main-login>.main .cbi-button-reset { + display: none !important +} +.node-main-login>.main .cbi-value { + margin-bottom: 10px; + border: none; + display: block +} +.node-main-login>.main .cbi-value.cbi-value .cbi-value-title, .node-main-login>.main .cbi-value.cbi-value-last .cbi-value-title { + display: none !important +} +.node-main-login>.main .container .cbi-map-descr { + display: none; +} +.node-main-login>.main .cbi-value.cbi-value .cbi-value-field .cbi-input-text, .node-main-login>.main .cbi-value.cbi-value .cbi-input-user, .node-main-login>.main .cbi-value.cbi-value-last .cbi-input-password, .node-main-login>.main .cbi-value.cbi-value-last .cbi-input-text[type="password"] { + + box-shadow: none; + border-radius: .375rem; + padding: 0; + color: #fff; + text-shadow: 0px 2px 4px #222; + background-image: none; + text-align: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + text-indent: 0rem; + background-color: rgba(255,255,255,0.3); + width: 14rem; + min-width: 100% !important; + margin: 0 +} +.node-main-login>.main .cbi-value.cbi-value .cbi-value-field, .node-main-login>.main .cbi-value.cbi-value-last .cbi-value-field { + position: relative; + color: #eee; + text-shadow: 0px 1px 3px #222; + width: 14rem +} +.node-main-login>.main .cbi-value.cbi-value .cbi-value-field:before { + font-family: 'sirpdboy-kucat' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-os x-font-smoothing: grayscale; + content: "\e91d"; + color: #eee; + text-shadow: 0px 1px 3px #222; + font-size: 1.3rem; + position: absolute; + z-index: 100; + left: 10px; + top: 10px +} +.node-main-login>.main .cbi-value.cbi-value-last .cbi-value-field:before { + font-family: 'sirpdboy-kucat' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: "\e93c"; + color: #eee; + text-shadow: 0px 1px 3px #222; + font-size: 1.3rem; + position: absolute; + z-index: 100; + left: 10px; + top: 10px +} +.node-main-login>.main .cbi-value.cbi-value-last, .node-main-login>.main .cbi-value { + display: block; + position: relative; + padding: 0rem; + background-image: none !important; + margin-bottom: 1rem; + border-bottom: 0px solid #222 !important; + background-color: rgba(255, 255, 255, 0) +} +.node-main-login>.main .cbi-value.cbi-value-last { + margin-top: 2rem; + background-color: rgba(255, 255, 255, 0) !important +} +.node-main-login .main .main-right #maincontent .alert-message p { + color: #f8f8f8; + font-weight: normal; + font-size: var(--font-x); +} +.node-main-login .alert-message.warning { + display: none !important +} +.node-main-login .errorbox { + width: calc(100% - 2rem); + box-sizing: border-box; + padding: 0.6rem 0rem; + position: absolute; + color: #fff !important; + margin-top: 6rem; + border-radius: 0.25rem; + text-align: center; + background-color: #e39f4d !important; +} +.node-main-login footer { + text-align: center; + margin-top: 4rem; + color: #eee; + text-shadow: 0 0 2px #222; + font-size: var(--font-x); + -webkit-backdrop-filter: none; + backdrop-filter: none; + background-color: rgba(255,255,255, 0); +} +.node-main-login footer a { + color: #eee; + text-shadow: 0 0 2px #222; + text-decoration: none +} +/* fix status */ +.node-status-overview > .main fieldset:nth-child(4) .td:nth-child(2), .node-status-processes > .main .table .tr .td:nth-child(3) { + white-space: normal; +} + +/* samba */ +#cbi-samba [data-tab="template"] .cbi-value-field { + display: block; +} +#cbi-samba [data-tab="template"] .cbi-value-title { + width: auto; + padding-bottom: .6rem; +} +/* software */ +.controls > * > .btn:not([aria-label$="page"]) { + flex-grow: initial !important; + margin-top: .1rem; +} +.controls > #pager > .btn[aria-label$="page"] { + font-size: 1.4rem; + font-weight: bold; +} +.controls > * > label { + margin-bottom: .2rem; +} +.control-group { + /*display: flex; */ + flex-wrap: wrap; + white-space: normal; + gap: 0px; +} +.control-group>input, +.control-group>.cbi-button-neutral{ + margin: 0.2rem 0 0.2rem 0; +} +.node-admin-system-opkg .control-group { + display: block +} +[data-page="admin-system-opkg"] div.btn { + line-height: 3; + display: inline; + padding: .3rem .6rem; +} +[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear) { + margin-top: 2rem; + padding-top: .1rem; +} +.td.version, .td.size { + white-space: normal !important; + /* word-break: break-word; */ + white-space: nowrap; +} +.cbi-tabmenu + .cbi-section { + margin-top: 0; +} +/* wireless overview */ +#cbi-wireless > #wifi_assoclist_table > .tr { + box-shadow: inset 1px -1px 0 #ddd, inset -1px -1px 0 #ddd; +} +#cbi-wireless > #wifi_assoclist_table > .tr.placeholder > .td { + right: 33px; + bottom: 33px; + left: 33px; + border-top: thin solid #ddd !important; +} +#cbi-wireless > #wifi_assoclist_table > .tr.table-titles { + box-shadow: inset 1px 0 0 #ddd, inset -1px 0 0 #ddd; +} +#cbi-wireless > #wifi_assoclist_table > .tr.table-titles > .th { + border-bottom: thin solid #ddd; + box-shadow: 0 -1px 0 0 #ddd; +} +#wifi_assoclist_table > .tr > .td[data-title="RX Rate / TX Rate"] { + width: 23rem; +} +/* firewall */ +#iptables { + margin: 0; +} + +#cbi-firewall-redirect table *, #cbi-network-switch_vlan table *, #cbi-firewall-zone table * { + font-size: small; + white-space: nowrap; +} +#cbi-firewall-redirect table input[type="text"], #cbi-network-switch_vlan table input[type="text"], #cbi-firewall-zone table input[type="text"] { + width: 8rem; +} +#cbi-firewall-redirect table select, #cbi-network-switch_vlan table select, #cbi-firewall-zone table select { + min-width: 3.5rem; +} +#cbi-network-switch_vlan .th, #cbi-network-switch_vlan .td { + flex-basis: 12%; +} +#cbi-firewall-zone .table, #cbi-network-switch_vlan .table { + display: block; +} +#cbi-firewall-zone .td, #cbi-network-switch_vlan .td { + width: 100%; +} +/* applyreboot fix */ +#applyreboot-container { + margin: 2rem; +} +#applyreboot-section { + line-height: 300%; + margin: 2rem; +} +/* openvpn bug fix */ +.OpenVPN a { + line-height: initial !important; +} +/* custom commands */ +.commandbox { + width: 24% !important; + margin: 10px 0 0 10px !important; + padding: .5rem 1rem; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .2), 0 1px 2px rgba(0, 0, 0, .05); +} +.commandbox h3 { + line-height: normal !important; + overflow: hidden; + margin: 6px 0 !important; + white-space: nowrap; + text-overflow: ellipsis; +} +.commandbox div { + left: auto !important; +} +.commandbox code { + position: absolute; + overflow: hidden; + max-width: 60%; + margin-left: 4px; + padding: 2px 3px; + white-space: nowrap; + text-overflow: ellipsis; +} +.commandbox code:hover { + overflow-y: auto; + max-height: 50px; + white-space: normal; +} +.commandbox p:first-of-type { + margin-top: -6px; +} +.commandbox p:nth-of-type(2) { + margin-top: 2px; +} +[data-page^="admin-system-commands"] .panel-title, [data-page^="command-cfg"] .mobile-hide, [data-page^="command-cfg"] .showSide { + display: none; +} +#command-rc-output .alert-message { + line-height: 1.42857143; + position: absolute; + top: 40px; + right: 32px; + max-width: 40%; + margin: 0; + animation: anim-fade-in 1.5s forwards; + word-break: break-word; + opacity: 0; +} +.node-admin-status-hnet #maincontent .container>div>div { + border:none !important; +} + +#view>p { + margin: 0 1.5rem 1rem 1.5rem; +} +.node-admin-status-routes #view h3 { + font-size: var(--font-d); + ; + line-height: 1; + display: block; + width: 100%; + margin: 0; + margin-bottom: 0; + padding: 0.8755rem 1.25rem; + color: var(--body-color); + border-radius: 0.25rem; + padding-bottom: unset; + font-weight: bold; +} +/* fix tables */ +.node-admin-status-routes #view table, .node-admin-status-processes #view table, .node-admin-status-realtime-connections #view table[id*=connections] { + padding: 10px; + border: 1px; + font-weight: normal; + font-style: normal; + line-height: 1; + font-family: inherit; + text-align: left; + min-width: inherit; + overflow-x: auto; + overflow-y: hidden; + background-color: rgba(var(--primary-rgbbody), 0.5); + -webkit-overflow-scrolling: touch; + margin-bottom: 20px; + margin-top: 5px; +} +.node-admin-status-realtime-connections #view table[id*=connections] { + white-space: nowrap; +} +/* fix node-admin-status-processe */ +.node-admin-status-processe button.btn.cbi-button-action { + background-color: rgba(var(--primary-rgbm), 0.9); +} +/* fix node-admin-system-opkg cbi-button-positive */ +.node-admin-system-opkg button.btn.cbi-button-positive { + background-color: rgba(var(--primary-rgbm), 0.9); +} +/* fix node-admin-status-realtime-load */ +.node-admin-status-realtime-load #view div, .node-admin-status-realtime-bandwidth #view > div > div > div > div, .node-admin-status-realtime-connections #view div { + border: none !important; + background-color: rgba(var(--primary-rgbbody), 0.5)!important; + border-radius: 0.25rem; + border: 0px solid #000!important; +} +/* fix */ + +.cbi-page-actions.control-group .cbi-dropdown.btn.cbi-button.important { + padding: 0.6rem 0rem 0.6rem 1rem; +} + +.cbi-page-actions.control-group .cbi-dropdown.btn.cbi-button.cbi-button-negative.important > ul, +.cbi-page-actions.control-group .cbi-dropdown.btn.cbi-button.cbi-button-apply.important > ul { +overflow-y: hidden; +} +.cbi-dropdown.btn.cbi-button.cbi-button-action { + padding-right: 5px; +} +.node-admin-system-ttyd-ttyd iframe { + border-radius: 0.25rem !important; +} +/* fix node-admin-status-overview color */ +.node-admin-status-overview h3 { + color: unset !important; +} + +[data-page="admin-status-iptables"] .right { + margin-bottom: 0rem !important; +} + +#syslog { + border: 1px solid var(--inputborder-color); +} +#syslog { + width: 100%; + min-height: 15rem; + font-family: var(--font-apple); + color: var(--body-color); + background-color: var(--inputbg-color); + font-size: small; + outline: none; + border-radius: 0.25rem; + overflow-x: hidden; +} + +#syslog:focus{ + outline: 0; + box-shadow: 0 0 1px rgba(0,0,0,0.12)!important; +} +#mwan_status_text > div { +padding: 0.5rem!important; +} +#detail-bubble.in { + z-index: 500 +} +.node-admin-nlbw-display .cbi-section ul { + padding: 0.875rem 1.5rem +} +.node-admin-nlbw-backup form { + margin-left: 1.5rem +} +.node-admin-nas-usb_printer em { + display: block +} +.node-admin-nas-usb_printer em { + display: block +} +.node-admin-system-flashops .cbi-section ul { + padding: .875rem 1.5rem; + font-size: var(--font-x); +} +#cbi-netspeedtest-homebox>.cbi-section-node>.cbi-value { + display: inline-block; +} +.chat_window { + background-color:rgba(255,255,255,0.1)!important; +} +.top_menu { +background-color: rgba(255,255,255,0.12)!important; +} +#chatlog .response { + background-color: rgba(255,255,255,0.1)!important; +} + +/* fix luci-app-wrtbwmon */ +.main #view .cbi-value-title { + width: 15rem; +} +/* fix node-admin-status-overview +.node-admin-network-network > .main #view td:nth-child(3), */ +.cbi-map>.cbi-section .cbi-value:nth-of-type(4) td:nth-child(2), .Interfaces > .main #view td:nth-child(3), .node-admin-network-network > .main #view td:nth-child(3), .Processes > .main #view td:nth-child(3), .node-admin-status-processes > .main #view td:nth-child(3), .node-admin-status-overview > .main #view td:nth-child(2) { + white-space: normal !important; +} +.Details>.main .tr.table-totals { + background-color: rgba(255,255,255,0.1)!important; +} +#intervalSelect { + width: 4rem!important; +} +.node-admin-nlbw-usage > .main .tr.table-totals, .node-admin-nlbw-usage > .main .cbi-progressbar { + background-color: rgba(255,255,255,0.1)!important; +} +.node-admin-services-vssr .ssr-button { + margin-left: .3rem +} +.node-admin-services-vssr .block h4 { + color: #ccc !important; + color: var(--body-color) +} +.node-admin-services-vssr .status-bar { + color: var(--body-color); + box-shadow: 0 0 .5rem 0 rgba(0, 0, 0, 0.35); + background-color: rgba(255, 255, 255, 0.1) !important; +} +.node-admin-services-vssr .block, .node-admin-services-shadowsocksr>.block, .node-admin-services-ssrpro>.block, .block, .node-admin-services-bypass>.main .block, .node-admin-services-vssr>.main .block, .main .block { + color: var(--body-color); + background-color: #fff !important; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.32), 0 1px 5px 0 rgba(0, 0, 0, 0.2) +} +.node-admin-services-shadowsocksr>.block:active, .node-admin-services-ssrpro>.block:active, .block:active, .node-admin-services-bypass>.main .block:active, .main .block:active, .node-admin-services-shadowsocksr>.block:hover, .node-admin-services-ssrpro>.block:hover, .block:hover, .node-admin-services-bypass>.main .block:hover, .main .block:hover { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 4px 6px rgba(0, 0, 0, 0.35); + background-color: rgba(250, 250, 250, 0.7) +} +.node-admin-services-samba>.main .cbi-tabcontainer:nth-child(3) .cbi-value-title { + margin-bottom: 1rem; + width: auto +} +.node-admin-services-samba>.main .cbi-tabcontainer:nth-child(3) .cbi-value-field { + display: list-item +} +.node-admin-services-samba>.main .cbi-tabcontainer:nth-child(3) .cbi-value-description { + padding-top: 1rem +} +.node-admin-services-adguardhome>.main .cbi-value-field { + width: 27% !important +} +#cbi-passwall-socks .cbi-section-node .cbi-section-table .cbi-value-field, #cbi-passwall-socks table>tbody>tr>td { + white-space: normal !important; +} +#cbi-AdGuardHome-AdGuardHome-escconf, #cbi-netspeedtest>fieldset>fieldset>div, .Software>fieldset>fieldset>div, .vue-form-render .cbi-section .cbi-section-node .cbi-value, #cbi-AdGuardHome >.main .cbi-section-node .cbi-value, { + display: inline-block; +} +#cbi-openclash .CodeMirror.cm-s-idea.CodeMirror-wrap { + background: rgba(0,0,0,0.1)!important; + color: var(--body-color)!important; +} +#cbi-openclash .cbi-section>div>ul>li { + width: auto!important; + padding: 0rem 0.5rem!important; + +} + +#cbi-openclash .CodeMirror-merge,#cbi-openclash .CodeMirror-merge-2pane { + height: auto!important; + border: none!important +} +#cbi-openclash .cm-s-idea span.cm-variable { + color: #fd8c73!important; +} + +#cbi-openclash .CodeMirror-scroll>.CodeMirror-gutters { + background-color: rgba(255,255,255,0.1)!important; + border-right: 1px solid #777!important; +} + +.node-admin-services-adguardhome>.main .cbi-value .cbi-input-textarea { + width: 100%!important; + box-shadow: 0 0 1px rgba(0, 0, 0, 0.12)!important; +} +.node-admin-system-packages>.main .cbi-page-actions { + padding: 1rem 0 +} + +.node-admin-system-leds .cbi-section em { + display: block +} + +#maincontent>.container>#view, +.node-admin-status-routes #view, +.node-admin-network-diagnostics #view, +.node-admin-status-processes #view { + + min-width: inherit; + overflow-x: auto; + overflow-y: hidden; +} +.node-admin-network-diagnostics #view .table{ + margin-top: 2rem; +} + + @keyframes anim-fade-in { + 100% { + opacity: 1; +} +} + +/* IE hacks */ +@media all and (-ms-high-contrast: none) { +.main > .main-left > .nav > .slide > .menu::before { + top: 30.25%; +} +.main > .main-left > .nav > li:last-child::before { + top: 20%; +} +.showSide::before { + top: -12px; +} +} + +@media screen and (max-width: 1280px) { +.main>.main-left>.nav>li, .main>.main-left>.nav>li>a, .main .main-left .nav>li>a:first-child, .main>.main-left>.nav>.slide>.menu{ + font-size: var(--font-d); +} + +.node-admin-status-iptables>.main fieldset li>a { + padding: .3rem .6rem +} +.main-left { + width: calc(0% + 17rem); +} +.main-right { + width: calc(100% - 17rem); +} +[ data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table) { + display: block +} +[ data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table), [data-page="admin-network-firewall-rules"] .table:not(.cbi-section-table), [data-page="admin-network-hosts"] .table, [data-page="admin-network-routes"] .table { + overflow-y: visible +} +.th.cbi-value-field, .td.cbi-value-field, .th.cbi-section-table-cell, .td.cbi-section-table-cell { + flex-basis: auto; + padding-top: 1rem; +} +#cbi-firewall-zone .cbi-input-select { + min-width: 9rem; +} +.cbi-input-textarea { + font-size: var(--font-x); +} +.node-admin-status > .main fieldset li > a { + padding: .3rem .6rem; +} +.tabs > li > a, .cbi-tabmenu > li > a { + padding: .2rem .5rem; +} + +.table .cbi-input-text { + width: 100%; +} +[data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table) { + display: block; +} +[data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table), [data-page="admin-network-firewall-rules"] .table:not(.cbi-section-table), [data-page="admin-network-hosts"] .table, [data-page="admin-network-routes"] .table { + overflow-y: visible; +} +.commandbox { + width: 32% !important; +} +.node-admin-network-network .cbi-section-table tr, .node-admin-network-network .cbi-section-table td { + font-size: var(--font-x) !important +} +.node-admin-network-network .cbi-section-node table td:nth-of-type(1) { + padding: 5px !important +} +.node-admin-network-network .cbi-section-node table td, .node-admin-network-wireless fieldset .cbi-section-node table td { + padding: 10px !important +} +.node-admin-network-network .cbi-section-table br { + display: initial +} +.node-admin-status-overview .main #view td:nth-child(1) { + width: 8rem; + padding-right: 0.6rem; +} +.cbi-value-title { + width: 15rem; + padding-right: 1rem; +} + +} + +@media screen and (max-width: 992px) { + +header>.fill>.container>.flex1>.brand { + display: inline-block; +} + + .showWord { + display: none; + text-decoration: none + } +html, .main { + overflow-y: hidden; +} +#diag-rc-output>pre, #command-rc-output>pre, [data-page="admin-services-wol"] .notice code { + font-size: var(--font-z); +} +.main-left { + width: calc(0% + 17rem); + box-shadow: rgba(0, 0, 0, 0.75) 0 0 20px -5px; + -webkit-box-shadow: rgba(0, 0, 0, 0.75) 0 0 20px -5px; + position: fixed; + visibility: hidden; + z-index: 100 +} +.main-right { + width: 100%; +} + +header .fill .container .flex1 .showSide { + + display: inline-block; + z-index: 99; +} +.showSide:hover { + background-color: var(--menu-item-hover-bgcolor); + border-radius: 0.25rem; + text-decoration: none +} +.showSide:before { + content: "\e20e"; + font-size: 1.7rem !important +} +.node-main-login .showSide { + display: none !important +} + +.node-admin-status-overview .main #view td:nth-child(1) { + width: 6rem; + padding-right: 0.6rem; +} + +.modal .cbi-value-title, +.cbi-value-title { + width: 12rem; + padding-right: 0.6rem; +} + +.Interfaces .table { + overflow-x: hidden; +} +.node-admin-status-overview .table[width="100%"] > .tr { + flex-wrap: nowrap; +} +.tr.placeholder { + border-bottom: thin solid #ddd; +} +.tr.placeholder > .td, #cbi-firewall .tr > .td, #cbi-network .tr:nth-child(2) > .td, .cbi-section #wifi_assoclist_table .tr > .td { + border-top: 0; +} +.node-admin-admin-status.Overview .tr { + display: flex; + flex-direction: row; + flex-wrap: wrap +} +input[name="ping"], input[name="traceroute"], input[name="nslookup"] { + width: 100% +} +.td.cbi-value-field, .cbi-section-table-cell { + /* display: inline-block; nowarp */ + flex: 10 10 auto; + flex-basis: 50%; + text-align: center; +} +.td.cbi-section-actions { + vertical-align: bottom; +} +.tr[data-title]::before, .tr.cbi-section-table-titles.named::before { + font-size: var(--font-z); + display: block; + flex: 1 1 100%; + border-bottom: thin solid rgba(0, 0, 0, .26); + background: #90c0e0; +} +.td[data-title], [data-page^="admin-status-realtime"] .td[id] { + text-align: left; +} +.td.cbi-section-actions > * > *, .td.cbi-section-actions > * > form > * { + margin: 2.1px 3px; +} +.cbi-section > h3:first-child, .panel-title { + font-size: var(--font-z); + padding-bottom: 0.5rem; +} +.commandbox { + width: 100% !important; + margin-left: 0 !important; +} +table>tbody>tr>td, table>tfoot>tr>td, table>thead>tr>td { + font-size: var(--font-x); + color: var(--body-color); + padding: 1rem 0.5rem +} +.node-admin-services-vssr .status-info { + font-size: var(--font-x); +} +.node-admin-services-vssr .icon-con { + margin-top: 0.4rem +} +.node-admin-services-vssr .icon-con img { + width: 2.3rem !important; + height: auto +} +#ethinfo td { + padding: 0.75rem 0.2rem !important +} +.cbi-page-actions>div>input { + display: none +} +.mobile-hide { + display: none +} +.cbi-value-field .cbi-dropdown, .cbi-value-field .cbi-input-select, .cbi-value input[type="text"], .cbi-value input[type="password"] { + min-width: 8rem; + width: 16rem; + +} +.cbi-input-textarea, textarea { + font-size: var(--font-x); + width: 100%; + min-width: 16rem; +} + +} + +@media screen and (max-width:600px) { +.main-left { + width: calc(0% + 17rem); +} +.table { + display: block; +} +.cbi-section>div>table.table { + display: inline-table; +} +.mobile-hide { + display: none +} +.node-main-login footer { + display: none +} +span[data-indicator="uci-changes"] { + display: none; +} + +[data-page="admin-status-processes"] .td[data-title="Hang Up"]::before, [data-page="admin-status-processes"] .td[data-title="Terminate"]::before, [data-page="admin-status-processes"] .td[data-title="Kill"]::before { + display: none; +} +.hide-sm, .hide-xs:not([data-title="MAC-Address"]) { + display: none; +} + +.node-admin-status-overview > .main #view td:nth-child(1), +.cbi-value-title { + text-align: left; +} + +.node-admin-status-overview .main #view td:nth-child(1) { + width: 5rem; + padding-right: 0.4rem; +} + +.cbi-value-field, .cbi-value-description { + display: block !important; + padding: 0!important; +} +.cbi-value-description { + padding: 0rem 0rem 1rem 0.5rem !important; +} + +#maincontent > .container { + padding: 1rem 0.5rem 3rem 0.5rem !important +} + +} + +@media screen and (max-width:480px) { + +#maincontent>.container { + padding: 1rem 0.5rem 3rem 0.5rem !important +} +.node-admin-status-overview .main #view td:nth-child(1) { + width: 4rem; + padding-right: 0.2rem; +} + +.cbi-section-node { + padding: 10px !important; +} +.cbi-value-description { + width: 100%; + display: block; +} + +[ data-page="admin-system-flash"] legend { + padding: 1rem 0 1rem 1rem +} +[ data-page="admin-system-flash"] .cbi-section-descr { + padding: 1rem 0 1rem 1rem +} +[ data-page="admin-system-flash"] .cbi-value { + padding: 0 1rem +} +[ data-page="admin-network-dhcp"] [data-tab-active="true"] { + padding: 1rem 1rem !important +} +.Diagnostics form .cbi-map .cbi-section div { + width: 100% !important +} +.cbi-section>div { + width: 100% +} + +.node-main-login footer { + display: none +} +select, input { + font-size: var(--font-x); + box-sizing: border-box; + min-width: unset +} + +.cbi-value input[name^="pw"], .cbi-value input[data-update="change"]:nth-child(2) { + width: 8rem; + min-width: 8rem +} + + +} diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/favicon.ico b/luci-theme-kucat/htdocs/luci-static/kucat/favicon.ico new file mode 100644 index 000000000..1ebc4ded4 Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/favicon.ico differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.eot b/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.eot new file mode 100644 index 000000000..03324c813 Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.eot differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.svg b/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.svg new file mode 100644 index 000000000..c85259464 --- /dev/null +++ b/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.svg @@ -0,0 +1,100 @@ + + + + + + +{ + "fontFamily": "sirpdboy-kucat", + "majorVersion": 3, + "minorVersion": 8, + "version": "Version 3.8", + "fontId": "sirpdboy-kucat", + "psName": "sirpdboy-kucat", + "subFamily": "Regular", + "fullName": "sirpdboy-kucat", + "description": "Font generated by IcoMoon." +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.ttf b/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.ttf new file mode 100644 index 000000000..176f0954c Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.ttf differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.woff b/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.woff new file mode 100644 index 000000000..3f83aace1 Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/fonts/sirpdboy-kucat.woff differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/apple-touch-icon.png b/luci-theme-kucat/htdocs/luci-static/kucat/img/apple-touch-icon.png new file mode 100644 index 000000000..30b4e6d73 Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/img/apple-touch-icon.png differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/applelogo144.png b/luci-theme-kucat/htdocs/luci-static/kucat/img/applelogo144.png new file mode 100644 index 000000000..a2fefc7c8 Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/img/applelogo144.png differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/bg1.jpg b/luci-theme-kucat/htdocs/luci-static/kucat/img/bg1.jpg new file mode 100644 index 000000000..92e7a473d Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/img/bg1.jpg differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/favicon.ico b/luci-theme-kucat/htdocs/luci-static/kucat/img/favicon.ico new file mode 100644 index 000000000..c9952110b Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/img/favicon.ico differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/logo16.png b/luci-theme-kucat/htdocs/luci-static/kucat/img/logo16.png new file mode 100644 index 000000000..1e92eb185 Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/img/logo16.png differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/logo32.png b/luci-theme-kucat/htdocs/luci-static/kucat/img/logo32.png new file mode 100644 index 000000000..d15ff5bc5 Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/img/logo32.png differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/logo48.png b/luci-theme-kucat/htdocs/luci-static/kucat/img/logo48.png new file mode 100644 index 000000000..dc0360193 Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/img/logo48.png differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/logo64.png b/luci-theme-kucat/htdocs/luci-static/kucat/img/logo64.png new file mode 100644 index 000000000..cd4049a27 Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/img/logo64.png differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/logo96.png b/luci-theme-kucat/htdocs/luci-static/kucat/img/logo96.png new file mode 100644 index 000000000..bac7f7b91 Binary files /dev/null and b/luci-theme-kucat/htdocs/luci-static/kucat/img/logo96.png differ diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/logourl b/luci-theme-kucat/htdocs/luci-static/kucat/img/logourl new file mode 100644 index 000000000..b4cb8096d --- /dev/null +++ b/luci-theme-kucat/htdocs/luci-static/kucat/img/logourl @@ -0,0 +1 @@ +https://www.github.com/sirpdboy/openwrt \ No newline at end of file diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/logoword b/luci-theme-kucat/htdocs/luci-static/kucat/img/logoword new file mode 100644 index 000000000..da5823308 --- /dev/null +++ b/luci-theme-kucat/htdocs/luci-static/kucat/img/logoword @@ -0,0 +1 @@ +做事先做人,尊重他人劳动成果,是为人的基本准则! \ No newline at end of file diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/refresh.svg b/luci-theme-kucat/htdocs/luci-static/kucat/img/refresh.svg new file mode 100644 index 000000000..3a3802b0f --- /dev/null +++ b/luci-theme-kucat/htdocs/luci-static/kucat/img/refresh.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/img/spinner.svg b/luci-theme-kucat/htdocs/luci-static/kucat/img/spinner.svg new file mode 100644 index 000000000..6ddd3b3ed --- /dev/null +++ b/luci-theme-kucat/htdocs/luci-static/kucat/img/spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/js/jquery.min.js b/luci-theme-kucat/htdocs/luci-static/kucat/js/jquery.min.js new file mode 100644 index 000000000..fb8e4b919 --- /dev/null +++ b/luci-theme-kucat/htdocs/luci-static/kucat/js/jquery.min.js @@ -0,0 +1,6 @@ +/*! jQuery v1.11.3 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.3",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b="length"in a&&a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1; + +return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function aa(){return!0}function ba(){return!1}function ca(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ha=/^\s+/,ia=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ja=/<([\w:]+)/,ka=/\s*$/g,ra={option:[1,""],legend:[1,"",""],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sa=da(y),ta=sa.appendChild(y.createElement("div"));ra.optgroup=ra.option,ra.tbody=ra.tfoot=ra.colgroup=ra.caption=ra.thead,ra.th=ra.td;function ua(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ua(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function va(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wa(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xa(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function ya(a){var b=pa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function za(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Aa(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Ba(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xa(b).text=a.text,ya(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!ga.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ta.innerHTML=a.outerHTML,ta.removeChild(f=ta.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ua(f),h=ua(a),g=0;null!=(e=h[g]);++g)d[g]&&Ba(e,d[g]);if(b)if(c)for(h=h||ua(a),d=d||ua(f),g=0;null!=(e=h[g]);g++)Aa(e,d[g]);else Aa(a,f);return d=ua(f,"script"),d.length>0&&za(d,!i&&ua(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=da(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(la.test(f)){h=h||o.appendChild(b.createElement("div")),i=(ja.exec(f)||["",""])[1].toLowerCase(),l=ra[i]||ra._default,h.innerHTML=l[1]+f.replace(ia,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&ha.test(f)&&p.push(b.createTextNode(ha.exec(f)[0])),!k.tbody){f="table"!==i||ka.test(f)?""!==l[1]||ka.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ua(p,"input"),va),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ua(o.appendChild(f),"script"),g&&za(h),c)){e=0;while(f=h[e++])oa.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ua(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&za(ua(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ua(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fa,""):void 0;if(!("string"!=typeof a||ma.test(a)||!k.htmlSerialize&&ga.test(a)||!k.leadingWhitespace&&ha.test(a)||ra[(ja.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ia,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ua(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ua(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&na.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ua(i,"script"),xa),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ua(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,ya),j=0;f>j;j++)d=g[j],oa.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qa,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Ca,Da={};function Ea(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fa(a){var b=y,c=Da[a];return c||(c=Ea(a,b),"none"!==c&&c||(Ca=(Ca||m("