diff --git a/dae/Makefile b/dae/Makefile index 06bd9306..e7e539bf 100644 --- a/dae/Makefile +++ b/dae/Makefile @@ -6,12 +6,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dae PKG_VERSION:=2026.10.18 -PKG_RELEASE:=1 +PKG_RELEASE:=2 -PKG_SOURCE:=dae-src-2026.10.18-9ec2736d61b0.tar.gz +PKG_SOURCE:=dae-src-2026.10.18-32ed09f0d784.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) -PKG_HASH:=9ec2736d61b0d697e61d05472a3225709a6db836bce1b3075482f9d209cd5beb +PKG_HASH:=32ed09f0d784dce8bc021c9fe63530fbabe74c0c7989da6516b92a0e3de63091 PKG_LICENSE:=AGPL-3.0-only PKG_LICENSE_FILE:=LICENSE diff --git a/daed/Makefile b/daed/Makefile index 12cc713c..81e06706 100644 --- a/daed/Makefile +++ b/daed/Makefile @@ -6,12 +6,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=daed PKG_VERSION:=2026.10.18 -PKG_RELEASE:=2 +PKG_RELEASE:=3 -PKG_SOURCE:=daed-src-2026.10.18-ac22e7a32e17.tar.gz +PKG_SOURCE:=daed-src-2026.10.18-97c371b47eb5.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) -PKG_HASH:=ac22e7a32e176a4a88292cce3c22badc4b62f6956e99bb98245fc92e1a10e159 +PKG_HASH:=97c371b47eb5fd46a4badd5e51b18296858c0dd9aa1808d7cd9d284385a52ca6 PKG_LICENSE:=AGPL-3.0-only MIT PKG_LICENSE_FILES:=LICENSE wing/LICENSE diff --git a/honk/Makefile b/honk/Makefile index 561cd2a1..c8759ec5 100644 --- a/honk/Makefile +++ b/honk/Makefile @@ -12,8 +12,8 @@ HONK_RELEASE_TAG:=v0.1.0-beta.3 HONK_ASSET_TAG:=v0.1.0-beta.3 # SHA256 of each release tarball, kept in sync by scripts/update_honk_version.sh. -HONK_HASH_X86_64:=c839b648a443abde8b9bbc8290ebc3c6cd6a5c3e579f391a79016304820354ff -HONK_HASH_AARCH64:=216ace91ba157f909fca2ecf1f3b33015251b9481382609542fc6907dd521602 +HONK_HASH_X86_64:=259836292247434ee1ced18f8239c4486697c4855ac124195ee6f4c80e0df697 +HONK_HASH_AARCH64:=97e49b43c8efc4618f77a9ccb8a1b015e07e5e942728fd466a2d118cd82db169 PKG_LICENSE:=AGPL-3.0-only PKG_LICENSE_FILE:=LICENSE @@ -27,9 +27,9 @@ else ifeq ($(ARCH),aarch64) HONK_HASH:=$(HONK_HASH_AARCH64) endif -# Default to the stock-malloc build: lower RSS high-water on small gateways. -# Use an empty suffix to switch back to the mimalloc build (faster QUIC). -HONK_SUFFIX:=-stock +# mimalloc build: faster QUIC. honk.init sets MIMALLOC_ARENA_RESERVE=0 so the +# 1 GiB arena reservation stays out of VmSize. Use "-stock" to go back. +HONK_SUFFIX:= # Upstream feature variant: -native-api (recommended for OpenWrt gateway), -full, or empty for legacy releases. HONK_VARIANT:=-native-api HONK_ASSET:=honk-core-$(HONK_ASSET_TAG)-$(HONK_TARGET)$(HONK_SUFFIX)$(HONK_VARIANT).tar.gz diff --git a/honk/files/honk.init b/honk/files/honk.init index ceb4faa7..386a2a8b 100644 --- a/honk/files/honk.init +++ b/honk/files/honk.init @@ -87,7 +87,9 @@ start_service() { hijack_resolv_conf procd_open_instance "$CONF" - procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" + # mimalloc pre-reserves a 1 GiB arena; VSZ only, but it dwarfs the real + # footprint in ps. MIMALLOC_ARENA_RESERVE=0: grow on demand instead. + procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" MIMALLOC_ARENA_RESERVE="0" procd_set_param command "$PROG" --config "$config_file" procd_set_param netdev "$HONK_HOST_IFACE" "$HONK_PEER_IFACE" procd_set_param limits core="unlimited" diff --git a/luci-app-adguardhome/.github/workflows/build-apk.yaml b/luci-app-adguardhome/.github/workflows/build-apk.yaml index 3eab5d1b..64b83777 100644 --- a/luci-app-adguardhome/.github/workflows/build-apk.yaml +++ b/luci-app-adguardhome/.github/workflows/build-apk.yaml @@ -11,14 +11,19 @@ permissions: contents: write env: - TARGET: x86 - SUBTARGET: '64' - PKG_DIR_NAME: luci-app-adguardhome + SDK_DIR: /builder + PKG_DIR_NAME: luci-app-adguardhome-tk jobs: build: runs-on: ubuntu-latest timeout-minutes: 90 + strategy: + matrix: + sdk: [x86-64-25.12.6] + container: + image: ghcr.io/openwrt/sdk:${{ matrix.sdk }} + options: --user root defaults: run: shell: bash @@ -29,121 +34,29 @@ jobs: with: fetch-depth: 0 - - name: 安装依赖 - run: | - sudo apt update - sudo apt install -y build-essential clang flex bison g++ gawk \ - gcc-multilib g++-multilib gettext git libncurses-dev libssl-dev \ - python3-setuptools rsync swig unzip zlib1g-dev file curl zstd jq - - - name: 确定 SDK 版本 - id: sdk - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - RELEASE=$( - curl --fail --silent --show-error --location --retry 3 \ - -H "Authorization: Bearer ${GH_TOKEN}" \ - 'https://api.github.com/repos/openwrt/openwrt/releases?per_page=100' \ - | jq -r '.[] | select(.draft == false and .prerelease == false) | .tag_name - | select(test("^v25\\.12\\.[0-9]+$")) | ltrimstr("v")' \ - | sort -V | tail -n 1 - ) - [[ -n "$RELEASE" ]] || { echo '::error::未找到 OpenWrt 25.12 正式版'; exit 1; } - BASE="https://downloads.openwrt.org/releases/${RELEASE}/targets/${TARGET}/${SUBTARGET}" - SDK_CACHE="${RUNNER_TEMP}/adguardhome-sdk-download" - mkdir -p "$SDK_CACHE" - curl -fSL --retry 3 "$BASE/sha256sums" -o "$RUNNER_TEMP/adguardhome-sdk-sha256sums" - SDK_TARBALL=$(awk '{ name=$2; sub(/^\*/, "", name); if (name ~ /^openwrt-sdk-.*\.tar\.zst$/) print name }' "$RUNNER_TEMP/adguardhome-sdk-sha256sums") - [[ -n "$SDK_TARBALL" && "$SDK_TARBALL" != *$'\n'* && "$SDK_TARBALL" != */* ]] || { - echo '::error::无法唯一确定 SDK 文件'; exit 1; - } - SDK_SHA=$(awk -v name="$SDK_TARBALL" '{ file=$2; sub(/^\*/, "", file); if (file == name) print $1 }' "$RUNNER_TEMP/adguardhome-sdk-sha256sums") - [[ "$SDK_SHA" =~ ^[0-9a-f]{64}$ ]] - echo "sha256=$SDK_SHA" >> "$GITHUB_OUTPUT" - echo "SDK_URL=$BASE/$SDK_TARBALL" >> "$GITHUB_ENV" - echo "SDK_TARBALL=$SDK_TARBALL" >> "$GITHUB_ENV" - echo "SDK_SHA=$SDK_SHA" >> "$GITHUB_ENV" - echo "OpenWrt SDK: ${RELEASE}" >> "$GITHUB_STEP_SUMMARY" - - - name: 缓存 SDK 压缩包 - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/adguardhome-sdk-download - key: adguardhome-sdk-v1-${{ runner.os }}-${{ env.TARGET }}-${{ env.SUBTARGET }}-${{ steps.sdk.outputs.sha256 }} - - - name: 校验并解压 SDK - run: | - set -euo pipefail - SDK_CACHE="${RUNNER_TEMP}/adguardhome-sdk-download" - SDK_ROOT="${RUNNER_TEMP}/adguardhome-sdk" - mkdir -p "$SDK_CACHE" "$SDK_ROOT" - cd "$SDK_CACHE" - # 缓存命中也校验官方摘要;缺失或损坏时重新下载。 - if ! printf '%s %s\n' "$SDK_SHA" "$SDK_TARBALL" | sha256sum -c -; then - curl -fSL --retry 3 "$SDK_URL" -o "$SDK_TARBALL" - fi - printf '%s %s\n' "$SDK_SHA" "$SDK_TARBALL" | sha256sum -c - - tar --zstd -xf "$SDK_TARBALL" -C "$SDK_ROOT" - SDK_DIR=$(find "$SDK_ROOT" -mindepth 1 -maxdepth 1 -type d -name 'openwrt-sdk-*' -print) - [[ -n "$SDK_DIR" && "$SDK_DIR" != *$'\n'* ]] - echo "SDK_DIR=$SDK_DIR" >> "$GITHUB_ENV" - - name: 缓存依赖源码下载 uses: actions/cache@v5 with: path: ${{ env.SDK_DIR }}/dl - key: adguardhome-dl-v1-${{ runner.os }}-${{ steps.sdk.outputs.sha256 }}-${{ hashFiles('Makefile', '.github/workflows/build-apk.yaml') }} + key: adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-${{ hashFiles('Makefile', '.github/workflows/build-apk.yaml', 'scripts/*.sh') }} restore-keys: | - adguardhome-dl-v1-${{ runner.os }}-${{ steps.sdk.outputs.sha256 }}- + adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}- + + - name: 准备 SDK + run: bash scripts/prepare-sdk.sh - name: 构建包 - run: | - set -euo pipefail - cd "$SDK_DIR" - - mkdir -p package/${PKG_DIR_NAME} - rsync -a --delete "$GITHUB_WORKSPACE/" "package/${PKG_DIR_NAME}/" \ - --exclude '.git*' --exclude '.local' --exclude '_sdk' --exclude 'dist' - - ./scripts/feeds update -a - ./scripts/feeds install luci-base tar - - touch .config - sed -i -E '/^(# )?CONFIG_(ALL|ALL_KMODS|ALL_NONSHARED|PACKAGE_TAR_XZ|PACKAGE_luci-app-adguardhome)(=| is not set)/d' .config - printf '%s\n' \ - 'CONFIG_ALL=n' \ - 'CONFIG_ALL_KMODS=n' \ - 'CONFIG_ALL_NONSHARED=n' \ - '# CONFIG_PACKAGE_TAR_XZ is not set' \ - 'CONFIG_PACKAGE_luci-app-adguardhome=y' >> .config - make defconfig - if ! grep -Eq '^CONFIG_PACKAGE_luci-app-adguardhome=(y|m)$' .config; then - echo '::error::luci-app-adguardhome was not enabled after make defconfig' - grep -n 'luci-app-adguardhome' .config || true - sed -n '/^[[:space:]]*config PACKAGE_luci-app-adguardhome$/,/^[[:space:]]*help/p' tmp/.config-package.in - exit 1 - fi - - make package/compile -j"$(nproc)" V=sc + run: runuser -u buildbot -- bash scripts/build-apk.sh - name: 整理 APK 产物 - run: | - set -euo pipefail - mkdir -p "$GITHUB_WORKSPACE/dist" - mapfile -t packages < <(find "$SDK_DIR/bin/packages" -type f -name "${PKG_DIR_NAME}-*.apk") - if (( ${#packages[@]} == 0 )); then - echo '::error::未找到 AdGuardHome APK' - exit 1 - fi - cp "${packages[@]}" "$GITHUB_WORKSPACE/dist/" + run: bash scripts/collect-apks.sh - - name: 上传独立 APK + - name: 上传 APK 压缩包 uses: actions/upload-artifact@v7 with: + name: adguardhome-apks path: dist/*.apk - archive: false + archive: true if-no-files-found: error - name: 发布到 Releases @@ -152,6 +65,6 @@ jobs: with: tag_name: ${{ github.ref_name }} prerelease: ${{ startsWith(github.ref_name, 'dev-') }} - make_latest: ${{ startsWith(github.ref_name, 'dev-') && 'false' || 'true' }} + make_latest: 'false' fail_on_unmatched_files: true files: dist/*.apk diff --git a/luci-app-adguardhome/Makefile b/luci-app-adguardhome/Makefile index 07cea060..b30a52b9 100644 --- a/luci-app-adguardhome/Makefile +++ b/luci-app-adguardhome/Makefile @@ -1,56 +1,75 @@ # # Copyright (C) 2025 w9315273 # -# This is free software, licensed under the MIT License. -# include $(TOPDIR)/rules.mk -PKG_NAME:=luci-app-adguardhome -PKG_VERSION:=2.5.0 -PKG_RELEASE:=2 +PKG_NAME:=luci-app-adguardhome-tk +PKG_VERSION:=2.6.0 +PKG_RELEASE:=1 -PKG_LICENSE:=MIT -PKG_LICENSE_FILES:=LICENSE PKG_MAINTAINER:=w9315273 PKG_BUILD_DEPENDS:=luci-base/host include $(INCLUDE_DIR)/package.mk -define Package/luci-app-adguardhome +define Package/luci-app-adguardhome-tk SECTION:=luci CATEGORY:=LuCI SUBMENU:=3. Applications TITLE:=LuCI app for AdGuardHome PKG_MAINTAINER:=https://github.com/w9315273/luci-app-adguardhome PKGARCH:=all + PROVIDES:=luci-app-adguardhome + CONFLICTS:=luci-app-adguardhome DEPENDS:=+luci-base +rpcd-mod-ucode +uclient-fetch +jsonfilter +flock +ca-bundle +!(PACKAGE_libustream-openssl||PACKAGE_libustream-wolfssl):libustream-mbedtls +tar endef -define Package/luci-app-adguardhome/description +define Package/luci-app-adguardhome-tk/description LuCI support for AdGuardHome (nftables-based DNS redirect). This package does NOT include the AdGuardHome core binary. endef +define Package/luci-i18n-adguardhome-tk-zh-cn + SECTION:=luci + CATEGORY:=LuCI + HIDDEN:=1 + TITLE:=AdGuardHome - Simplified Chinese translation + PKGARCH:=all + PROVIDES:=luci-i18n-adguardhome-zh-cn + CONFLICTS:=luci-i18n-adguardhome-zh-cn + DEPENDS:=+luci-app-adguardhome-tk + DEFAULT:=LUCI_LANG_zh_Hans +endef + +define Package/luci-i18n-adguardhome-tk-zh-cn/description + Simplified Chinese translation for luci-app-adguardhome-tk. +endef + define Build/Prepare endef define Build/Compile endef -define Package/luci-app-adguardhome/conffiles +define Package/luci-app-adguardhome-tk/conffiles /etc/config/AdGuardHome endef -define Package/luci-app-adguardhome/install +define Package/luci-app-adguardhome-tk/install cp -pR ./root/* $(1)/ +endef +define Package/luci-i18n-adguardhome-tk-zh-cn/install + $(INSTALL_DIR) $(1)/etc/uci-defaults + echo "uci set luci.languages.zh_cn='简体中文 (Simplified Chinese)'; uci commit luci" \ + > $(1)/etc/uci-defaults/luci-i18n-adguardhome-tk-zh-cn $(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n po2lmo ./po/zh-cn/AdGuardHome.po \ $(1)/usr/lib/lua/luci/i18n/AdGuardHome.zh-cn.lmo endef -define Package/luci-app-adguardhome/postinst +define Package/luci-app-adguardhome-tk/postinst #!/bin/sh [ -n "$${IPKG_INSTROOT}" ] && exit 0 @@ -70,7 +89,7 @@ rm -f /tmp/luci-modulecache/* exit 0 endef -define Package/luci-app-adguardhome/prerm +define Package/luci-app-adguardhome-tk/prerm #!/bin/sh if [ -z "$${IPKG_INSTROOT}" ]; then /etc/init.d/AdGuardHome disable @@ -85,4 +104,6 @@ fi exit 0 endef -$(eval $(call BuildPackage,luci-app-adguardhome)) +$(eval $(call BuildPackage,luci-app-adguardhome-tk)) + +$(eval $(call BuildPackage,luci-i18n-adguardhome-tk-zh-cn)) diff --git a/luci-app-adguardhome/docs/README.en.md b/luci-app-adguardhome/docs/README.en.md index 4145f130..1dc3727a 100644 --- a/luci-app-adguardhome/docs/README.en.md +++ b/luci-app-adguardhome/docs/README.en.md @@ -17,7 +17,7 @@ Manage AdGuard Home in OpenWrt's LuCI interface. LuCI pages are rewritten in Jav > Run in the terminal of an OpenWrt router using the APK package manager: > > ```sh -> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/install.sh && sh /tmp/install-adguardhome.sh +> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh > ``` ## Screenshots diff --git a/luci-app-adguardhome/docs/README.md b/luci-app-adguardhome/docs/README.md index d3aa8d9d..ae6ad614 100644 --- a/luci-app-adguardhome/docs/README.md +++ b/luci-app-adguardhome/docs/README.md @@ -17,7 +17,7 @@ > 在使用 APK 包管理器的 OpenWrt 路由器终端执行: > > ```sh -> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/install.sh && sh /tmp/install-adguardhome.sh +> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh zh-cn > ``` ## 截图 diff --git a/luci-app-adguardhome/install.sh b/luci-app-adguardhome/install.sh deleted file mode 100644 index 325cbd09..00000000 --- a/luci-app-adguardhome/install.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh -set -e - -REPO="w9315273/luci-app-adguardhome" - -echo "Getting latest version..." -TAG=$(uclient-fetch -qO- "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null \ - | sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) - -[ -n "$TAG" ] || { echo "ERROR: Failed to get latest version"; exit 1; } - -case "$TAG" in - *-*) ;; - *) echo "ERROR: Unexpected tag format: $TAG"; exit 1 ;; -esac - -VERSION=${TAG#v} -VERSION=${VERSION%-*}-r${VERSION##*-} - -URL="https://github.com/${REPO}/releases/download/${TAG}/luci-app-adguardhome-${VERSION}.apk" - -echo "Version: $VERSION" -echo "Downloading..." -trap 'rm -f /tmp/agh.apk' EXIT -uclient-fetch -qO /tmp/agh.apk "$URL" - -echo "Installing..." -apk add --allow-untrusted /tmp/agh.apk - -echo "Refreshing LuCI..." -rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null -/etc/init.d/rpcd reload >/dev/null 2>&1 - -echo "Done." \ No newline at end of file diff --git a/luci-app-adguardhome/scripts/build-apk.sh b/luci-app-adguardhome/scripts/build-apk.sh new file mode 100755 index 00000000..612df72f --- /dev/null +++ b/luci-app-adguardhome/scripts/build-apk.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$SDK_DIR" + +mkdir -p package/${PKG_DIR_NAME} +rsync -a --delete "$GITHUB_WORKSPACE/" "package/${PKG_DIR_NAME}/" \ + --exclude '.git*' --exclude '.local' --exclude '_sdk' --exclude 'dist' + +./scripts/feeds update -a +./scripts/feeds install luci-base tar + +touch .config +sed -i -E '/^(# )?CONFIG_(ALL|ALL_KMODS|ALL_NONSHARED|PACKAGE_TAR_XZ|PACKAGE_luci-app-adguardhome-tk|PACKAGE_luci-i18n-adguardhome-tk-zh-cn)(=| is not set)/d' .config +printf '%s\n' \ + 'CONFIG_ALL=n' \ + 'CONFIG_ALL_KMODS=n' \ + 'CONFIG_ALL_NONSHARED=n' \ + '# CONFIG_PACKAGE_TAR_XZ is not set' \ + 'CONFIG_PACKAGE_luci-app-adguardhome-tk=y' \ + 'CONFIG_PACKAGE_luci-i18n-adguardhome-tk-zh-cn=y' >> .config +make defconfig +if ! grep -Eq '^CONFIG_PACKAGE_luci-app-adguardhome-tk=(y|m)$' .config; then + echo '::error::luci-app-adguardhome-tk was not enabled after make defconfig' + grep -n 'luci-app-adguardhome-tk' .config || true + sed -n '/^[[:space:]]*config PACKAGE_luci-app-adguardhome-tk$/,/^[[:space:]]*help/p' tmp/.config-package.in + exit 1 +fi + +grep -Eq '^CONFIG_PACKAGE_luci-i18n-adguardhome-tk-zh-cn=(y|m)$' .config || { + echo '::error::Simplified Chinese translation package was not enabled' + exit 1 +} + +make package/compile -j"$(nproc)" V=sc diff --git a/luci-app-adguardhome/scripts/collect-apks.sh b/luci-app-adguardhome/scripts/collect-apks.sh new file mode 100755 index 00000000..b9067ad2 --- /dev/null +++ b/luci-app-adguardhome/scripts/collect-apks.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail +mkdir -p "$GITHUB_WORKSPACE/dist" +for package in "$PKG_DIR_NAME" luci-i18n-adguardhome-tk-zh-cn; do + mapfile -t packages < <(find "$SDK_DIR/bin/packages" -type f -name "${package}-*.apk") + if (( ${#packages[@]} == 0 )); then + echo "::error::Missing APK: ${package}" + exit 1 + fi + cp "${packages[@]}" "$GITHUB_WORKSPACE/dist/" +done diff --git a/luci-app-adguardhome/scripts/install.sh b/luci-app-adguardhome/scripts/install.sh new file mode 100644 index 00000000..54121919 --- /dev/null +++ b/luci-app-adguardhome/scripts/install.sh @@ -0,0 +1,86 @@ +#!/bin/sh +set -e + +REPO="w9315273/luci-app-adguardhome" +LANGUAGE=${1:-en} +case "$LANGUAGE" in + en|zh-cn) ;; + *) echo "Usage: sh install.sh [en|zh-cn]"; exit 1 ;; +esac +[ "$#" -le 1 ] || { echo "Usage: sh install.sh [en|zh-cn]"; exit 1; } + +echo "Getting latest version..." +RELEASE=$(uclient-fetch -qO- "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null) +TAG=$(printf '%s\n' "$RELEASE" | sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) + +[ -n "$TAG" ] || { echo "ERROR: Failed to get latest version"; exit 1; } + +case "$TAG" in + *-*) ;; + *) echo "ERROR: Unexpected tag format: $TAG"; exit 1 ;; +esac + +VERSION=${TAG#v} +VERSION=${VERSION%-*}-r${VERSION##*-} + +PACKAGE=luci-app-adguardhome +I18N_PACKAGE=luci-i18n-adguardhome-zh-cn +if printf '%s\n' "$RELEASE" | grep -Eq '"name"[[:space:]]*:[[:space:]]*"luci-app-adguardhome-tk-[^"]*\.apk"'; then + PACKAGE=luci-app-adguardhome-tk + I18N_PACKAGE=luci-i18n-adguardhome-tk-zh-cn +fi +URL="https://github.com/${REPO}/releases/download/${TAG}/${PACKAGE}-${VERSION}.apk" + +echo "Version: $VERSION" +echo "Downloading..." +work=$(mktemp -d /tmp/agh-install.XXXXXX) +trap 'rm -rf "$work"' EXIT +uclient-fetch -qO "$work/main.apk" "$URL" +set -- "$work/main.apk" + +I18N="${I18N_PACKAGE}-${VERSION}.apk" +has_i18n=0 +if [ "$LANGUAGE" = zh-cn ] && printf '%s\n' "$RELEASE" | grep -Eq "\"name\"[[:space:]]*:[[:space:]]*\"${I18N_PACKAGE}-[^\"]*\\.apk\""; then + uclient-fetch -qO "$work/zh-cn.apk" "https://github.com/${REPO}/releases/download/${TAG}/${I18N}" + set -- "$@" "$work/zh-cn.apk" + has_i18n=1 +fi + +if [ "$LANGUAGE" = zh-cn ] && [ "$PACKAGE" = luci-app-adguardhome-tk ] && [ "$has_i18n" != 1 ]; then + echo "ERROR: The release is missing the Chinese package" + exit 1 +fi + +legacy_packages= +if [ "$PACKAGE" = luci-app-adguardhome-tk ]; then + installed=$(apk info) + for old in luci-app-adguardhome luci-i18n-adguardhome-zh-cn; do + if printf '%s\n' "$installed" | grep -qx "$old"; then + if [ "$old" != luci-i18n-adguardhome-zh-cn ] || [ "$LANGUAGE" = zh-cn ]; then + set -- "$@" "$old" + fi + legacy_packages="$legacy_packages $old" + fi + done +fi + +echo "Installing..." +apk add --allow-untrusted "$@" +if [ -n "$legacy_packages" ]; then + apk del $legacy_packages +fi + +if [ "$LANGUAGE" = en ]; then + installed=$(apk info) + for translation in luci-i18n-adguardhome-zh-cn luci-i18n-adguardhome-tk-zh-cn; do + if printf '%s\n' "$installed" | grep -qx "$translation"; then + apk del "$translation" + fi + done +fi + +echo "Refreshing LuCI..." +rm -f /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache/* 2>/dev/null +/etc/init.d/rpcd reload >/dev/null 2>&1 + +echo "Done." diff --git a/luci-app-adguardhome/scripts/prepare-sdk.sh b/luci-app-adguardhome/scripts/prepare-sdk.sh new file mode 100755 index 00000000..f1c2f0e6 --- /dev/null +++ b/luci-app-adguardhome/scripts/prepare-sdk.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$SDK_DIR" + +if [[ ! -x scripts/feeds ]]; then + runuser -u buildbot -- bash ./setup.sh +fi + +mkdir -p dl +chown -R buildbot:buildbot dl diff --git a/luci-app-chatgpt-web/.github/workflows/main.yml b/luci-app-chatgpt-web/.github/workflows/main.yml deleted file mode 100644 index 3de4bc6d..00000000 --- a/luci-app-chatgpt-web/.github/workflows/main.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Build - -on: - push: - tags: - - v* - -jobs: - build: - name: Build ${{ matrix.arch }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - arch: - - - aarch64_generic - - i386_pentium-mmx - - loongarch64_generic - - mips_mips32 - - mipsel_24kc - - mipsel_mips32 - - x86_64 - - steps: - - uses: actions/checkout@main - with: - fetch-depth: 0 - - name: Building packages - uses: sbwml/openwrt-gh-action-sdk@go1.24 - env: - ARCH: ${{ matrix.arch }}-openwrt-24.10 - FEEDNAME: packages_ci - PACKAGES: luci-app-chatgpt-web - NO_REFRESH_CHECK: true - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.arch }} - path: bin/packages/${{ matrix.arch }}/packages_ci/*.ipk - - - name: Upload packages - uses: ncipollo/release-action@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - allowUpdates: true - replacesArtifacts: true - artifacts: "bin/packages/${{ matrix.arch }}/packages_ci/*.ipk" diff --git a/luci-app-chatgpt-web/Makefile b/luci-app-chatgpt-web/Makefile deleted file mode 100644 index b5cae97b..00000000 --- a/luci-app-chatgpt-web/Makefile +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-only -# -# Copyright (C) 2023-2025 sirpdboy https://github.com/sirpdboy/luci-app-chatgpt-web -# -# This is free software, licensed under the Apache License, Version 2.0 . -# -include $(TOPDIR)/rules.mk - -NAME:=chatgpt-web -PKG_NAME:=luci-app-$(NAME) -PKG_VERSION:=1.2.1 -PKG_RELEASE:=16 - -LUCI_TITLE:=LuCI Support for chatgpt-web Client -LUCI_PKGARCH:=all - -define Package/$(PKG_NAME)/conffiles -/etc/config/chatgpt-web -endef -include $(TOPDIR)/feeds/luci/luci.mk - -# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-chatgpt-web/README.md b/luci-app-chatgpt-web/README.md deleted file mode 100644 index ab5de559..00000000 --- a/luci-app-chatgpt-web/README.md +++ /dev/null @@ -1,179 +0,0 @@ -## 访问数:![hello](https://views.whatilearened.today/views/github/sirpdboy/deplives.svg)[![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ) - - - -

-
luci-app-chatgpt-web
-

- -

-OpenWrt -Chrome -Safari -Firefox - GitHub release -Downloads -

- - -![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg) - -请 **认真阅读完毕** 本页面,本页面包含注意事项和如何使用。 - -## 功能说明 - -luci-app-chatgpt Pure ChatGPT by OPENWRT based on OpenAI API - -luci-app-chatgpt 在OPENWRT上实现ChatGPT项目,基于OpenAI API - - - -- [luci-app-chatgpt](#功能说明) - - [特性](#特性) - - [编译方法](#编译方法) - - [使用方法](#使用方法) - - [自定义选项](#自定义选项)。 - - [说明](#说明) - - [界面](#界面) - - [捐助](#捐助) - - - -## 特性 - -- 无须第三依赖,直接使用官方API接口。 -- 支持复制/更新/刷新会话,语音输入,朗读等功能,以及众多[自定义选项](#自定义选项)。 -- 支持搜索会话,导入导出会话和设置,自定义头像,自定义系统角色等。 - -参考项目: -[markdown-it](https://github.com/markdown-it/markdown-it), -[highlight.js](https://github.com/highlightjs/highlight.js), -[github-markdown-css](https://github.com/sindresorhus/github-markdown-css), -[chatgpt-html](https://github.com/slippersheepig/chatgpt-html), -[markdown-it-copy](https://github.com/ReAlign/markdown-it-copy), -[markdown-it-texmath](https://github.com/goessner/markdown-it-texmath), -[awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) - - -## 编译方法 - -- 将luci-app-chatgpt添加至 LEDE/OpenWRT 源码的方法。 - -### 下载源码方法: - - ```Brach - - # 下载源码 - - git clone https://github.com/sirpdboy/chatgpt-web.git package/luci-app-chatgpt - make menuconfig - - ``` -### 配置菜单 - - ```Brach - make menuconfig - # 找到 LuCI -> Applications, 选择 luci-app-chatgpt, 保存后退出。 - ``` - -### 编译 - - ```Brach - # 编译固件 - make package/luci-app-chatgpt/compile V=s - ``` - -## 使用方法 - - - 打开luci-app-chatgpt基本设置,填写API密钥、GPT模型、填写OpenAI接口、选择用户图像等。 - - - 可选GPT模型,默认gpt-3.5,当前使用gpt-4模型需通过openai的表单申请。 - - - OpenAI接口当本地可正常访问`api.openai.com`,填写`https://api.openai.com/` - - - 不可正常访问`api.openai.com`,填写其反代地址,注意:反代接口响应需添加跨域Header `Access-Control-Allow-Origin` - - - chatgpt-web页面就可以正常使用了,如果需要设置更多请看 自定义设置设置语音和系统角色等。 - -## 自定义选项 - -- 左边栏支持,清除对话,搜索会话,新建/重命名/删除(会话/文件夹)导出/导入/重置会话和设置数据,显示本地存储。 - -- 可选系统角色,默认不开启,有四个预设角色,后期会加入更多角色。 - -- 可选角色性格,默认灵活创新,对应接口文档的top_p参数。 - -- 可选回答质量,默认平衡,对应接口文档的temperature参数。 - -- 修改打字机速度,默认较快,值越大速度越快。 - -- 允许连续对话,默认开启,对话中包含上下文信息,会导致api费用增加。 - -- 允许长回复,默认关闭,**开启后可能导致api费用增加,并丢失大部分上下文,对于一些要发送`继续`才完整的回复,不用发`继续`了。** - -- 选择语音,默认Bing语音,支持Azure语音和系统语音,可分开设置提问语音和回答语音。 - -- 音量,默认最大。 - -- 语速,默认正常。 - -- 音调,默认正常。 - -- 允许连续朗读,默认开启,连续郎读到所有对话结束。 - -- 允许自动朗读,默认关闭,自动朗读新的回答。**(iOS需打开设置-自动播放视频预览,Mac上Safari需打开此网站的设置-允许全部自动播放)** - -- 支持语音输入,默认识别为普通话,可长按语音按钮修改识别选项。**语音识别必需条件:使用chrome内核系浏览器 + https网页或本地网页。** 如点击语音按钮没反应,可能是未授予麦克风权限或者没安装麦克风设备。 - - -## 说明 - --源码来源:https://github.com/sirpdboy/chatgpt-web --你可以随意使用其中的源码,但请注明出处。 - -![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明2.jpg) - -## 界面 - -![screenshots](./doc/chatgpt1.png) - -![screenshots](./doc/chatgpt2.png) - -![screenshots](./doc/chatgpt3.png) - - -## 使用与授权相关说明 - -- 本人开源的所有源码,任何引用需注明本处出处,如需修改二次发布必告之本人,未经许可不得做于任何商用用途。 - -# My other project - -- 路由安全看门狗 :https://github.com/sirpdboy/luci-app-watchdog -- 网络速度测试 :https://github.com/sirpdboy/luci-app-netspeedtest -- 计划任务插件(原定时设置) : https://github.com/sirpdboy/luci-app-taskplan -- 关机功能插件 : https://github.com/sirpdboy/luci-app-poweroffdevice -- opentopd主题 : https://github.com/sirpdboy/luci-theme-opentopd -- kucat酷猫主题: https://github.com/sirpdboy/luci-theme-kucat -- kucat酷猫主题设置工具: https://github.com/sirpdboy/luci-app-kucat-config -- NFT版上网时间控制插件: https://github.com/sirpdboy/luci-app-timecontrol -- 家长控制: https://github.com/sirpdboy/luci-theme-parentcontrol -- 定时限速: https://github.com/sirpdboy/luci-app-eqosplus -- 系统高级设置 : https://github.com/sirpdboy/luci-app-advanced -- ddns-go动态域名: https://github.com/sirpdboy/luci-app-ddns-go -- 进阶设置(系统高级设置+主题设置kucat/agron/opentopd): https://github.com/sirpdboy/luci-app-advancedplus -- 网络设置向导: https://github.com/sirpdboy/luci-app-netwizard -- 一键分区扩容: https://github.com/sirpdboy/luci-app-partexp -- lukcy大吉: https://github.com/sirpdboy/luci-app-lukcy - -## 捐助 - -![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明3.jpg) - -| 图飞了😂 | 图飞了😂 | -| :-----------------: | :-------------: | -|![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) | - - - 图飞了😂 - - diff --git a/luci-app-chatgpt-web/doc/chatgpt1.png b/luci-app-chatgpt-web/doc/chatgpt1.png deleted file mode 100644 index f715b006..00000000 Binary files a/luci-app-chatgpt-web/doc/chatgpt1.png and /dev/null differ diff --git a/luci-app-chatgpt-web/doc/chatgpt2.png b/luci-app-chatgpt-web/doc/chatgpt2.png deleted file mode 100644 index 7de824a2..00000000 Binary files a/luci-app-chatgpt-web/doc/chatgpt2.png and /dev/null differ diff --git a/luci-app-chatgpt-web/doc/chatgpt3.png b/luci-app-chatgpt-web/doc/chatgpt3.png deleted file mode 100644 index 9d280941..00000000 Binary files a/luci-app-chatgpt-web/doc/chatgpt3.png and /dev/null differ diff --git a/luci-app-chatgpt-web/luasrc/controller/chatgpt-web.lua b/luci-app-chatgpt-web/luasrc/controller/chatgpt-web.lua deleted file mode 100644 index c100132c..00000000 --- a/luci-app-chatgpt-web/luasrc/controller/chatgpt-web.lua +++ /dev/null @@ -1,17 +0,0 @@ --- Copyright (C) 2023-2025 sirpdboy https://github.com/sirpdboy/luci-app-chatgpt-web --- Licensed to the public under the Apache License 2.0. - -module("luci.controller.chatgpt-web", package.seeall) - -function index() - if not nixio.fs.access("/etc/config/chatgpt-web") then - return - end - - local e = entry({"admin", "services", "chatgpt-web"}, alias("admin", "services", "chatgpt-web", "setting"),_("Chatgpt Web"), 20) - e.dependent=false - e.acl_depends = { "luci-app-chatgpt-web" } - entry({"admin", "services", "chatgpt-web", "setting"}, cbi("chatgpt-web"), _("Base Setting"), 10).leaf=true - entry({"admin", "services", "chatgpt-web", "chatgpt-web"}, cbi("chatgpt",{hideapplybtn=true, hidesavebtn=true, hideresetbtn=true}), _("Chatgpt Web"), 30).leaf=true -end - diff --git a/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt-web.lua b/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt-web.lua deleted file mode 100644 index 38f5e7da..00000000 --- a/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt-web.lua +++ /dev/null @@ -1,36 +0,0 @@ --- Copyright (C) 2023-2025 sirpdboy https://github.com/sirpdboy/https://github.com/sirpdboy/luci-app-chatgpt-web - -local m, s ,o - -m = Map("chatgpt-web") -m.title = translate("Chatgpt Web") -m.description = translate("ChatGPT-Web is the version used on OPENWRT of OPENAI")..translate("
For specific usage, see:")..translate("GitHub @sirpdboy/chatgpt-web\ ") - -s = m:section(TypedSection, "basic", translate("Base Setting")) -s.addremove = false -s.anonymous = true - - -o = s:option(Value,"userpic",translate("Character images")) -o:value("girl.jpg",translate("girl")) -o:value("girl2.jpg",translate("girl2")) -o:value("girl3.jpg",translate("girl3")) -o:value("boy.jpg",translate("boy")) -o:value("boy2.jpg",translate("boy2")) -o:value("boy3.jpg",translate("boy3")) -o:value("dog.jpg",translate("dog")) -o:value("cartoon.jpg",translate("cartoon")) -o:value("cartoonboy.jpg",translate("cartoonboy")) -o:value("cartoonboy2.jpg",translate("cartoonboy2")) -o:value("cartoongirl.jpg",translate("cartoongirl")) -o:value("cartoongirl2.jpg",translate("cartoongirl2")) -o.rmempty = true -o.default="girl.jpg" - - -o=s:option(TextValue, "systemrole" ,translate("Default Role Settings")) -o.rows = 5 -o.rmempty=false - - -return m diff --git a/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt.lua b/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt.lua deleted file mode 100644 index 162de6d0..00000000 --- a/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt.lua +++ /dev/null @@ -1,9 +0,0 @@ --- Copyright (C) 2023-2025 sirpdboy https://github.com/sirpdboy/luci-app-chatgpt-web -require("luci.util") -local m, s - -m = Map("chatgpt-web",translate("")) - -m:section(SimpleSection).template = "chatgpt-web" - -return m diff --git a/luci-app-chatgpt-web/luasrc/view/chatgpt-web.htm b/luci-app-chatgpt-web/luasrc/view/chatgpt-web.htm deleted file mode 100644 index 078e5ac7..00000000 --- a/luci-app-chatgpt-web/luasrc/view/chatgpt-web.htm +++ /dev/null @@ -1,6845 +0,0 @@ -<%# - Copyright 2023-2025 sirpdboy Wich https://github.com/sirpdboy/luci-app-chatgpt-web - Licensed to the public under the Apache License 2.0. --%> -<% -local fs = require "nixio.fs" -local dsp = require "luci.dispatcher" -local uci = luci.model.uci.cursor() -local apikeys = uci:get_first('chatgpt-web', 'basic', 'apikey') -local userpic = "/luci-static/chatgpt-web/user/" .. uci:get_first('chatgpt-web', 'basic', 'userpic') -local sysrole = uci:get_first('chatgpt-web', 'basic', 'systemrole') - --%> - -
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
ChatGPT
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- - 4o - - -
-
-
- - -
- - -
-
-
-
- -
-
-
-
- - -
- - -
-
-
-
-
-
- - - -
-
-
-
-
- - - -
-
- - - -
-
-
-
-
-
ChatGPT
-
DeepSeek
-
Azure
-
Gemini
-
Claude
-
-
- -
-
-
-
ChatGPT
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
- - - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - - -
\ No newline at end of file diff --git a/luci-app-chatgpt-web/po/zh-cn/chatgpt-web.po b/luci-app-chatgpt-web/po/zh-cn/chatgpt-web.po deleted file mode 100644 index 91654dbd..00000000 --- a/luci-app-chatgpt-web/po/zh-cn/chatgpt-web.po +++ /dev/null @@ -1,71 +0,0 @@ -msgid "" -msgstr "Content-Type: text/plain; charset=UTF-8" - -msgid "chatgpt-web" -msgstr "ChatGPT-Web" - -msgid "ChatGPT-Web is the version used on OPENWRT of OPENAI" -msgstr "ChatGPT-Web是OPENAI的OPENWRT上使用的WEB版本,不需要第三方依赖,绿色安全直接使用OPENAI的API接口" - -msgid "Base Setting" -msgstr "基本设置" - -msgid "GPT Model" -msgstr "GPT模型" - -msgid "Character images" -msgstr "人物图像" - -msgid "boy" -msgstr "男生" - -msgid "boy2" -msgstr "男生2" - -msgid "boy3" -msgstr "男生3" - -msgid "girl" -msgstr "女生" - -msgid "girl2" -msgstr "女生2" - -msgid "girl3" -msgstr "女生3" - -msgid "dog" -msgstr "狗" - -msgid "cartoon" -msgstr "卡通图像" - -msgid "cartoonboy" -msgstr "卡通男孩" - -msgid "cartoonboy2" -msgstr "卡通小男孩" - -msgid "cartoongirl" -msgstr "卡通女生" - -msgid "cartoongirl2" -msgstr "卡通女生2" - -msgid "OpenAI interface" -msgstr "OpenAI接口" - -msgid "API Key" -msgstr "API密钥" - -msgid "Enter your own API key, and if you do not have one, enter the official website API to generate it:" -msgstr "输入自己的API密钥(sk-开头),没有的进官网API生成:" - -msgid "Click to open" -msgstr "点击前往" - -msgid "Default Role Settings" -msgstr "默认角色设置" - -msgid "
For specific usage, see:" -msgstr "
具体使用方法参见:" diff --git a/luci-app-chatgpt-web/po/zh_Hans/chatgpt-web.po b/luci-app-chatgpt-web/po/zh_Hans/chatgpt-web.po deleted file mode 100644 index 91654dbd..00000000 --- a/luci-app-chatgpt-web/po/zh_Hans/chatgpt-web.po +++ /dev/null @@ -1,71 +0,0 @@ -msgid "" -msgstr "Content-Type: text/plain; charset=UTF-8" - -msgid "chatgpt-web" -msgstr "ChatGPT-Web" - -msgid "ChatGPT-Web is the version used on OPENWRT of OPENAI" -msgstr "ChatGPT-Web是OPENAI的OPENWRT上使用的WEB版本,不需要第三方依赖,绿色安全直接使用OPENAI的API接口" - -msgid "Base Setting" -msgstr "基本设置" - -msgid "GPT Model" -msgstr "GPT模型" - -msgid "Character images" -msgstr "人物图像" - -msgid "boy" -msgstr "男生" - -msgid "boy2" -msgstr "男生2" - -msgid "boy3" -msgstr "男生3" - -msgid "girl" -msgstr "女生" - -msgid "girl2" -msgstr "女生2" - -msgid "girl3" -msgstr "女生3" - -msgid "dog" -msgstr "狗" - -msgid "cartoon" -msgstr "卡通图像" - -msgid "cartoonboy" -msgstr "卡通男孩" - -msgid "cartoonboy2" -msgstr "卡通小男孩" - -msgid "cartoongirl" -msgstr "卡通女生" - -msgid "cartoongirl2" -msgstr "卡通女生2" - -msgid "OpenAI interface" -msgstr "OpenAI接口" - -msgid "API Key" -msgstr "API密钥" - -msgid "Enter your own API key, and if you do not have one, enter the official website API to generate it:" -msgstr "输入自己的API密钥(sk-开头),没有的进官网API生成:" - -msgid "Click to open" -msgstr "点击前往" - -msgid "Default Role Settings" -msgstr "默认角色设置" - -msgid "
For specific usage, see:" -msgstr "
具体使用方法参见:" diff --git a/luci-app-chatgpt-web/root/etc/config/chatgpt-web b/luci-app-chatgpt-web/root/etc/config/chatgpt-web deleted file mode 100644 index a7a1636a..00000000 --- a/luci-app-chatgpt-web/root/etc/config/chatgpt-web +++ /dev/null @@ -1,8 +0,0 @@ - -config basic - option apiHost 'https://api.openai.com/' - option modelver 'gpt-3.5-turbo' - option userpic 'girl.jpg' - option apikey '' - option systemrole '你是一个乐于助人的专业助手,尽量简明扼要地回答' - diff --git a/luci-app-chatgpt-web/root/usr/share/rpcd/acl.d/luci-app-chatgpt-web.json b/luci-app-chatgpt-web/root/usr/share/rpcd/acl.d/luci-app-chatgpt-web.json deleted file mode 100644 index c8c06f50..00000000 --- a/luci-app-chatgpt-web/root/usr/share/rpcd/acl.d/luci-app-chatgpt-web.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "luci-app-chatgpt-web": { - "description": "Grant UCI access for luci-app-chatgpt-web", - "read": { - "uci": [ "chatgpt-web" ] - }, - "write": { - "uci": [ "chatgpt-web" ] - } - } -} diff --git a/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/11.11.1github.min.css b/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/11.11.1github.min.css deleted file mode 100644 index 275239a7..00000000 --- a/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/11.11.1github.min.css +++ /dev/null @@ -1,10 +0,0 @@ -pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! - Theme: GitHub - Description: Light theme as seen on github.com - Author: github.com - Maintainer: @Hirse - Updated: 2021-05-15 - - Outdated base version: https://github.com/primer/github-syntax-light - Current colors taken from GitHub's CSS -*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0} \ No newline at end of file diff --git a/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/chatgpt-web.css b/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/chatgpt-web.css deleted file mode 100644 index 7a24e376..00000000 --- a/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/chatgpt-web.css +++ /dev/null @@ -1,2166 +0,0 @@ - - .chat_window { - display: flex; - width: 100%; - max-width: 1920px; - height: 888px; - border-radius: 8px; - background-color: rgba(255,255,255,0.5); - overflow: auto; - } - - .chat_window>.overlay { - width: 100%; - height: 100%; - position: absolute; - left: 0; - top: 0; - background-color: rgba(0, 0, 0, .3); - z-index: 90; - opacity: 0; - visibility: hidden; - cursor: pointer; - transition: all 120ms ease-in-out; - } - - .chat_window .mainContent .cbi-input-invalid { - color: var(--title-color); - border-bottom-color: var(--title-color); - } - - - @media screen and (min-width: 800px) { - - .mainContent { - width: calc(100% - 250px); - } - - .chat_window>.nav { - position: relative; - margin-left: -250px; - transition: margin-left 250ms; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - } - - .show-nav .nav { - margin-left: 0; - } - - #sysDialog { - max-width: 600px; - } - - .sysContent { - display: flex; - } - - .sysSwitch { - flex-shrink: 0; - width: 160px; - } - - .sysSwitch>div { - padding-left: 6px; - } - - .sysDetail { - margin-left: 12px; - margin-top: 5px; - } - } - - @media screen and (max-width: 800px) { - - .mainContent { - width: 100%; - } - - .chat_window>.nav { - position: absolute !important; - left: -250px; - transition: left 250ms; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - } - - .show-nav .nav { - left: 0; - box-shadow: rgba(0, 0, 0, 0.2) 0px 8px 10px -5px, rgba(0, 0, 0, 0.14) 0px 16px 24px 2px, rgba(0, 0, 0, 0.12) 0px 6px 30px 5px; - } - - .show-nav .overlay { - visibility: visible; - opacity: 1; - } - - #sysDialog { - max-width: 400px; - } - - .sysSwitch { - display: flex; - } - - .sysSwitch>div { - width: 50%; - justify-content: center; - } - - .sysDetail { - margin-top: 8px; - } - } - .full_window { - max-width: none; - max-height: none; - } - - .chat_window>.nav { - width: 250px; - background-color: rgba(var(--primary-rgbbody),1); - color: var(--body-color); - top: 0; - z-index: 99; - flex-shrink: 0; - display: flex; - flex-direction: column; - margin-left: -250px; - transition: left 120ms ease-in-out; - } - - .mainContent { - height: 100%; - position: relative; - width: 100%; - display: flex; - flex-direction: column; - flex: 1; - } - - .show-nav .nav { - margin-left: 0; - } - .show-nav .chatnav { - margin-left: 0; - box-shadow: rgba(0, 0, 0, 0.2) 0px 8px 10px -5px, rgba(0, 0, 0, 0.14) 0px 16px 24px 2px, rgba(0, 0, 0, 0.12) 0px 6px 30px 5px; - } - - .show-nav .overlay { - opacity: 1; - visibility: visible; - } - .top_menu { - width: 100%; - background-color: rgba(255,255,255,0.5); - height: 50px; - padding: 5px 0; - } - - .top_menu .toggler { - margin-left: 10px; - width: 40px; - height: 40px; - float: left; - padding: 5px 7px; - border-radius: 4px; - cursor: pointer; - -webkit-tap-highlight-color: transparent; - } - - .top_menu .toggler:hover { - background: #e0e0e0; - } - - .top_menu .toggler .button { - width: 26px; - height: 4px; - border-radius: 4px; - position: absolute; - pointer-events: none; - } - - .top_menu .toggler .button.close { - margin-top: 3px; - background-color: #000; - } - - .top_menu .toggler .button.minimize { - margin-top: 13px; - background-color: #000; - } - - .top_menu .toggler .button.maximize { - margin-top: 23px; - background-color: #000; - } - - .top_menu .title { - font-size: 20px; - height: 40px; - line-height: 40px; - position: relative; - pointer-events: none; - } - - #selector { - position: absolute; - top: 50%; - left: 50%; - padding: 0 4px 0 10px; - cursor: pointer; - font-size: 20px; - transform: translate(-50%, -50%); - pointer-events: auto; - border-radius: 4px; - -webkit-user-select: none; - user-select: none; - display: flex; - align-items: center; - } - - #selector:hover { - color: var(--title-color); - } - - #modelVer { - margin-left: 6px; - margin-right: 4px; - height: 40px; - line-height: 38px; - } - - .messages { - position: relative; - flex: 1; - overflow-x: hidden; - overflow-y: auto; - font-size: 16px; - width: 100%; - text-align: center; - } - - .messages::-webkit-scrollbar, - - #chatlog .markdown-body>pre::-webkit-scrollbar, - #chatlog .markdown-body>pre>code::-webkit-scrollbar, - #chatlog .responseBody pre>code::-webkit-scrollbar, - #setDialog::-webkit-scrollbar, - .allList::-webkit-scrollbar, - .sysDetail::-webkit-scrollbar, - #apiSelect::-webkit-scrollbar { - width: 10px; - height: 10px; - } - - .messages::-webkit-scrollbar-track, - - #chatlog .markdown-body>pre::-webkit-scrollbar-track, - #chatlog .markdown-body>pre>code::-webkit-scrollbar-track, - #chatlog .responseBody pre>code::-webkit-scrollbar-track, - #setDialog::-webkit-scrollbar-track, - .allList::-webkit-scrollbar-track, - .sysDetail::-webkit-scrollbar-track, - #apiSelect::-webkit-scrollbar-track { - background-clip: padding-box; - background: transparent; - border: none; - } - - .messages::-webkit-scrollbar-corner, - - #chatlog .markdown-body>pre::-webkit-scrollbar-corner, - #chatlog .markdown-body>pre>code::-webkit-scrollbar-corner, - #chatlog .responseBody pre>code::-webkit-scrollbar-corner, - #setDialog::-webkit-scrollbar-corner, - .allList::-webkit-scrollbar-corner, - .sysDetail::-webkit-scrollbar-corner, - #apiSelect::-webkit-scrollbar-corner { - background-color: transparent; - } - - .messages::-webkit-scrollbar-thumb, - - #chatlog .markdown-body>pre::-webkit-scrollbar-thumb, - #chatlog .markdown-body>pre>code::-webkit-scrollbar-thumb, - #chatlog .responseBody pre>code::-webkit-scrollbar-thumb, - #setDialog::-webkit-scrollbar-thumb, - .allList::-webkit-scrollbar-thumb, - .sysDetail::-webkit-scrollbar-thumb, - #apiSelect::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.1); - background-clip: padding-box; - border: solid transparent; - border-radius: 10px; - } - - .messages::-webkit-scrollbar-thumb:hover, - - #chatlog .markdown-body>pre::-webkit-scrollbar-thumb:hover, - #chatlog .markdown-body>pre>code::-webkit-scrollbar-thumb:hover, - #chatlog .responseBody pre>code::-webkit-scrollbar-thumb:hover, - #setDialog::-webkit-scrollbar-thumb:hover, - .allList::-webkit-scrollbar-thumb:hover, - .sysDetail::-webkit-scrollbar-thumb:hover, - #apiSelect::-webkit-scrollbar-thumb:hover { - background-color: rgba(0, 0, 0, 0.4); - } - - #chatlog { - word-wrap: break-word; - text-align: start; - } - - #chatlog>div { - padding: 18px 40px 18px 54px; - } - .chatAvatar { - position: absolute; - width: 30px; - height: 30px; - border-radius: 2px; - } - - .request>.chatAvatar { - top: 18px; - left: 13px; - margin-top: 15px; - margin-bottom: 15px; - } - .response>.chatAvatar { - background: #10a37f; - top: 18px; - left: 13px; - margin-top: 15px; - margin-bottom: 15px; - } - - .gptAvatar>svg { - margin-top: 3px; - margin-left: 3px; - } - - .chatAvatar>img { - display: block; - width: 100%; - height: 100%; - border-radius: 2px; - } - - .modelAvatar>svg, - .chatAvatar>svg { - display: block; - margin-top: 4px; - margin-left: 4px; - } - - #chatlog .request { - position: relative; - background-color: var(--inputborder-color); - display: flex; - justify-content: center; - } - - - .response .responseBody { - display: flex; - justify-content: flex-end; - margin: 18px 45px 18px 20px; - font-size: 15px; - - } - .requestBody { - white-space: pre-wrap; - margin: 18px; - flex: 1; - } - - #chatlog .response { - background-color: var(--inputborder-color); - position: relative; - } - - .response .markdown-body { - margin: 18px 65px 18px 0px; - flex: 1; - font-size: 15px; - background-color: rgba(255,255,255,0)!important; - - } - - #chatlog .markdown-body a { - padding: 0 1px 0 2px; - } - - #chatlog .markdown-body pre { - overflow-x: auto; - padding: 10px; - position: relative; - white-space: pre-wrap; - background-color: var(--inputborder-color)!important; - - } - - #chatlog .markdown-body .mermaid { - margin-bottom: 8px; - padding: 0 10px 0 10px; - position: relative; - border-radius: 6px; - } - - .mermaid>svg { - cursor: pointer; - max-height: 600px; - display: block; - } - - #chatlog .markdown-body pre>code { - overflow-x: auto; - white-space: pre-wrap; - display: block; - } - - #chatlog .markdown-body ul { - list-style-type: disc; - } - - .m-mdic-copy-wrapper { - position: absolute; - top: 5px; - right: 16px; - -webkit-user-select: none; - user-select: none; - } - - .m-mdic-copy-wrapper span.u-mdic-copy-code_lang { - position: absolute; - top: 3px; - right: calc(100% + 4px); - font-family: system-ui; - font-size: 12px; - line-height: 18px; - color: #bbb; - } - - .m-mdic-copy-wrapper div.u-mdic-copy-notify { - position: absolute; - top: 0; - right: 0; - padding: 3px 6px; - border: 0; - border-radius: 3px; - background: none; - font-family: system-ui; - font-size: 12px; - line-height: 18px; - color: #555; - outline: none; - right: 100%; - padding-right: 4px; - } - - .m-mdic-copy-wrapper button.u-mdic-copy-btn { - position: relative; - top: 0; - right: 0; - padding: 3px 6px; - border: 0; - border-radius: 3px; - background: none; - font-family: system-ui; - font-size: 12px; - line-height: 18px; - color: #bbb; - outline: none; - cursor: pointer; - transition: color 0.2s; - } - - .m-mdic-copy-wrapper span.u-mdic-copy-code_lang::before, - .m-mdic-copy-wrapper div.u-mdic-copy-notify::before, - .m-mdic-copy-wrapper button.u-mdic-copy-btn::before { - content: attr(text); - } - - .m-mdic-copy-wrapper button.u-mdic-copy-btn:hover { - color: #555; - } - - #stopChat { - display: none; - margin: 0 auto; - margin-top: 3px; - width: 80px; - height: 32px; - text-align: center; - line-height: 32px; - color: white; - background: #f8b26a; - cursor: pointer; - border-radius: 3px; - position: sticky; - bottom: 2px; - justify-content: center; - align-items: center; - } - - #stopChat>svg { - margin-right: 8px; - } - - #stopChat:hover { - background: #f0aa60; - } - - .bottom_wrapper { - position: relative; - width: 100%; - padding: 10px 10px; - max-width: 91%; - margin: 0 auto; - } - - .bottom_wrapper .message_input_wrapper { - width: calc(100% - 134px); - position: relative; - text-align: left; - } - - .bottom_wrapper .message_input_wrapper .message_input_text { - border-radius: 4px; - border: none; - outline: none; - resize: none; - background-color: var(--inputborder-color); - height: 47px; - font-size: 16px; - max-height: 200px; - padding: 13px 0 13px 13px; - width: 100%!important; - display: block; - transition: background-color 250ms; - } - - .bottom_wrapper .message_input_wrapper .message_input_text:focus { - background-color: var(--inputborder-color); - } - - .bottom_wrapper .message_input_wrapper .message_input_text::-webkit-scrollbar { - display: none; - width: 0; - height: 0; - } - - #sendbutton { - width: 70px; - height: 47px; - font-size: 18px; - font-weight: bold; - color: #fff; - background-color: #7ac516; - padding: 0.5rem 0.5rem; - transition: all 0.2s linear; - position: absolute; - right: 65px; - bottom: 10px; - cursor: not-allowed; - } - - .activeSendBtn { - background-color: #99c959 !important; - cursor: pointer !important; - } - - .activeSendBtn:hover { - background-color: #90c050 !important; - } - - .clearConv { - position: absolute; - right: 10px; - bottom: 10px; - width: 47px; - padding: 0.5rem 0.5rem; - height: 47px; - } - - .clearConv>svg { - margin: 0 auto; - top: 50%; - left: 50%; - } - - .clearConv:hover { - background-color: var(--body-hover-bgcolor); - } - - .clearConv svg:first-child { - display: none; - } - - .clearConv svg:nth-child(2) { - display: block; - } - - .closeConv { - background: #e0e0e0;; - } - - .closeConv:hover { - background: #e8e8e8; - } - - .closeConv svg:first-child { - display: block; - } - - .closeConv svg:nth-child(2) { - display: none; - } - - .loaded>span { - display: inline-block; - } - - .loaded>svg { - display: none; - } - - .loading { - background: rgba(0,0,0,0) !important; - } - - .loading>span { - display: none; - } - - .loading>svg { - display: block; - } - - .switch-slide { - display: inline-block; - vertical-align: middle; - } - - .switch-slide-label { - display: block; - width: 38px; - height: 18px; - background: #909090; - border-radius: 30px; - cursor: pointer; - position: relative; - -webkit-transition: 0.3s ease; - transition: 0.3s ease; - } - - .switch-slide-label:after { - content: ""; - display: block; - width: 16px; - height: 16px; - border-radius: 100%; - background: #fff; - box-shadow: 0 1px 1px rgba(0, 0, 0, .1); - position: absolute; - left: 1px; - top: 1px; - -webkit-transform: translateZ(0); - transform: translateZ(0); - -webkit-transition: all 250ms; - transition: all 250ms; - } - - .switch-slide input:checked+label { - background: #99c959; - -webkit-transition: all 250ms; - transition: all 250ms; - } - - .switch-slide input:checked+label:after { - left: 21px; - } - - .settings { - padding: 0px 7px; - margin-top: -40px; - float: right; - border-radius: 4px; - cursor: pointer; - top: 5px; - } - - - .setBtn { - margin-left: 2px; - cursor: pointer; - padding: 5px; - border: none; - border-radius: 4px; - } - - .setBtn>svg { - display: block; - color: #909090; - } - - .setBtn:hover { - background: #e0e0e0;; - } - - #setting { - right: 15px; - } - - #toggleFull { - right: 56px; - } - - #toggleLight *, - #toggleFull * { - pointer-events: none; - } - - .showSetting { - background-color: var(--inputborder-color); - } - - .showModels { - background-color: var(--inputborder-color); - } - - .showModels>svg:last-child { - transform: scaleY(-1); - } - - #modelDialog { - position: absolute; - z-index: 2; - background: rgba(var(--primary-rgbbody), 1); - width: 240px; - left: 50%; - transform: translateX(-50%); - top: 46px; - overflow-y: auto; - max-height: calc(100% - 55px); - -webkit-user-select: none; - user-select: none; - border-radius: 4px; - padding: 8px 5px 8px 5px; - box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); - } - - .modelSingle { - height: 40px; - border-radius: 3px; - cursor: pointer; - line-height: 40px; - display: flex; - align-items: center; - position: relative; - } - - .modelSingle * { - pointer-events: none; - } - - .modelSingle:hover { - background-color: var(--body-hover-bgcolor); - } - - .activeModel::before { - content: ""; - position: absolute; - right: 15px; - top: 7px; - width: 8px; - height: 16px; - border-style: solid; - border-width: 0 4px 4px 0; - transform: rotate(45deg); - } - - .modelAvatar { - margin-left: 6px; - margin-right: 10px; - width: 30px; - flex-shrink: 0; - border-radius: 2px; - height: 30px; - } - - #setDialog { - position: absolute; - z-index: 2; - background-color: rgba(var(--primary-rgbbody),1); - width: 320px; - right: 6px; - top: 46px; - overflow-y: auto; - max-height: calc(100% - 55px); - -webkit-user-select: none; - user-select: none; - border-radius: 5px; - padding: 8px 12px 8px 12px; - box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); - } - - #setDialog input { - width: 100%; - } - - #setDialog .inlineTitle { - display: inline-block; - width: 80px; - line-height: 16px; - vertical-align: middle; - } - - #convOption, - #speechOption, - #speechDetail, - #recOption { - margin-bottom: 6px; - } - - #convOption>div, - #speechOption>div, - #speechDetail>div, - #recOption div { - margin-top: 10px; - } - - #voiceRecSetting select, #speechDetail select { - color: var(--title-color); - - } - .inputTextClass { - outline: none; - border-radius: 2px; - margin-top: 3px; - height: 32px; - font-size: 15px; - padding-left: 6px; - border: none; - } - - .areaTextClass { - width: 100%; - height: 150px; - white-space: normal; - resize: none; - padding: 6px; - } - - input[type="range"] { - -webkit-appearance: none; - appearance: none; - display: block; - margin: 4px 0 3px 0; - height: 8px; - background: #909090; - border-radius: 5px; - background-image: linear-gradient(#99c959, #99c959); - background-size: 100% 100%; - background-repeat: no-repeat; - } - - input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - height: 15px; - width: 15px; - border-radius: 50%; - background: #99c959; - cursor: ew-resize; - } - - input[type=range]::-webkit-slider-runnable-track { - -webkit-appearance: none; - box-shadow: none; - border: none; - background: transparent; - } - - .justSetLine { - display: flex; - justify-content: space-between; - align-items: center; - } - - .justSetBtn { - height: 32px; - border-radius: 3px; - line-height: 32px; - background-color: var(--inputborder-color); - text-align: center; - padding: 0px 8px; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - } - - .justSetBtn:hover { - background-color: #d0d0d0; - } - - .justSetBtn>svg { - margin-right: 3px; - } - - .readyTestVoice>div:not(:first-child) { - display: none; - } - - .pauseTestVoice>div:nth-child(1), - .pauseTestVoice>div:nth-child(3) { - display: none; - } - - .resumeTestVoice>div:nth-child(1), - .resumeTestVoice>div:nth-child(2) { - display: none; - } - - .presetSelect>div { - display: inline-block; - } - - .presetSelect select { - outline: none; - border-radius: 3px; - width: 120px; - border-color: rgba(0, 0, 0, .3); - } - - .selectDef { - display: flex; - justify-content: space-between; - font-size: 13px; - color: #909090; - } - - #preSetSpeech { - width: 100%; - outline: none; - height: 30px; - line-height: 30px; - font-size: 14px; - margin-top: 5px; - border-radius: 3px; - border-color: rgba(0, 0, 0, .3); - } - - .mdOption { - position: absolute; - right: 0px; - top: 0; - width: 50px; - height: 100%; - pointer-events: none; - } - - .mdOption>div { - pointer-events: auto; - cursor: pointer; - } - - .mdOption svg * { - pointer-events: none; - } - - .refreshReq svg:not(:first-child) { - display: none; - } - - .halfRefReq svg:not(:nth-child(2)) { - display: none; - } - - .optionItems { - position: absolute; - bottom: -5px; - display: flex; - justify-content: space-between; - visibility: visible; - z-index: 1; - color: #808080; - } - - .optionItems:hover { - visibility: visible; - } - - .request:hover .optionItems, - .request:hover .voiceCls, - .response:hover .optionItems, - .response:hover .voiceCls { - visibility: visible; - } - - .optionItem { - border-radius: 10px; - height: 24px; - width: 32px; - border: 1px solid #e0e0e0;; - background-color: #f7f7f8; ; - display: flex !important; - justify-content: center; - align-items: center; - } - - .optionItem * { - pointer-events: none; - } - - .optionItem:hover { - background: #e0e0e0;; - } - - .voiceCls { - position: relative; - height: 100%; - visibility: visible; - display: flex; - align-items: center; - } - - .voiceCls>svg { - color: #c0c0c0; - display: block; - margin-left: 5px; - position: relative; - pointer-events: none; - } - - .voiceCls:hover>svg { - color: #808080; - } - - .showVoiceCls, - .showVoiceCls .markdown-body { - background: #e0e0e0 !important; - } - - .showVoiceCls .voiceCls { - visibility: visible !important; - } - - .showEditReq { - position: sticky !important; - top: 0; - bottom: 0; - z-index: 1; - } - - .showEditReq, - .showEditReq .markdown-body { - background: #e0e0e0 !important; - } - - .readyVoice svg:not(:first-child) { - display: none; - } - - .pauseVoice svg:not(:nth-child(2)) { - display: none; - } - - .resumeVoice svg:not(:nth-child(3)) { - display: none; - } - - #voiceTypes>span { - border-radius: 3px; - margin-left: 4px; - cursor: pointer; - color: var(--title-color); - padding: 1px 5px; - } - - #voiceTypes>span:hover { - background-color: var(--body-hover-bgcolor); - } - - .selVoiceType { - background-color: var(--inputborder-color); - } - - .navHeader { - width: 100%; - padding: 5px 10px; - display: flex; - justify-content: space-between; - } - - #newChat { - text-align: center; - width: 80%; - height: 40px; - border-radius: 3px; - background: var(--inputborder-color); - display: flex; - align-items: center; - justify-content: center; - -webkit-user-select: none; - user-select: none; - cursor: pointer; - flex: 1; - } - - .navHeader>div:hover { - background: var(--body-hover-bgcolor)!important; - } - - #newChat>svg { - margin-right: 2px; - } - - #newFolder { - height: 40px; - width: 40px; - margin-left: 10px; - border-radius: 3px; - background: var(--inputborder-color); - cursor: pointer; - position: relative; - -webkit-user-select: none; - user-select: none; - } - - #newFolder>svg { - display: block; - margin: 8px auto; - } - - .extraChat { - padding: 2px 10px 6px 10px; - position: relative; - } - - #searchChat { - width: 100%; - height: 36px; - padding-left: 10px; - padding-right: 16px; - font-size: 16px; - outline: none; - border: none; - background: var(--inputborder-color); - border-radius: 3px; - } - - #searchChat:focus { - background: var(--inputborder-color); - } - - #searchChat:placeholder-shown+#clearSearch { - display: none; - } - - #clearSearch { - position: absolute; - right: 12px; - top: 8px; - cursor: pointer; - } - - #clearSearch:hover { - color: var(--title-color); - } - - #clearSearch>svg { - display: block; - } - - .seledSearch { - background: var(--inputborder-color)!important; - } - - #matchCaseSearch { - position: absolute; - right: 12px; - top: 8px; - cursor: pointer; - border-radius: 3px; - color: var(--inputborder-color); - } - - #matchCaseSearch:hover { - background: var(--body-hover-bgcolor); - } - - #matchCaseSearch>svg { - display: block; - } - - .navFooter { - padding-bottom: 8px; - background: var(--inputborder-color); - } - - .navFooter .divider { - width: 100%; - border-top: 1px solid var(--inputborder-color); - margin: 4px 0; - } - - .navFunc { - padding-top: 5px; - display: flex; - justify-content: space-evenly; - } - - .navFunc svg { - display: block; - } - - .navFunc>div, - .navFunc>label { - border-radius: 20px; - text-align: center; - padding: 8px 8px; - font-size: 14px; - cursor: pointer; - } - - .navFunc>div:hover, - .navFunc>label:hover { - background: var(--body-hover-bgcolor); - } - - .navFooter .links { - text-align: center; - -webkit-user-select: none; - user-select: none; - } - - .navFooter .links a { - text-decoration: none; - } - - - .allList { - width: 100%; - flex: 1; - overflow-y: auto; - } - - #chatList { - min-height: 50px; - } - - .dragingLi { - filter: brightness(90%); - } - - .dragingChat { - background: #e8e8e8; - } - - .expandFolder>.headLi>svg { - transform: rotate(90deg); - } - - .expandFolder>.chatsInFolder { - display: block; - } - - .chatsInFolder { - display: none; - margin-left: 22px; - padding-left: 2px; - border-left: 1px solid #909090; ; - } - - .headLi, - .chatLi { - cursor: pointer; - width: 100%; - height: 50px; - display: flex; - justify-content: space-between; - align-items: center; - position: relative; - } - - .headLi *, - .chatLi * { - pointer-events: none; - } - - .headLi>svg, - .chatLi>svg { - margin-left: 10px; - pointer-events: none; - } - - .folderOption svg, - .chatOption svg { - pointer-events: auto; - } - - .headLi svg *, - .chatLi svg * { - pointer-events: none; - } - - .headLi .folderInfo { - position: absolute; - left: 40px; - height: 40px; - max-width: calc(100% - 115px); - } - - .chatLi .chatInfo { - position: absolute; - left: 45px; - height: 40px; - max-width: calc(100% - 100px); - } - - .folderInfo *, - .chatInfo * { - -webkit-user-select: none; - user-select: none; - } - - .chatInfo span { - background: var(--inputborder-color)!important; - } - - .headLi .folderName, - .chatLi .chatName { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - line-height: 20px; - height: 20px; - } - - .headLi .folderNum, - .chatLi .chatPre { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - font-size: 12px; - line-height: 20px; - height: 20px; - } - - .headLi .folderOption, - .chatLi .chatOption { - visibility: hidden; - display: flex; - margin-right: 5px; - } - - .folderLi .chatLi>svg { - margin-left: 5px; - } - - .folderLi .chatLi .chatInfo { - left: 35px; - max-width: calc(100% - 85px); - } - - .folderLi .chatLi #activeChatEdit { - left: 32px; - width: calc(100% - 60px) - } - - .folderLi:hover { - background: var(--body-hover-bgcolor)!important; - } - - .chatLi:hover { - background: var(--body-hover-bgcolor)!important; - } - - .headLi:hover .folderOption, - .chatLi:hover .chatOption { - visibility: visible !important; - } - - .activeFolder, - .activeChatLi { - background: var(--inputborder-color)!important; - } - - .activeChatLi .chatOption { - visibility: visible !important; - } - - .folderOption>svg:hover, - .chatOption>svg:hover { - color: var(--title-color); - } - - #activeChatEdit { - position: absolute; - left: 42px; - font-size: 16px; - border-radius: 2px; - color: var(--title-color); - background-color: rgba(var(--primary-rgbbody),1); - outline: none; - border: none; - pointer-events: auto; - height: 24px; - line-height: 24px; - width: calc(100% - 75px); - padding: 20px 3px; - z-index: 1; - } - - #loadMask { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 100; - background-color: #edeff2; - } - - #loadMask>div { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - text-align: center; - } - - @keyframes loading { - - 0%, - 100% { - transform: scale(0); - } - - 50% { - transform: scale(1); - } - } - - .loadingCSSIcon { - position: relative; - display: flex; - align-items: center; - justify-content: space-evenly; - } - - .loadingCSSIcon div { - width: 15%; - height: 0; - padding-bottom: 15%; - border-radius: 50%; - animation: loading 1s cubic-bezier(0.3, 0, 0.7, 1) infinite; - } - - .loadingCSSIcon div:nth-child(1) { - background: #e15b64; - animation-delay: -0.4s - } - - .loadingCSSIcon div:nth-child(2) { - background: #f8b26a; - animation-delay: -0.2s - } - - .loadingCSSIcon div:nth-child(3) { - background: #99c959; - animation-delay: 0s; - } - - #loadMask>div>:first-child { - font-size: 40px; - color: #909090; ; - } - - #loadMask>div>:last-child { - width: 140px; - height: 70px; - margin: 0 auto; - } - - #voiceRec { - position: absolute; - right: 0; - top: 0; - width: 47px; - height: 100%; - } - - .message_if_voice { - padding-right: 47px !important; - } - - #voiceRecIcon { - width: 100%; - height: 100%; - text-align: center; - cursor: pointer; - position: relative; - } - - #voiceRecIcon:hover>svg { - color: #808080; - } - - #voiceRecIcon>svg { - width: 38px; - height: 38px; - color: #b0b0b0; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - - #voiceRecIcon>svg .animVoice { - display: none; - } - - .voiceRecing>svg { - color: #99c959 !important; - } - - .voiceRecing .animVoice { - display: inline !important; - transform-origin: 0 64%; - animation-duration: 1.5s; - animation-name: scaleVoice; - animation-timing-function: ease; - animation-iteration-count: infinite; - } - - .voiceLong .animVoice { - display: inline !important; - transform-origin: 0 64%; - animation-duration: 0.3s; - animation-name: longVoice; - animation-timing-function: ease-in-out; - animation-iteration-count: 1; - } - - @keyframes longVoice { - 0% { - transform: scaleY(0); - } - - 100% { - transform: scaleY(1); - } - } - - @keyframes scaleVoice { - 0% { - transform: scaleY(0.28); - } - - 20% { - transform: scaleY(0.60); - } - - 28% { - transform: scaleY(0.28); - } - - 36% { - transform: scaleY(0.45); - } - - 44% { - transform: scaleY(0.28); - } - - 52% { - transform: scaleY(0.45); - } - - 62% { - transform: scaleY(0.80); - } - - 72% { - transform: scaleY(0.80); - } - - 90% { - transform: scaleY(0.28); - } - - 100% { - transform: scaleY(0.28); - } - } - - #voiceRecSetting { - display: none; - position: absolute; - top: -70px; - left: -26px; - z-index: 1; - padding: 4px 4px; - -webkit-user-select: none; - user-select: none; - border-radius: 3px; - box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); - } - - #voiceRecSetting select { - width: 102px; - outline: none; - height: 28px; - border-radius: 3px; - border-color: rgba(0, 0, 0, .3); - } - - .presetModelCls label { - margin-right: 8px; - } - - .presetModelCls select { - height: 30px; - line-height: 30px; - margin-top: 2px; - font-size: 15px; - } - - .modelSwitch, - .setSwitch { - display: flex; - } - - .modelSwitch { - margin-bottom: 10px; - -webkit-user-select: none; - user-select: none; - } - - .modelSwitch>div, - .setSwitch>div { - border-radius: 3px; - width: calc(100% / 3); - padding: 8px; - line-height: 1; - text-align: center; - cursor: pointer; - } - - .modelSwitch>div:hover, - .setSwitch>div:hover { - background-color: var(--body-hover-bgcolor); - } - - .activeSwitch { - background-color: var(--inputborder-color); - } - - .checkLoad { - height: 32px; - border-radius: 3px; - line-height: 32px; - background: #d0d0d0; - text-align: center; - display: flex; - justify-content: center; - cursor: pointer; - } - - .checkLoad:hover { - background: #c0c0c0; - } - - .voiceChecking { - background-color: #c0c0c0 !important; - } - - .voiceChecking>svg { - display: inline !important; - } - - .checkLoad>svg { - display: none; - margin-right: 8px; - height: 32px; - width: 64px; - } - - #preSetSystem { - height: 30px; - line-height: 30px; - } - - #sysMask { - display: none; - position: fixed; - z-index: 200; - top: 0; - left: 0; - bottom: 0; - right: 0; - cursor: pointer; - justify-content: center; - align-items: center; - background: rgba(0, 0, 0, .4); - -webkit-tap-highlight-color: transparent; - } - - #sysDialog { - position: relative; - background: rgba(var(--primary-rgbbody),1); - cursor: auto; - max-height: 100%; - width: 100%; - display: flex; - flex-direction: column; - border-radius: 4px; - padding: 20px; - } - - .sysTitle { - font-size: 20px; - font-weight: bold; - margin-bottom: 8px; - -webkit-user-select: none; - user-select: none; - } - - .sysSwitch, - .sysSwitch>div * { - pointer-events: none; - } - - .sysSwitch>div { - border-radius: 3px; - height: 32px; - line-height: 32px; - text-align: center; - cursor: pointer; - pointer-events: auto; - font-weight: bold; - display: flex; - align-items: center; - -webkit-user-select: none; - user-select: none; - } - - .sysSwitch>div>svg { - margin-right: 4px; - } - - .sysSwitch>div:hover { - background-color: var(--body-hover-bgcolor); - } - - .sysDetail { - overflow-y: auto; - flex: 1; - margin-top: 4px; - } - - #closeSet { - position: absolute; - right: 0px; - top: 0px; - cursor: pointer; - padding: 10px 14px; - } - - #closeSet:hover { - color: #000; - } - - .setContent { - margin-top: 8px; - } - - .setNotNormalFlow { - position: relative; - } - - .setTitle { - margin-bottom: 6px; - font-weight: bold; - -webkit-user-select: none; - user-select: none; - } - - .setDetail { - margin: 0 10px; - -webkit-user-select: none; - user-select: none; - } - - .autoSelect>label, - .autoSelect>input { - cursor: pointer; - } - - .dataDetail { - display: flex; - } - - .dataDetail svg { - margin-right: 4px; - } - - .dataDetail>div, - .dataDetail>label { - border-radius: 3px; - text-align: center; - padding: 6px 8px; - margin-right: 12px; - font-size: 15px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: space-between; - } - - .dataDetail>div:hover, - .dataDetail>label:hover { - background-color: var(--body-hover-bgcolor); - } - - .keyOptionDetail { - display: flex; - margin-top: 10px; - } - - #resetHotKey { - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; - font-size: 15px; - border-radius: 3px; - padding: 6px 8px; - margin-left: 90px; - } - - #resetHotKey>svg { - margin-right: 4px; - } - - #resetHotKey:hover { - background-color:var(--body-hover-bgcolor); - } - - .hotKeyDetail>div { - position: relative; - height: 30px; - margin-bottom: 2px; - } - - .hotKeyDetail label { - line-height: 30px; - } - - .hotKeyDetail select { - position: absolute; - left: 110px; - outline: none; - border-radius: 3px; - width: 120px; - border-color: rgba(0, 0, 0, .3); - height: 30px; - font-size: 15px; - } - - .avatarDetail { - display: flex; - margin-top: 2px; - } - - .avatarDetail img { - border-radius: 2px; - width: 32px; - height: 32px; - flex-shrink: 0; - margin-right: 10px; - margin-top: 2px; - } - - .inputDetail input { - outline: none; - border-radius: 3px; - padding-left: 8px; - font-size: 15px; - width: 100%; - height: 34px; - border: 1px solid rgba(0, 0, 0, .3); - } - - .themeDetail { - display: flex; - width: 180px; - justify-content: space-between; - pointer-events: none; - } - - .themeDetail svg { - display: block; - } - - .themeDetail>div { - pointer-events: auto; - border-radius: 20px; - text-align: center; - padding: 8px 8px; - font-size: 14px; - cursor: pointer; - } - - .themeDetail>div * { - pointer-events: none; - } - - .themeDetail>div:hover { - background-color: var(--body-hover-bgcolor); - } - - .darkTheme>div:first-child { - background-color: var(--inputborder-color); - } - - .lightTheme>div:nth-child(2) { - background-color: var(--inputborder-color); - } - - .autoTheme>div:nth-child(3) { - background-color: var(--inputborder-color); - } - - .langDetail { - width: 110px; - } - - .enLang>div:first-child { - background-color: var(--inputborder-color); - } - - .zhLang>div:nth-child(2) { - background-color: var(--inputborder-color); - } - - #customAutoSet input { - width: 100px; - height: 30px; - line-height: 30px; - font-size: 15px; - outline: none; - border: 1px solid rgba(0, 0, 0, .3); - text-align: center; - border-radius: 3px; - background: #fff; - color: #24292f; - } - - #customAutoSet label { - margin-right: 8px; - } - - .progressBar { - position: relative; - width: 100%; - height: 12px; - border-radius: 6px; - background: #e0e0e0; - overflow: hidden; - } - - .nowProgress { - position: absolute; - left: 0; - top: 0; - height: 12px; - min-width: 1px; - border-radius: 6px; - background: #99c959; - } - - .progressDetail { - display: flex; - justify-content: space-between; - font-size: 15px; - } - - .cursorCls { - background-color: var(--inputborder-color); - width: 4px; - animation: 1s cursor-blinker infinite step-start; - } - - @keyframes cursor-blinker { - 0% { - opacity: 0; - } - - 50% { - opacity: 1; - } - - 100% { - opacity: 0; - } - } - - .apiSelsContainer { - position: relative; - z-index: 1; - } - - #apiSelect { - position: absolute; - top: 64px; - padding: 4px 0; - width: calc(100% - 20px); - left: 10px; - box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); - max-height: 180px; - overflow-y: auto; - -webkit-user-select: none; - user-select: none; - } - - #apiSelect>div { - pointer-events: auto; - cursor: pointer; - font-size: 15px; - padding: 6px 0 6px 8px; - height: 36px; - display: flex; - justify-content: space-between; - align-items: center; - } - - #apiSelect>div:hover { - background-color:var(--body-hover-bgcolor); - } - - #apiSelect>div>span { - height: 100%; - line-height: 23px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - #apiSelect>div * { - pointer-events: none; - } - - .delApiOption:hover { - background-color: var(--body-hover-bgcolor); - } - - .delApiOption { - width: 36px; - height: 36px; - flex-shrink: 0; - pointer-events: auto !important; - } - - .delApiOption>svg { - margin: 6px; - display: block; - } - - @keyframes textInOut { - from { - opacity: 0; - } - - to { - opacity: 1; - } - } - - .response[data-loading="true"]>.responseBody>*:not(.cursorCls) { - animation: textInOut 0.48s ease-in-out forwards; - } - - .cotBtn { - width: 120px; - height: 32px; - font-size: 14px; - user-select: none; - background-color: var(--inputborder-color); - position: relative; - padding-left: 14px; - line-height: 32px; - border-radius: 8px; - margin-bottom: 12px; - cursor: pointer; - } - - .cotBtn:hover { - background-color: var(--body-hover-bgcolor); - } - - .cotBtn.endCot { - width: 142px; - } - - .cotBtn::after { - content: ''; - display: inline-block; - position: absolute; - right: 15px; - top: 14px; - width: 6px; - height: 6px; - transform: rotate(-45deg); - } - - .cotBtn.down::after { - top: 11px; - transform: rotate(135deg); - } - - .cotContent { - background-color: var(--inputborder-color); - padding-left: 12px; - font-size: 14px; - } - - .cotContent>:last-child { - margin-bottom: 12px !important; - } - - .hide { - display: none; - } - - +
- +
- 【 <%=api.get_version()%> 】
- 【 <%=translate("Null")%> 】 /dev/null -rm -f /tmp/etc/passwall_tmp/binver_* /tmp/luci-indexcache /tmp/luci-indexcache.* -rm -rf /tmp/luci-modulecache/ +rm -f /usr/share/passwall/rules/*.nft +rm -rf /tmp/etc/passwall_tmp + +rm -f /tmp/luci-indexcache /tmp/luci-indexcache.* +rm -rf /tmp/luci-modulecache killall -HUP rpcd 2>/dev/null exit 0 diff --git a/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm b/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm index aa3c0681..c2eb0305 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm @@ -312,11 +312,31 @@ local com = self.com //]]> + +
- +
- 【 <%=api.get_version()%> 】
- 【 <%=translate("Null")%> 】 /dev/null exit 0 diff --git a/luci-theme-footstrap/LICENSE b/luci-theme-footstrap/LICENSE deleted file mode 100644 index d6456956..00000000 --- a/luci-theme-footstrap/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - 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-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile deleted file mode 100644 index 34ff9d88..00000000 --- a/luci-theme-footstrap/Makefile +++ /dev/null @@ -1,146 +0,0 @@ -# -# Copyright (C) 2026 -# -# This is free software, licensed under the Apache License, Version 2.0 . -# - -include $(TOPDIR)/rules.mk - -PKG_NAME:=luci-theme-footstrap -# luci.mk keys the Build/Prepare hook name on LUCI_NAME, which defaults to the checkout's -# directory name — a differently-named checkout symlinked into the feed would silently skip -# the CSS build. Pin it. -LUCI_NAME:=luci-theme-footstrap - -# Version is git-derived; never hardcode PKG_VERSION. luci.mk falls back to PKG_SRC_VERSION, -# which it always defines, when nothing here sets it — see docs/package.md for why no override -# lives here. -LUCI_TITLE:=Footstrap Theme -# +luci-base is the WHOLE list. Nothing shipped fetches at run time; `curl` is not in OpenWrt's -# default set (8 MB devices), so fall back to uclient-fetch, jsonfilter, sha256sum or do without. -LUCI_DEPENDS:=+luci-base -LUCI_PKGARCH:=all -# Else luci.mk claims MAINTAINER "OpenWrt LuCI community" and URL openwrt/luci. -LUCI_MAINTAINER:=Ivan Kvashonkin -LUCI_URL:=https://github.com/VizzleTF/luci-theme-footstrap - -# CSS ships verbatim: luci.mk's csstidy mangles :has(), color-mix() and nested calc(). build-css.sh -# minifies. -LUCI_MINIFY_CSS:=0 -# -# JS: the released package is built by owfeed (tools/stage.sh), not by this Makefile through the -# SDK, so LUCI_MINIFY_JS keeps luci.mk's jsmin. jsmin's one-character lookback does not know `n` or -# `>`, so `return /re/` makes it eat the rest of the file AND EXIT 0 (openwrt/luci#8299): wrap a -# regex literal there in parentheses. - -# Apache-2.0, not a free choice: styles/base began as a fork of luci-theme-bootstrap's cascade.css, -# the ucode templates derive from LuCI's and a few JS helpers are verbatim copies; the notices -# travel with it. No OFL-1.1: the theme ships no Font Software. -PKG_LICENSE:=Apache-2.0 -PKG_LICENSE_FILES:=LICENSE - -# /etc/config/footstrap is SHIPPED as an empty stub and WRITTEN AT RUNTIME: Appearance -> -# "Save as default" has rpcd uci-set the router-wide axes into that very file. Without this define -# the package manager owns it as an ordinary file and REPLACES it on upgrade, so the admin's saved -# defaults are wiped by the theme's own one-click Update, silently and reported as success. -# OpenWrt honours the define for BOTH formats (include/package-pack.mk), so one covers 24.10's opkg -# and 25.12's apk alike. Any future root/etc/config/* must be listed here too. -define Package/luci-theme-footstrap/conffiles -/etc/config/footstrap -endef - -# No `rpcd reload`: it re-execs rpcd, which drops a plugin that is absent mid-write until the next -# reload, and this package registers no rpcd object (rpcd re-reads acl.d at every login). The postinst -# ends a session only when a fresh login would grant it more. Comments inside a define ship into the -# router's script store (1984 of 3239 B), so the bodies are code only (`set -f`: the unquoted -# `$$(uci -q get …)` lists must split, not glob a `*`). docs/package.md, "Why postinst/postrm never -# call `rpcd reload`". -define Package/luci-theme-footstrap/postinst -#!/bin/sh -[ -n "$${IPKG_INSTROOT}" ] || { - rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* >/dev/null 2>&1 || true - set -f - ubus list 2>/dev/null | grep -qx session && { - for s in $$(ubus call session list 2>/dev/null | grep -o '"ubus_rpc_session": "[a-f0-9]*"' | cut -d'"' -f4); do - case "$$s" in *[!0]*) ;; *) continue ;; esac - ubus call session access "{\"ubus_rpc_session\":\"$$s\",\"scope\":\"uci\",\"object\":\"footstrap\",\"function\":\"write\"}" 2>/dev/null | grep -q true && continue - u=$$(ubus call session get "{\"ubus_rpc_session\":\"$$s\"}" 2>/dev/null | grep -o '"username": *"[^"]*"' | cut -d'"' -f4) - [ -n "$$u" ] || continue - perm=$$(uci show rpcd 2>/dev/null | grep -F .username= | while read -r kv; do - sect=$${kv%%.username=*} - un=$${kv#*.username=} - un=$${un#\'} - un=$${un%\'} - [ "$$un" = "$$u" ] || continue - for grp in $$(uci -q get "$$sect.write"); do - [ "$$grp" = luci-theme-footstrap ] && { echo write; break 2; } - done - for grp in $$(uci -q get "$$sect.read"); do - [ "$$grp" = luci-theme-footstrap ] && { echo read; break 2; } - done - done) - [ -n "$$perm" ] || continue - [ "$$perm" = read ] && ubus call session access "{\"ubus_rpc_session\":\"$$s\",\"scope\":\"uci\",\"object\":\"footstrap\",\"function\":\"read\"}" 2>/dev/null | grep -q true && continue - ubus call session destroy "{\"ubus_rpc_session\":\"$$s\"}" >/dev/null 2>&1 || true - done - } -} -exit 0 -endef - -# opkg runs the OLD package's postrm with "upgrade" on a version upgrade; it must change nothing -# there, since reverting mediaurlbase flipped upgrading 24.10 users back to bootstrap (apk never -# runs it on upgrade). Removal restores bootstrap only if its media dir and header.ut exist, drops -# /etc/footstrap (the uploaded login background) and, as in the postinst, no rpcd reload. -define Package/luci-theme-footstrap/postrm -#!/bin/sh -case "$$1" in *upgrade*) exit 0 ;; esac -[ -n "$${IPKG_INSTROOT}" ] || { - uci -q delete luci.themes.Footstrap - case "$$(uci -q get luci.main.mediaurlbase)" in - /luci-static/footstrap*) - [ -d /www/luci-static/bootstrap ] && \ - [ -f /usr/share/ucode/luci/template/themes/bootstrap/header.ut ] && \ - uci set luci.main.mediaurlbase=/luci-static/bootstrap ;; - esac - uci commit luci - rm -rf /etc/footstrap >/dev/null 2>&1 || true - rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* >/dev/null 2>&1 || true -} -endef - -# Runs right after luci.mk copies the sources into PKG_BUILD_DIR, so it rewrites the build copy -# only. cascade.css is generated, not committed, and styles/ is not in luci.mk's copy list: -# build-css.sh reads it from $(CURDIR) (cat and awk only, so it runs on a buildbot). The git-derived -# version is stamped into fs-version.js unconditionally; only an un-built checkout keeps -# '0.0.0-dev'. The catalogue is luci.mk's business: `po/` is what LUCI_LANGUAGES globs. -define Build/Prepare/luci-theme-footstrap - # PKG_LICENSE_FILES resolves against $(PKG_BUILD_DIR), which luci.mk fills with only src/ luasrc/ - # htdocs/ root/ ucode/; the copy is metadata only, luci.mk never installs it. - $(CP) $(CURDIR)/LICENSE $(PKG_BUILD_DIR)/ - $(SHELL) $(CURDIR)/build-css.sh $(PKG_BUILD_DIR)/htdocs/luci-static/footstrap/cascade.css - # Private `--fs-*` names are 13% of the sheet; runs BEFORE the templates strip, which takes the - # comments it reads. - $(SHELL) $(CURDIR)/mangle-tokens.sh \ - $(PKG_BUILD_DIR)/htdocs/luci-static/footstrap/cascade.css \ - $(PKG_BUILD_DIR)/htdocs/luci-static/resources $(PKG_BUILD_DIR)/ucode - # Strips `{# … #}`, whole-line `/* … */` and the indentation from the templates (-27.2 KB of 47.2). - # dev-sync.sh does not run this, so a router being debugged keeps its comments and indentation. - $(SHELL) $(CURDIR)/strip-build.sh templates $(PKG_BUILD_DIR)/ucode - # The same for the shell under root/ (about half of uci-defaults and the CGI are comments); - # whole-line `#` only. - $(SHELL) $(CURDIR)/strip-build.sh shell $(PKG_BUILD_DIR)/root - # The SVG favicon's comment and the manifest's indentation; awk only, so no node on a buildbot. - $(SHELL) $(CURDIR)/strip-build.sh assets $(PKG_BUILD_DIR)/htdocs/luci-static/footstrap - $(SED) "s#const FS_VERSION *= *'[^']*'#const FS_VERSION = '$(if $(PKG_VERSION),$(PKG_VERSION),$(PKG_SRC_VERSION))'#" \ - $(PKG_BUILD_DIR)/htdocs/luci-static/resources/fs-version.js -endef - -# Absolute, not a relative path: the usual ../../luci.mk resolves only inside the luci feed. -include $(TOPDIR)/feeds/luci/luci.mk - -# Load-bearing, not boilerplate: include/scan.mk finds packages by GREPPING Makefiles for -# `call (Build/DefaultTargets|BuildPackage|KernelPackage)`. luci.mk calls BuildPackage, which the -# grep cannot see, so without the literal line below the package gets no CONFIG_PACKAGE_* symbol and -# `make package//compile` fails with "No rule to make target". -# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-theme-footstrap/README.md b/luci-theme-footstrap/README.md deleted file mode 100644 index fd99bf69..00000000 --- a/luci-theme-footstrap/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# luci-theme-footstrap (the package) - -A LuCI theme for OpenWrt **24.10.3 and newer** (ucode templates). Installing and using it: -[the repository README](../README.md). Developer documentation: [`../docs/`](../docs/README.md). - -Internal name: `footstrap`. Media path: `/luci-static/footstrap`. - -## One theme, one entry - -`luci.themes` carries **exactly one** entry, `Footstrap` → `/luci-static/footstrap`. Everything -else is a **client** axis on the System → Footstrap page: layout (top bar by default, or the side -menu), mode, palette, density, wallpaper, rounding and every colour. The colours are the accent, the -three status colours and the four surfaces. The axes live in `localStorage` plus attributes and -inline properties on `:root`. Nothing is written to the router until "Save to router" is pressed. There are no per-layout or per-mode themes. - -There is one menu renderer too, `renderMainMenu` in `fs-chrome.js`. The top bar is its own markup, -morphed by CSS through `:root[data-layout]`. No second template, no second renderer, no `footstrap-top` symlink. - -**The theme's boundary:** it supplies the chrome and the design language; page content is drawn by -the view JS of `luci-mod-*`. The one exception is `fs-overview.js`, which draws no content of its own -and only re-arranges the stock overview sections. - -## Layout - -The main parts; the full tree is in [`../docs/package.md`](../docs/package.md) "Source tree". - -``` -Makefile luci.mk; Build/Prepare (CSS, mangle, strip, version) -styles/ CSS source: layers tokens / base / theme / pages -build-css.sh styles/ → htdocs/luci-static/footstrap/cascade.css -po/ translation catalogue (Weblate translates this) -ucode/template/themes/footstrap/ header.ut, footer.ut, sysauth.ut, partials/ -htdocs/luci-static/footstrap/ cascade.css (generated), logo.svg; no webfonts, no wallpaper -htdocs/luci-static/resources/ menu-footstrap*.js, fs-*.js, view/footstrap/appearance.js -root/ uci-defaults, the conffile, menu.d, the ACL, keep.d, the pattern CGI -``` - -There is no update checker. The theme is installed from the package feed (`install.sh` adds it), so -`apk upgrade` / `opkg upgrade` carries it forward like everything else on the router. The -System → Footstrap page shows the version it is running and makes no network call to do it. - -**Do not edit `cascade.css`.** `build-css.sh` generates it from `styles/`, and git ignores it. -Colours go in `styles/03-palettes.css`, scales and tokens in `styles/02-tokens.css`. - -## Working on a router - -The normal dev stand is the disposable routers `../owlab.yaml` defines, driven by `owlab`. See -[`../docs/development.md`](../docs/development.md). `dev-sync.sh` targets a **hardware** router over -ssh: - -```sh -./dev-sync.sh [host] # push over ssh, default host router2512 (registers the theme but does NOT activate it) -ssh router2512 'uci set luci.main.mediaurlbase=/luci-static/footstrap; uci commit luci; rm -f /tmp/luci-indexcache*' -ssh router2512 'uci set luci.main.mediaurlbase=/luci-static/bootstrap; uci commit luci' # roll back -``` - -Before pushing, see [`../docs/verifying.md`](../docs/verifying.md). The rules and the traps are in -[`../docs/conventions.md`](../docs/conventions.md); packaging is in -[`../docs/package.md`](../docs/package.md). diff --git a/luci-theme-footstrap/build-apk.sh b/luci-theme-footstrap/build-apk.sh deleted file mode 100755 index fe9fe915..00000000 --- a/luci-theme-footstrap/build-apk.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/bin/sh -# Build luci-theme-footstrap as an OpenWrt .apk via the SDK. The theme is noarch, so the package -# installs on any router of that release whatever its CPU architecture. -# -# ./build-apk.sh # download SDK if needed, then build -# BUILD_DIR=~/x ./build-apk.sh -# -# NOT how the release is built (`./tools/stage.sh && owfeed build` is). This exercises the other -# path: the Makefile, luci.mk, jsmin and the SDK's packaging, for anyone building without owfeed. -set -eu - -# Release and gcc string are baked into the SDK filename and downloads.openwrt.org keeps only the -# current point release, so this goes stale: keep it at owfeed.lock's 25.12 `point:`. Override with -# OPENWRT_RELEASE and OPENWRT_SDK_FILE. -REL="${OPENWRT_RELEASE:-25.12.5}" -SDK_BASE="https://downloads.openwrt.org/releases/${REL}/targets/mediatek/filogic" -SDK_FILE="${OPENWRT_SDK_FILE:-openwrt-sdk-${REL}-mediatek-filogic_gcc-14.3.0_musl.Linux-x86_64.tar.zst}" -SDK_URL="$SDK_BASE/$SDK_FILE" -# MUST be a case-sensitive fs (ext4/…), NOT an NTFS/9p Windows mount. -BUILD_DIR="${BUILD_DIR:-/tmp/ow-footstrap-build}" -# FORCE=1 overrides buildroot's host-prereq bail-outs (see step 4). -export FORCE=1 -THEME_DIR="$(cd "$(dirname "$0")" && pwd)" # this package -REPO="$(cd "$THEME_DIR/.." && pwd)" # repo root (holds tools/, .github/) -SDK_DIR="$BUILD_DIR/sdk" -# the SDK signing channel, derived from REL (25.12.2 -> 25.12) -CHANNEL="${REL%.*}" - -mkdir -p "$BUILD_DIR" -cd "$BUILD_DIR" - -# 1. SDK -if [ ! -d "$SDK_DIR" ]; then - echo ">> downloading SDK $REL ..." - # --https-only: GNU wget follows https -> http redirects - wget -q --https-only -O sdk.tar.zst "$SDK_URL" - - # Signature first, checksum under it: the toolchain lands inside the built package. `sha256sums` - # alone proves nothing, it is served unsigned beside the tarball (docs/conventions.md). The - # ed25519 signature, checked with a key pinned from another host (github.com/openwrt/keyring), - # is what holds. Fails CLOSED. Every pin comes from luci-upstream.pin. - . "$THEME_DIR/luci-upstream.pin" - [ -n "${OPENWRT_KEYRING_PIN:-}" ] || { echo "build-apk: OPENWRT_KEYRING_PIN missing from luci-upstream.pin" >&2; exit 1; } - - # OPENWRT_SDK_KEY_, through eval: /bin/sh has no associative arrays - CH_VAR="$(printf '%s' "$CHANNEL" | tr -d .)" - eval "SDK_KEY=\${OPENWRT_SDK_KEY_$CH_VAR:-}" - eval "SDK_KEY_SHA256=\${OPENWRT_SDK_KEY_${CH_VAR}_SHA256:-}" - [ -n "$SDK_KEY" ] && [ -n "$SDK_KEY_SHA256" ] \ - || { echo "build-apk: no SDK signing key for channel $CHANNEL in luci-upstream.pin" >&2; exit 1; } - - echo ">> verifying SDK signature (key $SDK_KEY, channel $CHANNEL) ..." - rm -rf "$BUILD_DIR/usign" - U="$("$REPO/tools/build-usign.sh" "$BUILD_DIR/usign")" - wget -q --https-only -O sha256sums "$SDK_BASE/sha256sums" - wget -q --https-only -O sha256sums.sig "$SDK_BASE/sha256sums.sig" - wget -q --https-only -O openwrt.pub \ - "https://raw.githubusercontent.com/openwrt/keyring/$OPENWRT_KEYRING_PIN/usign/$SDK_KEY" - echo "$SDK_KEY_SHA256 openwrt.pub" | sha256sum -c - >/dev/null \ - || { echo "build-apk: OpenWrt's $CHANNEL key does not match its pin — refusing to verify with it" >&2; exit 1; } - "$U" -V -m sha256sums -p openwrt.pub -x sha256sums.sig \ - || { echo "build-apk: BAD SIGNATURE on OpenWrt's sha256sums — refusing to build with this SDK." >&2; exit 1; } - echo ">> signature verified; now the checksum means something." - - WANT="$(grep -F " *$SDK_FILE" sha256sums | cut -d' ' -f1)" - [ -n "$WANT" ] || { echo "build-apk: no sha256 published for $SDK_FILE" >&2; exit 1; } - GOT="$(sha256sum sdk.tar.zst | cut -d' ' -f1)" - [ "$WANT" = "$GOT" ] || { echo "build-apk: SDK checksum mismatch: want $WANT, got $GOT" >&2; exit 1; } - rm -f sha256sums sha256sums.sig openwrt.pub - echo ">> SDK verified." - - echo ">> extracting ..." - mkdir -p "$SDK_DIR" - tar --zstd -xf sdk.tar.zst -C "$SDK_DIR" --strip-components=1 - rm -f sdk.tar.zst -fi -cd "$SDK_DIR" - -# 2. feeds (need luci for luci.mk + BuildPackage macros) -if [ ! -f feeds/luci.index ] && [ ! -d feeds/luci ]; then - ./scripts/feeds update base luci -fi -./scripts/feeds install -a -p luci >/dev/null 2>&1 || true - -# 3. drop our theme into the luci themes feed (fresh copy) -DEST="feeds/luci/themes/luci-theme-footstrap" -rm -rf "$DEST" -cp -a "$THEME_DIR" "$DEST" -# a worktree can hold 0600/0700 (git stores 100644); uhttpd answers 403 on a file that is not world-readable -chmod -R u=rwX,go=rX "$DEST" -rm -rf "$DEST/build-apk.sh" "$DEST/dev-sync.sh" "$DEST/.git" 2>/dev/null || true - -./scripts/feeds update -i luci -./scripts/feeds install luci-theme-footstrap - -# 4. build. ncurses is only needed for interactive menuconfig, not for a noarch -# theme, so satisfy the host prereq stamp to skip that check. -mkdir -p staging_dir/host -touch staging_dir/host/.prereq-build -make defconfig FORCE=1 -make package/luci-theme-footstrap/clean FORCE=1 V=s >/dev/null 2>&1 || true -make package/luci-theme-footstrap/compile FORCE=1 V=s - -# 5. locate the artifact -echo -echo ">> built packages:" -find bin -name 'luci-theme-footstrap*' \( -name '*.apk' -o -name '*.ipk' \) -print diff --git a/luci-theme-footstrap/build-css.sh b/luci-theme-footstrap/build-css.sh deleted file mode 100755 index 8c84a4c9..00000000 --- a/luci-theme-footstrap/build-css.sh +++ /dev/null @@ -1,330 +0,0 @@ -#!/bin/sh -# Concatenate the styles/ tree into a single cascade.css. -# -# ./build-css.sh [outfile] [--dev] -# -# Directory order is layer order (styles/, base/, theme/, pages/); filename order is source order -# within a directory. A later layer beats an earlier one whatever the specificity, so nothing needs -# !important to override base. -# -# Minified unless --dev: uhttpd serves CSS with no gzip, so every byte is a wire byte. Comments and -# whitespace go, and five spellings shrink (docs/css.md, "The build"): [a="b"] -> [a=b], the space -# before !important, ::before -> :before, the spaces beside * and / in a declaration, and a flex -# triple with a keyword form (1 1 auto -> auto). Nothing else in a selector or declaration is -# rewritten, which is why csstidy stays off (it mangles :has() and color-mix()). Needs only cat and -# awk, so an OpenWrt buildbot can run it. -set -eu - -D="$(cd "$(dirname "$0")" && pwd)" -OUT="" -DEV=0 -for a in "$@"; do - case "$a" in - --dev) DEV=1 ;; - # Else a typo like `--devv` becomes the output file name. - -*) echo "build-css: unknown option: $a" >&2; exit 1 ;; - *) OUT="$a" ;; - esac -done -[ -n "$OUT" ] || OUT="$D/htdocs/luci-static/footstrap/cascade.css" - -for d in styles styles/base styles/theme styles/pages; do - [ -d "$D/$d" ] || { echo "build-css: $D/$d missing" >&2; exit 1; } -done - -TMP="$OUT.tmp.$$" -# $TMP.min too: an awk failure leaves it behind. -trap 'rm -f "$TMP" "$TMP.min" "$TMP.layer"' EXIT -trap 'exit 130' INT TERM -mkdir -p "$(dirname "$OUT")" - -# One `@layer X{` per LAYER, not per FILE: 37 file wrappers are 542 bytes. The wrapper comes from -# the DIRECTORY, so each file must open with the layer its directory means or it is silently -# re-layered. A file with no wrapper (00-header.css: banner and layer-order statement) is copied -# through and must come before the wrapped ones, or it lands inside the block. -emit_layer() { - layer="$1"; shift - body="$TMP.layer" - : > "$body" - for f in "$@"; do - if head -1 "$f" | grep -q '^@layer '; then - head -1 "$f" | grep -q "^@layer $layer {\$" || { - echo "build-css: $f is in a $layer directory but does not open with '@layer $layer {'" >&2 - exit 1; } - # the file's own wrapper: its first line, and the bare `}` where brace depth returns to 0. - # Anything but blank or comment after that `}` is refused: the source leaves it unlayered - # and the build would move it into the block, a silent cascade change. - awk -v F="$f" 'NR == FNR { - if (end) { - l = $0 - if (inc) { if (sub(/^.*\*\//, "", l)) inc = 0; else next } - gsub(/\/\*.*\*\//, "", l) - if (match(l, /\/\*/)) { inc = 1; l = substr(l, 1, RSTART - 1) } - if (!bad && l ~ /[^ \t\r]/) bad = NR - next - } - # depth, string- and comment-aware: a `{` in a multi-line comment or in content:"{" - # is not a block (the same walk as brace_count below) - l = $0; n = length(l) - for (i = 1; i <= n; i++) { - ch = substr(l, i, 1) - if (inc) { if (ch == "*" && substr(l, i + 1, 1) == "/") { inc = 0; i++ } continue } - if (q != "") { if (ch == "\\") i++; else if (ch == q) q = ""; continue } - if (ch == "\"" || ch == "'"'"'") { q = ch; continue } - if (ch == "/" && substr(l, i + 1, 1) == "*") { inc = 1; i++; continue } - if (ch == "{") d++; else if (ch == "}") d-- - } - if (NR > 1 && d == 0) end = NR; next } - END { if (bad) { printf "build-css: %s:%d: a rule after the @layer wrapper would be moved into the layer; put it inside\n", F, bad > "/dev/stderr"; exit 1 } } - FNR > 1 && FNR != end' "$f" "$f" >> "$body" - elif [ -s "$body" ]; then - echo "build-css: $f has no @layer wrapper but follows one that does — it would be" >&2 - echo "build-css: swallowed into the $layer block instead of staying above it." >&2 - exit 1 - else - cat "$f" - fi - done - if [ -s "$body" ]; then - printf '@layer %s {\n' "$layer" - cat "$body" - printf '}\n' - fi - # not covered by the EXIT trap: the success path runs `trap - EXIT` first - rm -f "$body" -} - -# glob expands in filename order -{ - emit_layer tokens "$D"/styles/*.css - emit_layer base "$D"/styles/base/*.css - emit_layer theme "$D"/styles/theme/*.css - emit_layer page "$D"/styles/pages/*.css -} > "$TMP" - -# Comments, and — unless told not to — the whitespace CSS ignores anyway, in one string-aware -# pass rather than two chained ones. squeeze=0 (the pre-compression rule count below; a -# `--dev` build skips the pass and keeps the raw sheet) strips comments and trims each line; -# squeeze=1 also does the ~9.5 KB whitespace pass. Why a scan has to be string-aware, what the -# whitespace pass leaves alone and why, and the sed bug this replaced: docs/css.md, "The build". -squeeze() { - awk -v SQUEEZE="$2" ' - # The exact flex triples that have a shorter keyword form (shorthand initial values: grow 1 - # shrink 1 basis auto). Bare 0 is left alone, since `flex:0` means grow 0. - function flexv(v, imp) { - if (v ~ /!important$/) { imp = "!important"; v = substr(v, 1, length(v) - 10) } - if (v == "0 0 auto") v = "none" - else if (v == "1 1 auto") v = "auto" - else if (v == "0 1 auto") v = "0 auto" - else if (v ~ /^1 1 ([0-9]+\.?[0-9]*|\.[0-9]+)(%|[A-Za-z]+)$/) v = substr(v, 5) - return v imp - } - BEGIN { q = ""; inc = 0; banner = 0; lastc = ""; buf = ""; lastreal = ""; d = 0; st = 0; fl = 0 } - { - line = $0 - # a banner already open on an earlier line: nothing on a pure continuation line can - # change q or inc, so the whole line is banner content until its own closing "*/". - if (banner) { - trimmed = line - sub(/^[ \t]+/, "", trimmed); sub(/[ \t]+$/, "", trimmed) - print trimmed - lastc = ""; lastreal = "" - if (index(line, "*/")) banner = 0 - next - } - # The line BREAK a squeeze pass is about to swallow is whitespace, and a declaration - # may be wrapped across it. Feed it to the whitespace-run logic below as a leading - # space: it survives only where a space would and is dropped next to { } ; , : — - # lastc == "" means output is already at the start of a line, with nothing to glue to. - if (SQUEEZE && lastc != "" && q == "" && !inc) line = " " line - out = ""; i = 1; n = length(line); opened = 0 - while (i <= n) { - c = substr(line, i, 1) - if (inc) { # inside a stripped /* ... */ - if (c == "*" && substr(line, i + 1, 1) == "/") { inc = 0; i += 2; continue } - i++; continue - } - if (q != "") { # inside a "..." or '"'"'...'"'"' string - out = out c - if (c == "\\") { out = out substr(line, i + 1, 1); i += 2; continue } - if (c == q) q = "" - lastreal = "" - i++; continue - } - if (c == "\"" || c == "'"'"'") { - # [attr="ident"] -> [attr=ident]: the same selector, 2 bytes less, 394 sites. Only a - # whole ident closed on this line right before `]`; "--" stays quoted, since - # mangle-tokens.sh reads an unquoted --fs- as a name to rename. - if (SQUEEZE) { - j = index(substr(line, i + 1), c) - v = substr(line, i + 1, j - 1) - if (j && substr(line, i + j + 1, 1) == "]" && out ~ /\[[-A-Za-z0-9_]+[~|^$*]?=$/ && - v ~ /^-?[A-Za-z_][-A-Za-z0-9_]*$/ && !index(v, "--")) { - out = out v; lastreal = ""; i += j + 1; continue - } - } - q = c; out = out c; lastreal = ""; i++; continue - } - if (c == "/" && substr(line, i + 1, 1) == "*") { - if (substr(line, i + 2, 1) == "!") { - # a genuine banner open: reached only with q=="" and inc==0 AT THIS - # CHARACTER, the same guard strip_comments used before this pass absorbed - # it — so a "/*!" that is really bytes inside a string or an already-open - # ordinary comment never lands here. Copied raw from the marker on, byte - # for byte: it is an Apache-2.0 attribution, not formatting. - banner = 1 - rest = substr(line, i) - out = out rest - if (index(rest, "*/")) banner = 0 - opened = 1 - break - } - inc = 1; i += 2; continue - } - if (SQUEEZE) { - if (c == " " || c == "\t") { # collapse a run of whitespace to one space - while (i <= n && (substr(line, i, 1) == " " || substr(line, i, 1) == "\t")) i++ - # the last char EMITTED, which on a continuation line lives on the - # previous output line — hence lastc, not just `out`. - prev = (length(out) ? substr(out, length(out), 1) : lastc) - nxt = (i <= n ? substr(line, i, 1) : "") - # Drop the space next to a delimiter; otherwise it may be a combinator. `>` joins: - # outside a string it is the child combinator or a range `>` - # (`@container (width > 560px)`), both valid unspaced. `~` and `+` do not: - # `[attr~=v]` and `calc(100% - 10px)` are ambiguous without bracket depth. - # `!` joins too: `red!important` is the same declaration (35 sites). - if (prev == "" || prev == "{" || prev == "}" || prev == ";" || prev == "," || prev == ":" || prev == ">") - continue - if (nxt == "{" || nxt == "}" || nxt == ";" || nxt == "," || nxt == "" || nxt == ">" || nxt == "!") - continue - # Inside a declaration only + and - need their spaces: calc(1px * 2) -> calc(1px*2), - # 16 / 9 -> 16/9. Not before a comment opener, and `/*` or `*/` is never joined. - if (bk[d] == "D" && (prev == "*" || prev == "/" || nxt == "*" || (nxt == "/" && substr(line, i + 1, 1) != "*")) && - (prev nxt) != "/*" && (prev nxt) != "*/") - continue - out = out " "; lastreal = " " - continue - } - # ::before/::after -> :before/:after, the CSS2 form every engine still parses and - # serialises as `::`; the first colon of 93 pseudo-elements. Not `::before-x`. - if (c == ":" && substr(line, i + 1, 1) == ":") { - w = (substr(line, i + 2, 6) == "before") ? 6 : (substr(line, i + 2, 5) == "after") ? 5 : 0 - if (w && substr(line, i + 2 + w, 1) !~ /[-A-Za-z0-9_]/) { i++; continue } - } - # bk[d] is the kind of the open block: G holds rules (@layer @media @supports @container - # @keyframes), D holds declarations (a style rule, @font-face, a keyframe step). st: a - # statement has started, pk: its kind. The value rewrites cannot tell a nested rule from a - # declaration, so a rule inside a D block fails the build. - if (c == "{") { - if (bk[d] == "D") { printf "build-css: a rule nested in a declaration block (docs/css.md, \"The build\")\n" > "/dev/stderr"; exit 1 } - bk[++d] = (st ? pk : "D"); st = 0 - } else if (c == "}") { d--; st = 0 } - else if (c == ";") st = 0 - else if (!st) { - st = 1 - pk = (substr(line, i, 24) ~ /^@(layer|media|supports|container|scope|starting-style|[-a-z]*keyframes)/) ? "G" : "D" - } - # flex: vs is the offset of the value in buf out; it is rewritten when the declaration ends - if (c == ":" && bk[d] == "D" && out ~ /flex$/ && (buf out) ~ /(^|[{;])flex$/) { fl = 1; vs = length(buf) + length(out) + 1 } - if (fl && (c == ";" || c == "}")) { - fl = 0; ft = buf out; fv = substr(ft, vs + 1); fw = flexv(fv) - if (fw != fv) { buf = ""; out = substr(ft, 1, vs) fw } - } - # The last `;` of a block is dropped here, inside the string-aware scanner: a sed - # `s/;}/}/` cannot see strings and turns `content: ";}"` into `content: "}"`. - # The `;` may already sit in the previous output line, so text is held in `buf` - # until the rule closes. - if (c == "}") { - if (length(out) && substr(out, length(out), 1) == ";") - out = substr(out, 1, length(out) - 1) - else if (!length(out) && length(buf) && substr(buf, length(buf), 1) == ";") - buf = substr(buf, 1, length(buf) - 1) - } - out = out c; lastreal = c; i++ - } else { - out = out c; i++ - } - } - if (opened) { - trimmed = out - sub(/^[ \t]+/, "", trimmed); sub(/[ \t]+$/, "", trimmed) - print trimmed - lastc = ""; lastreal = "" - next - } - if (SQUEEZE) { - buf = buf out - if (length(out)) lastc = substr(out, length(out), 1) - # newline only after a closing brace — one rule per line. lastreal, not lastc: a - # line ending in a QUOTED `}` (content: "}") is not the end of a rule, and - # flushing there would split the rule and lose the space before its next token. - if (lastreal == "}") { print buf; buf = ""; lastc = ""; lastreal = "" } - } else { - sub(/^[ \t]+/, "", out) - sub(/[ \t]+$/, "", out) - if (length(out)) print out - } - } - END { if (SQUEEZE) { if (length(buf)) print buf; else printf "\n" } } - ' "$1" -} - -# Fail loudly rather than let an unbalanced block ship. -# -# String-aware, for the same reason the comment stripper is: a brace inside a CSS STRING is not a -# block, so a counter that gsub()s over the raw line makes a perfectly valid rule fail the build. -# Measured, all three shapes: `content: ";}"`, `content: "{"`, and a data-URI carrying `;}`. It -# fails closed, so nothing is ever corrupted; what it costs is a hunt for an imbalance that is not -# there. -brace_count() { - awk ' - BEGIN { q = "" } - { - line = $0; n = length(line); i = 1 - while (i <= n) { - ch = substr(line, i, 1) - if (q != "") { # inside a string - if (ch == "\\") { i += 2; continue } # escape: skip the pair - if (ch == q) q = "" - i++; continue - } - if (ch == "\"" || ch == "'"'"'") { q = ch; i++; continue } - if (ch == "{") o++ - else if (ch == "}") c++ - i++ - } - } - END { - if (o != c) { printf "build-css: %s: unbalanced braces (%d { vs %d })\n", FILENAME, o, c > "/dev/stderr"; exit 1 } - if (o < 100) { printf "build-css: %s: suspiciously few rules (%d)\n", FILENAME, o > "/dev/stderr"; exit 1 } - print o - }' "$1" -} - -# Always brace-check a COMMENT-STRIPPED, UNSQUEEZED copy, --dev included: counting braces in the -# raw file made a stray "{" in prose fail the build on perfectly valid CSS. -squeeze "$TMP" 0 > "$TMP.min" -RULES_BEFORE=$(brace_count "$TMP.min") || exit 1 -rm -f "$TMP.min" - -if [ "$DEV" -eq 0 ]; then - squeeze "$TMP" 1 > "$TMP.min" - mv "$TMP.min" "$TMP" - - # Recount on what ships: the squeeze is the pass most able to corrupt the sheet. The rule COUNT, - # not brace balance, catches a truncated write: a cut right after the base layer closes is still - # balanced against the true rule count, and RULES_BEFORE comes from the untruncated sheet. - RULES_AFTER=$(brace_count "$TMP") || exit 1 - if [ "$RULES_BEFORE" != "$RULES_AFTER" ]; then - echo "build-css: the squeeze changed the rule count ($RULES_BEFORE -> $RULES_AFTER)." >&2 - exit 1 - fi -fi -# --dev: $TMP keeps comments and formatting (only the @layer wrappers are merged), for reading on a -# router rather than shipping. - -SIZE=$(wc -c < "$TMP" | tr -d ' ') -mv "$TMP" "$OUT" -trap - EXIT - -echo "build-css: $SIZE bytes -> $OUT" diff --git a/luci-theme-footstrap/dev-sync.sh b/luci-theme-footstrap/dev-sync.sh deleted file mode 100755 index 1d13b241..00000000 --- a/luci-theme-footstrap/dev-sync.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/sh -# Push the footstrap theme to a HARDWARE router over ssh (containers use owlab). -# ONE theme, one luci.themes entry; layout (top/sidebar) is a client axis on the -# System -> Footstrap page, not a theme entry. This registers the theme; it does NOT activate it. -set -eu - -R="${1:-router2512}" -N=footstrap -D="$(cd "$(dirname "$0")" && pwd)" - -# cascade.css is generated from styles/ and is not in git. --dev keeps the comments so the -# file on the router still reads like the source. -"$D"/build-css.sh "$D/htdocs/luci-static/$N/cascade.css" --dev - -ssh "$R" "mkdir -p /usr/share/ucode/luci/template/themes/$N \ - /www/luci-static/$N \ -" - -# the ONE template (+ partials/). Sidebar and top bar are the same markup, morphed by -# :root[data-layout] — there is no second theme dir to copy. -scp -q "$D"/ucode/template/themes/$N/*.ut "$R":/usr/share/ucode/luci/template/themes/$N/ -ssh "$R" "mkdir -p /usr/share/ucode/luci/template/themes/$N/partials" -scp -q "$D"/ucode/template/themes/$N/partials/*.ut \ - "$R":/usr/share/ucode/luci/template/themes/$N/partials/ - -# shared static: cascade.css, the logo (SVG + PNGs), the app icon and the manifest; no wallpapers. -scp -qr "$D"/htdocs/luci-static/$N/* "$R":/www/luci-static/$N/ -# scp carries a worktree's 0600 across, and uhttpd answers 403 on a file that is not world-readable -ssh "$R" "chmod -R u=rwX,go=rX /www/luci-static/$N /usr/share/ucode/luci/template/themes/$N" - -# Every resource JS goes as a TAR OF THE TREE, never by name or `scp *.js`: luci.mk copies htdocs/ -# wholesale, so a file listed by hand, or one in a subdirectory, ships in the package and never -# reaches the dev router. Every path under resources/ is ours to place. --mode is GNU tar (a -# checkout's umask would otherwise set the router's modes); bsdtar on macOS refuses it, so use gtar. -tar -C "$D/htdocs/luci-static/resources" --mode=u=rwX,go=rX -cf - . | - ssh "$R" "mkdir -p /www/luci-static/resources && tar -C /www/luci-static/resources -xf -" - -# stamp the git-derived version into the deployed fs-version.js (the package does the same in -# Build/Prepare) so the System -> Footstrap page shows a real version instead of "(dev)". The FILE -# NAME is part of the contract: FS_VERSION lives in fs-version.js, so moving it changes every sed. -FS_V="$(git -C "$D" describe --tags --always 2>/dev/null | sed 's/^v//')" -# if-form, not `[ -n ] && ssh`: under `set -e` a bare `&&`-list whose LAST command fails is what -# aborts, so `[ -n "$FS_V" ] && ssh …` would take the sync down whenever git describe yielded -# nothing (a copied tree without .git). An `&&` guarding an ASSIGNMENT is a different shape and is -# safe. expr refuses a tag with sed- or shell-special characters rather than interpolating it. -if [ -n "$FS_V" ] && expr "$FS_V" : '[0-9A-Za-z._-]*$' >/dev/null; then - ssh "$R" "sed -i \"s#const FS_VERSION *= *'[^']*'#const FS_VERSION = '$FS_V'#\" \ - /www/luci-static/resources/fs-version.js" -fi - -# The catalogue: owfeed bundles it in the release, so a dev router gets it only from this sync. -# po2lmo is a luci-base host tool, so this is best-effort: without it strings stay English. Build it -# with make -C modules/luci-base/src po2lmo (plural_formula.c is generated by contrib/lemon). -# The basename is `footstrap`, as luci.mk and owfeed.yml ship it (luci-i18n-footstrap-); the -# old `footstrap-theme.*.lmo` of an earlier dev sync is removed, or a stale catalogue could win. -if command -v po2lmo >/dev/null 2>&1; then - ssh "$R" "mkdir -p /usr/lib/lua/luci/i18n && rm -f /usr/lib/lua/luci/i18n/footstrap-theme.*.lmo" - for po in "$D"/po/*/*.po; do - [ -e "$po" ] || continue - lang="$(basename "$(dirname "$po")")" - po2lmo "$po" "/tmp/footstrap.$lang.lmo" - scp -q "/tmp/footstrap.$lang.lmo" "$R":/usr/lib/lua/luci/i18n/ - rm -f "/tmp/footstrap.$lang.lmo" - done -else - echo " (po2lmo not found — skipping the translation catalogue; strings stay English)" -fi - -# root/ -> / as a TREE with a GLOB of top-level dirs, never a list of names (luci.mk installs root/ -# wholesale; a hand-written `usr` list missed root/etc/config/footstrap). tar, not `scp -r`: its -# merge onto an existing /usr is ambiguous. Two subtrees are excluded by behaviour, handled below: -# - etc/uci-defaults runs from /tmp; in /etc/uci-defaults the next boot would run and DELETE it. -# - etc/config holds conffiles, which the package manager keeps on upgrade and so must we: the -# live file holds the admin's "Save to router". -set -- -for _d in "$D"/root/*/; do set -- "$@" "$(basename "$_d")"; done -tar -C "$D/root" --mode=u=rwX,go=rX --exclude=etc/uci-defaults --exclude=etc/config -cf - "$@" | - ssh "$R" "tar -C / -xf -" - -# conffiles: install only when ABSENT, which is exactly what the package manager does with them. -for _f in "$D"/root/etc/config/*; do - [ -f "$_f" ] || continue - _b=$(basename "$_f") - scp -q "$_f" "$R":"/tmp/.fs-conf-$_b" - ssh "$R" "[ -f /etc/config/$_b ] || { mv /tmp/.fs-conf-$_b /etc/config/$_b; \ - echo ' installed /etc/config/$_b (was absent)'; }; rm -f /tmp/.fs-conf-$_b" -done -# No rpcd reload here — same race as the Makefile's postinst note (Package/luci-theme- -# footstrap/postinst): this copies files onto a router where the theme registers no rpcd object, -# rpcd re-reads acl.d/*.json at LOGIN (not only on reload), and a developer's root session already -# carries read='*' write='*', so the theme's ACL group is irrelevant to it regardless. - -ssh "$R" " -# Sweep every pre-consolidation name, $N-top included: a stale mediaurlbase on its media dir renders -# a theme LuCI no longer lists. rm -rf, not rm -f (it refuses directories and this block has no -# set -e); every path is a literal built from \$N. -cd /www/luci-static -rm -rf $N/$N $N-top $N-dark $N-light $N-top-dark $N-top-light -cd /usr/share/ucode/luci/template/themes -rm -rf $N/$N $N-top $N-dark $N-light $N-top-dark $N-top-light -# The overview layout must not be left in luci-mod-status's global include dir, where LuCI loads -# every *.js under any theme: it is a chrome module now. A package upgrade drops the old file, but -# dev-sync copies rather than installs, so it has to be swept by name — otherwise the dev router -# runs both copies and the grid is arranged twice. -rm -f /www/luci-static/resources/view/status/include/05_footstrap_overview_layout.js -" - -# Register by running the package's own uci-defaults script — the single source of truth -# (ONE entry, luci.themes.Footstrap; layout, mode and palette are client-side toggles). -# PKG_UPGRADE=1 forces its upgrade branch, so the dev router's active theme is left alone. -scp -q "$D"/root/etc/uci-defaults/30_luci-theme-footstrap "$R":/tmp/30_luci-theme-footstrap -ssh "$R" "PKG_UPGRADE=1 sh /tmp/30_luci-theme-footstrap; rm -f /tmp/30_luci-theme-footstrap" - - -# ---- last: drop LuCI's menu cache and bust the browser cache ---- -# -# LuCI's index cache is rebuilt from the files and the registration above. head.ut versions assets -# with `?v={{ pkgs_update_time }}`, the mtime of the PACKAGE DATABASE. An install moves it, scp does -# not, so a dev deploy keeps the URL while the bytes change and a browser may serve a stylesheet -# several deploys old. Touching the file is what an install would do. -ssh "$R" 'rm -f /tmp/luci-indexcache*; - touch /lib/apk/db/installed 2>/dev/null || touch /usr/lib/opkg/status 2>/dev/null || true' - -echo "synced to $R (single Footstrap theme registered, active theme unchanged)" diff --git a/luci-theme-footstrap/htdocs/luci-static/footstrap/app-icon-192.png b/luci-theme-footstrap/htdocs/luci-static/footstrap/app-icon-192.png deleted file mode 100644 index fbcbf068..00000000 Binary files a/luci-theme-footstrap/htdocs/luci-static/footstrap/app-icon-192.png and /dev/null differ diff --git a/luci-theme-footstrap/htdocs/luci-static/footstrap/logo.svg b/luci-theme-footstrap/htdocs/luci-static/footstrap/logo.svg deleted file mode 100644 index 93302ff6..00000000 --- a/luci-theme-footstrap/htdocs/luci-static/footstrap/logo.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - diff --git a/luci-theme-footstrap/htdocs/luci-static/footstrap/logo_48.png b/luci-theme-footstrap/htdocs/luci-static/footstrap/logo_48.png deleted file mode 100644 index ae7a6461..00000000 Binary files a/luci-theme-footstrap/htdocs/luci-static/footstrap/logo_48.png and /dev/null differ diff --git a/luci-theme-footstrap/htdocs/luci-static/footstrap/manifest.json b/luci-theme-footstrap/htdocs/luci-static/footstrap/manifest.json deleted file mode 100644 index 974777ad..00000000 --- a/luci-theme-footstrap/htdocs/luci-static/footstrap/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "LuCI", - "start_url": "/cgi-bin/luci/", - "display": "standalone", - "background_color": "#f6f8fa", - "theme_color": "#f6f8fa", - "icons": [ - { - "src": "app-icon-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any maskable" - } - ] -} diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js deleted file mode 100644 index 637bf893..00000000 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js +++ /dev/null @@ -1,637 +0,0 @@ -'use strict'; -'require baseclass'; -'require ui'; -'require fs-prefs as prefs'; -'require fs-axes as axes'; -'require fs-router as router'; -'require fs-assets as assets'; -'require fs-version as ver'; - -/* The Appearance controls: the DOM that presents the axes. It owns no preference: fs-prefs.js and - * fs-axes.js hold the axes and fs-version.js the version string. `view/footstrap/appearance.js` - * calls `renderStandalone()`, the whole surface this file ships. - * - * The version line makes no request and must not grow one: which version is installed is what this - * page answers, and which is available is the package manager's question. */ - -/* Colour readout. Two questions no stored value answers: what colour a role is right now (the - * palette's own while the axis is off) and what contrast it lands at. */ -let _probe = null; -function probeColor(expr) { - if (!_probe) { - /* Off-screen rather than display:none, so the reading does not depend on a display:none element - * computing `color` in every engine. Every declaration is !important: an app's CSS can style a - * plain
. */ - _probe = E('span', { 'id': 'fs-appearance-probe', 'aria-hidden': 'true' }); - _probe.style.cssText = 'position:fixed!important;left:-9999px!important;top:0!important;' - + 'width:0!important;height:0!important;overflow:hidden!important;' - + 'pointer-events:none!important;'; - document.body.appendChild(_probe); - } - /* cleared first: an expression the engine rejects leaves the previous colour standing, which - * would report a stale answer as a fresh one */ - _probe.style.setProperty('color', ''); - _probe.style.setProperty('color', expr, 'important'); - return getComputedStyle(_probe).color; -} - -/* A computed colour -> [r,g,b] 0..255, or null. Rasterised, not parsed: a computed `color` keeps the - * space it was authored in, so `oklch(0.54 0.19 300)` parses as three numbers in the wrong units - * (measured: #010078, graded "Too faint to read"). Painting one pixel makes the engine convert. */ -let _cx = null; -function rasterCtx() { - if (!_cx) { - const cv = document.createElement('canvas'); - cv.width = cv.height = 1; - _cx = cv.getContext('2d', { willReadFrequently: true }); - } - return _cx; -} -function parseColor(s) { - const cx = rasterCtx(); - /* fillStyle keeps the last value it could parse, so a colour this engine rejects would - * report the previous one as a fresh reading — the trap probeColor() clears for */ - cx.fillStyle = '#000'; - cx.fillStyle = String(s || ''); - cx.clearRect(0, 0, 1, 1); - cx.fillRect(0, 0, 1, 1); - const d = cx.getImageData(0, 0, 1, 1).data; - /* translucent: composite over nothing is meaningless for a readout */ - return d[3] === 255 ? [ d[0], d[1], d[2] ] : null; -} - -/* WCAG 2.x relative luminance and contrast ratio, on sRGB. Used only to report: the theme states - * what a colour costs and leaves the choice with the user, never correcting it (03-palettes.css - * derives the ink over a fill, which is a different question). */ -function luminance(rgb) { - const c = rgb.map((v) => { - const x = v / 255; - return (x <= .03928) ? (x / 12.92) : Math.pow((x + .055) / 1.055, 2.4); - }); - return (.2126 * c[0]) + (.7152 * c[1]) + (.0722 * c[2]); -} -function contrastRatio(fgExpr, bgExpr) { - const fg = parseColor(probeColor(fgExpr)), bg = parseColor(probeColor(bgExpr)); - if (!fg || !bg) return null; - const a = luminance(fg), b = luminance(bg); - return (Math.max(a, b) + .05) / (Math.min(a, b) + .05); -} - -function toHex(s) { - const rgb = parseColor(s) || [ 0, 0, 0 ]; - return '#' + rgb.map((v) => v.toString(16).padStart(2, '0')).join(''); -} - -/* One colour axis: a native swatch, a hex field and a button back to the palette's own colour. - * Reports through onPick as a hex string, or 0 for "back to the palette". */ -function colorControl(onPick, label, o) { - const swatch = E('input', { 'type': 'color', 'class': 'fs-color-swatch', 'aria-label': label }); - const field = E('input', { - 'type': 'text', 'class': 'fs-color-hex', 'spellcheck': 'false', 'autocomplete': 'off', - 'inputmode': 'text', 'maxlength': '7', 'aria-label': label - }); - const clear = E('button', { 'class': 'btn fs-color-clear', 'type': 'button' }, [ _('Palette', 'footstrap') ]); - const ratio = o.contrast ? E('div', { 'class': 'cbi-value-description fs-color-contrast' }) : null; - - let typed = false; - - /* Repaint everything that mirrors the axis. Called after every edit, and through the returned - * refresh() after a preset, palette switch or dark-mode flip — each changes what the palette's - * own colour is while this axis stays off. */ - function reflect(v) { - const live = probeColor(o.probe); - const hex = (typeof v === 'string') ? v : toHex(live); - swatch.value = hex; - /* do not fight the user mid-edit: `#0` is a legal prefix, and overwriting the field on - * every keystroke made the input impossible to type into */ - if (document.activeElement !== field) { field.value = hex; typed = false; } - /* the button back to the palette doubles as the axis state readout: enabled means the axis - * holds a colour of its own, disabled means the field shows the palette's */ - clear.disabled = !v; - if (!ratio) return; - const r = contrastRatio(o.contrast.fg, o.contrast.bg); - if (r === null) { ratio.hidden = true; ratio.removeAttribute('title'); return; } - /* The readout states what the ratio means; the number itself stays in the title. Thresholds are - * WCAG AA: 4.5:1 for body text, 3:1 for large text and for a UI shape. */ - const where = o.contrast.label; - const grade = (o.contrast.kind === 'shape') - ? ((r >= 3) - ? { cls: 'fs-contrast-aa', text: _('Clearly visible %s', 'footstrap').format(where) } - : { cls: 'fs-contrast-aa-large', text: _('Barely visible %s', 'footstrap').format(where) }) - : (r >= 4.5) - ? { cls: 'fs-contrast-aa', text: _('Easy to read %s', 'footstrap').format(where) } - : (r >= 3) - ? { cls: 'fs-contrast-aa-large', text: _('Hard to read %s — large text only', 'footstrap').format(where) } - : { cls: 'fs-contrast-low', text: _('Too faint to read %s', 'footstrap').format(where) }; - ratio.className = 'fs-color-contrast ' + grade.cls; - ratio.textContent = grade.text; - /* a pass says nothing: nine "easy to read" lines buried the one that was not */ - ratio.hidden = (grade.cls === 'fs-contrast-aa'); - ratio.title = _('Contrast %s:1 (WCAG AA wants %s:1 here)', 'footstrap') - .format(r.toFixed(1), (o.contrast.kind === 'shape') ? '3' : '4.5'); - } - - const pick = (v) => { onPick(v); reflect(v); }; - - swatch.addEventListener('input', () => pick(swatch.value.toLowerCase())); - /* commit on blur and Enter, not per keystroke: a half-typed `#0096` would repaint the page - * under the cursor. An unparseable value snaps back to what the axis holds, so the field - * cannot claim a colour the page is not painted in. An untouched field returns first, so a - * bare Tab-through never pins the shown palette colour on any of the 9 axes. */ - const commit = () => { - if (!typed) return; - const v = field.value.trim().toLowerCase(); - if ((/^#[0-9a-f]{6}$/).test(v)) pick(v); - /* re-read rather than a cached copy: the page can change what the axis holds behind this - * control (a preset, Reset to built-in), which a snapshot taken at build time would miss */ - else reflect(o.read()); - }; - field.addEventListener('input', () => { typed = true; }); - field.addEventListener('blur', commit); - field.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); commit(); } }); - clear.addEventListener('click', () => pick(0)); - - const wrap = E('div', { 'class': 'fs-colorctl' }, [ - E('div', { 'class': 'fs-color-row' }, [ swatch, field, clear ]) - ].concat(ratio ? [ ratio ] : [])); - /* the caller decides when this runs: probeColor() needs the document, and this control is not - * in it yet */ - wrap.fsRefresh = () => reflect(o.read()); - return wrap; -} - -/* Every stamp that moves a colour lands on :root, including head.ut's OS listener and an app - * rewriting data-darkmode, neither of which calls back here. */ -let _watch = null; -function unwatchColours() { - if (_watch) _watch.disconnect(); - _watch = null; -} -router.onNavigate(unwatchColours); -function watchColours(refresh) { - unwatchColours(); - _watch = new MutationObserver(refresh); - _watch.observe(document.documentElement, { - attributes: true, attributeFilter: [ 'data-darkmode', 'data-palette', 'data-wallpaper' ] - }); -} - -/* Build the page; renderStandalone() hands it over. Four tabs, most-used first: the colour scheme, - * the interface, the background with its glass and the colours nobody touches twice; Saving sits - * under them. Everything applies at once and is this browser's, in localStorage; only "Save to - * router" writes anything, pushing the current look to the router for other browsers. */ -function build() { - /* 24.10.0-24.10.2 have no ui.RangeSlider and a package from the feed still installs there (opkg does - * not version luci-base); one readable line beats the raw "not a constructor" TypeError. */ - if (!ui.RangeSlider) - return E('p', { 'class': 'alert-message warning' }, [ _('Footstrap needs OpenWrt 24.10.3 or newer.', 'footstrap') ]); - - /* every saved axis re-checks the Save button after applying, so it greys the moment this - * browser matches the saved default and un-greys when it diverges. Wrapped around the appliers - * because the controls call them directly and have no other seam back here. */ - const bump = (fn) => (v) => { fn(v); refreshSave(); }; - - /* Every colour control mirrors something it does not own: the palette's colour while its own - * axis is off, and the contrast that colour lands at. A palette switch or dark-mode flip moves - * all of it under controls nobody touched, so they refresh together. */ - const colourCtls = []; - const refreshColours = () => colourCtls.forEach((c) => c.fsRefresh()); - const repaint = (fn) => (v) => { fn(v); refreshColours(); }; - - /* One captioned row in LuCI's own shape: `.cbi-value` > `label.cbi-value-title` + - * `.cbi-value-field`, which base/30-forms.css and theme/60-inputs.css already lay out. `make` is - * handed the caption because each control needs it again as the aria-label of its inner - * input/select (a stock widget's wrapper div cannot carry a name). `o.desc` is a help line under - * the control, `o.extra` the rows with more than a control, `o.cls` marks the rows CSS singles out. */ - const group = (label, make, opts) => { - const o = opts || {}; - const desc = o.desc ? [ E('div', { 'class': 'cbi-value-description' }, [ o.desc ]) ] : []; - return E('div', { 'class': 'cbi-value' + (o.cls ? ' ' + o.cls : '') }, [ - E('label', { 'class': 'cbi-value-title' }, [ label ]), - E('div', { 'class': 'cbi-value-field' }, [ make(label) ].concat(desc, o.extra || [])) - ]); - }; - - /* the three literals every colour row repeats; a string literal survives minification intact */ - const CARD_BG = 'var(--fs-panel)', INK = 'var(--fs-text)', ON_CARD = _('on a card', 'footstrap'); - - /* A list of more than five, the palette, is a `ui.Select`; every number a `ui.RangeSlider`, already - * dressed by base/30-forms.css and theme/60-inputs.css (`ui.RangeSlider` reached openwrt-24.10 in - * 24.10.3). Listen for `widget-change`, the seam UIElement publishes, not an event on the inner - * element. RangeSlider also emits `widget-update` while the handle moves, which resizes the page - * under the drag; the appliers are idempotent, so wiring both costs nothing. */ - const selectCtl = (current, choices, apply, label) => { - /* an array of [value, label] pairs when a key is integer-like: Object.keys hoists it ahead of - * the authored order ('2020' rendered above Footstrap, the default); `sort` pins the order */ - const pairs = Array.isArray(choices) ? choices : Object.entries(choices); - const w = new ui.Select(String(current), Object.fromEntries(pairs), { widget: 'select', sort: pairs.map((p) => p[0]) }); - const node = w.render(); - node.querySelector('select, input').setAttribute('aria-label', label); - node.addEventListener('widget-change', () => apply(w.getValue())); - return node; - }; - - /* Two to three short choices are one row of pills, every choice in sight: native radios under one - * name, so the arrow keys move between them and a screen reader counts them; 80-appearance.css - * draws the pills. */ - let segSeq = 0; - const segCtl = (current, choices, apply, label) => { - const name = 'fs-ap-seg-' + (++segSeq); - const pairs = Array.isArray(choices) ? choices : Object.entries(choices); - return E('div', { 'class': 'fs-seg', 'role': 'radiogroup', 'aria-label': label }, pairs.map(([ v, text ]) => { - const input = E('input', { 'type': 'radio', 'name': name, 'value': v }); - input.checked = (String(current) === v); - input.addEventListener('change', () => { if (input.checked) apply(v); }); - return E('label', {}, [ input, E('span', {}, [ text ]) ]); - })); - }; - - const sliderCtl = (axis, apply, label, opts) => { - const o = opts || {}; - const w = new ui.RangeSlider(String(axis.current()), { - min: axis.min, max: axis.max, step: o.step || 1 - }); - const node = w.render(); - node.querySelector('select, input').setAttribute('aria-label', label); - const push = () => apply(parseInt(w.getValue(), 10)); - /* `live: false` for an axis whose value moves this control: Content width widens the column the - * page is drawn in, so applying mid-drag slides the handle from under the pointer and one small - * drag runs to the maximum. A keyboard user still applies at once (an arrow key fires `change`). */ - if (o.live !== false) node.addEventListener('widget-update', push); - node.addEventListener('widget-change', push); - return node; - }; - - const colourGroup = (label, axis, probe, contrast, opts = {}) => group(label, (lbl) => { - const ctl = colorControl(bump(axis.apply), lbl, { - probe: probe, - read: axis.current, - contrast: contrast - }); - colourCtls.push(ctl); - return ctl; - }, opts); - - /* a card is a stock `.cbi-section` under its own title; a tab pane is one whose title is its tab - * (ui.tabs reads data-tab-title); a subheading splits either in two */ - const card = (title, rows) => E('div', { 'class': 'cbi-section fs-ap-card' }, [ E('h3', {}, [ title ]) ].concat(rows)); - const pane = (name, title, rows) => E('div', { 'class': 'cbi-section fs-ap-card', 'data-tab': name, 'data-tab-title': title }, rows); - const sub = (title, cls) => E('div', { 'class': 'fs-ap-sub' + (cls ? ' ' + cls : '') }, [ E('h4', {}, [ title ]) ]); - - /* Every label here carries the 'footstrap' context: LuCI serves one merged catalogue and the first - * archive holding the hash wins, so a bare msgid is a global name any luci-app can win. */ - - /* ---- the colour scheme: what most visits come to change ---- */ - const scheme = pane('scheme', _('Colour scheme', 'footstrap'), [ - group(_('Mode', 'footstrap'), (label) => segCtl(prefs.currentMode(), [ - [ 'auto', _('Auto', 'footstrap') ], - [ 'light', _('Light', 'footstrap') ], - [ 'dark', _('Dark', 'footstrap') ] - ], bump(prefs.applyMode), label)), - - group(_('Palette', 'footstrap'), (label) => selectCtl(axes.palette.current(), [ - [ 'footstrap', 'Footstrap' ], - [ 'hicontrast', 'Hi-Contrast' ], - [ 'bootstrap', 'Bootstrap' ], - /* names another package, luci-theme-openwrt-2020, whose colourway this is */ - [ '2020', 'OpenWrt 2020' ], - [ 'forum', 'Forum' ] - ], bump(axes.palette.apply), label)), - - /* Accent recolours the accented controls (buttons, toggles, sliders, focus rings), not the - * canvas; any #rrggbb is taken. Read as text on a card, the use that fails first (as a fill it - * carries derived ink). */ - colourGroup(_('Accent', 'footstrap'), axes.accent, 'var(--fs-accent)', - { fg: 'var(--fs-accent)', bg: CARD_BG, label: ON_CARD }) - ]); - - /* ---- the interface ---- */ - const iface = pane('interface', _('Interface', 'footstrap'), [ - group(_('Layout', 'footstrap'), (label) => segCtl(prefs.currentLayout(), [ - [ 'sidebar', _('Sidebar', 'footstrap') ], - [ 'top', _('Top', 'footstrap') ] - ], bump(prefs.applyLayout), label)), - - /* always built and hidden by CSS in the top layout (:root[data-layout="top"] .fs-ap-submenus): - * the page is built once, so a currentLayout() branch would freeze it to the layout the page - * loaded in while CSS morphs the chrome live */ - group(_('Submenus', 'footstrap'), (label) => segCtl(prefs.currentAutoCollapse() ? 'on' : 'off', [ - [ 'off', _('Keep open', 'footstrap') ], - [ 'on', _('Auto-collapse', 'footstrap') ] - ], bump(prefs.applyAutoCollapse), label), { cls: 'fs-ap-submenus' }), - - group(_('Density', 'footstrap'), (label) => segCtl(axes.density.current(), [ - [ 'compact', _('Compact', 'footstrap') ], - [ 'normal', _('Normal', 'footstrap') ], - [ 'large', _('Large', 'footstrap') ] - ], bump(axes.density.apply), label)), - - /* the content column's own cap, separate from Density. The slider starts at the 1280px every - * page was designed against and only widens, which also keeps --fs-content-min (500px, what - * the sidebar-to-bar fold is measured against) out of reach by construction. */ - group(_('Content width', 'footstrap'), - (label) => sliderCtl(axes.contentWidth, bump(axes.contentWidth.apply), label, { step: 40, live: false })), - - group(_('Rounding', 'footstrap'), - (label) => sliderCtl(axes.radius, bump(axes.radius.apply), label)) - ]); - - /* ---- the background: the canvas, the wallpaper and the glass over it ---- - * The rows a value brings are siblings of the Wallpaper row: nested in its `.cbi-value-field` - * they started 216px right of every other control (measured on the router). They are hidden as - * a group, as stock pages do with a dependent field. The pills are the per-browser switch that - * keeps the Save button honest; Choose/Remove only swap the picture and never touch the axis. */ - const REMOVE = _('Remove', 'footstrap'); - const background = (() => { - const err = E('div', { 'class': 'fs-ap-err', 'role': 'alert', 'hidden': '' }); - const preview = E('img', { 'class': 'fs-ap-bgprev', 'alt': '', 'hidden': '' }); - const fileInput = E('input', { 'type': 'file', 'accept': 'image/*', 'style': 'display:none' }); - const chooseLabel = _('Choose image', 'footstrap'); - const chooseBtn = E('button', { 'class': 'btn cbi-button', 'type': 'button' }, [ chooseLabel ]); - const removeBtn = E('button', { 'class': 'btn cbi-button-remove', 'type': 'button', 'hidden': '' }, [ REMOVE ]); - - const patErr = E('div', { 'class': 'fs-ap-err', 'role': 'alert', 'hidden': '' }); - const patPreview = E('img', { 'class': 'fs-ap-bgprev', 'alt': '', 'hidden': '' }); - const patInput = E('input', { 'type': 'file', 'accept': 'image/svg+xml,.svg', 'style': 'display:none' }); - const patChooseLabel = _('Choose SVG', 'footstrap'); - const patChoose = E('button', { 'class': 'btn cbi-button', 'type': 'button' }, [ patChooseLabel ]); - const patRemove = E('button', { 'class': 'btn cbi-button-remove', 'type': 'button', 'hidden': '' }, [ REMOVE ]); - - /* Colours decides whether the file's own palette is kept or replaced by the theme's: a mask - * uses the alpha only, right for line art and wrong for artwork with its own colours. */ - const patRows = [ - group(_('Pattern', 'footstrap'), - () => E('div', { 'class': 'fs-ap-bgrow' }, [ patChoose, patRemove ]), - { extra: [ patInput, patPreview, patErr ] }), - group(_('Scale', 'footstrap'), (lbl) => sliderCtl(axes.patternSize, bump(axes.patternSize.apply), lbl, { step: 20 })), - group(_('Strength', 'footstrap'), (lbl) => sliderCtl(axes.patternStrength, bump(axes.patternStrength.apply), lbl, { step: 5 })), - group(_('Colours', 'footstrap'), (lbl) => segCtl(axes.patternInk.current(), [ - [ 'theme', _('Theme', 'footstrap') ], - [ 'original', _('As in file', 'footstrap') ] - ], bump(axes.patternInk.apply), lbl)) - ]; - /* Dim is an ordinary saved axis, so it is bump()-ed or the Save button misreports its status */ - const fileRows = [ - group(_('File', 'footstrap'), - () => E('div', { 'class': 'fs-ap-bgrow' }, [ chooseBtn, removeBtn ]), - { extra: [ fileInput, preview, err ] }), - group(_('Dim', 'footstrap'), (lbl) => sliderCtl(axes.photoDim, bump(axes.photoDim.apply), lbl, { step: 5 })) - ]; - - function reflect(tok) { - if (tok) { preview.src = axes.loginBg.url(tok); preview.hidden = false; removeBtn.hidden = false; } - else { preview.removeAttribute('src'); preview.hidden = true; removeBtn.hidden = true; } - } - function reflectPattern(tok) { - if (tok) { patPreview.src = axes.pattern.url(tok); patPreview.hidden = false; patRemove.hidden = false; } - else { patPreview.removeAttribute('src'); patPreview.hidden = true; patRemove.hidden = true; } - } - /* `hidden` on the row, which base/10-reset.css restates with !important (the UA's bare - * `[hidden]` loses to `.cbi-value`'s display). Hidden, not removed: each row holds a live - * control that a rebuild would freeze to its construction-time state. */ - function togglePanel(v) { - patRows.forEach((r) => { r.hidden = (v !== 'pattern'); }); - fileRows.forEach((r) => { r.hidden = (v !== 'file'); }); - } - reflect(axes.loginBg.current()); - reflectPattern(axes.pattern.current()); - togglePanel(axes.wallpaper.current()); - - const setWallpaper = (v) => { axes.wallpaper.apply(v); refreshSave(); togglePanel(v); }; - - /* Both uploads share one wiring (pick, upload, report, remove) and never move the Wallpaper - * pills, which the user may change mid-upload. The input is cleared on every change so - * re-picking the same file fires `change`; the label is restored in `finally`, or a failed - * upload leaves "Uploading…" standing. */ - const wireUploader = (u) => { - const fail = (e) => { u.err.textContent = String((e && e.message) || e); u.err.hidden = false; }; - u.choose.addEventListener('click', () => { u.err.hidden = true; u.input.click(); }); - u.input.addEventListener('change', () => { - const f = u.input.files && u.input.files[0]; - u.input.value = ''; - if (!f) return; - u.err.hidden = true; u.choose.disabled = true; - u.choose.textContent = _('Uploading…', 'footstrap'); - u.upload(f) - .then(u.reflect) - .catch(fail) - .finally(() => { u.choose.disabled = false; u.choose.textContent = u.label; }); - }); - u.remove.addEventListener('click', () => { - u.err.hidden = true; u.remove.disabled = true; - u.drop().then(() => u.reflect('')).catch(fail) - .finally(() => { u.remove.disabled = false; }); - }); - }; - - wireUploader({ - choose: patChoose, remove: patRemove, input: patInput, err: patErr, - label: patChooseLabel, reflect: reflectPattern, - upload: assets.uploadPattern, drop: assets.removePattern - }); - - wireUploader({ - choose: chooseBtn, remove: removeBtn, input: fileInput, err: err, - label: chooseLabel, reflect: reflect, - upload: assets.uploadLoginBg, drop: assets.removeLoginBg - }); - - /* The glass rows show only over a wallpaper (CSS: .fs-ap-glass), Material only while a - * surface is glass at all, Blur only for a material that blurs. An OS asking for less - * transparency or more contrast keeps the surfaces solid whatever the sliders say, and the - * page says so. */ - const glassRows = [ - sub(_('Transparency', 'footstrap'), 'fs-ap-glass'), - group(_('Card opacity', 'footstrap'), - (lbl) => sliderCtl(axes.cardOpacity, bump(axes.cardOpacity.apply), lbl, { step: 5 }), { - cls: 'fs-ap-glass', - desc: axes.glassBlocked() ? _('The system asks for less transparency or more contrast, so surfaces stay solid.', 'footstrap') : null - }), - group(_('Menu opacity', 'footstrap'), - (lbl) => sliderCtl(axes.barOpacity, bump(axes.barOpacity.apply), lbl, { step: 5 }), { cls: 'fs-ap-glass' }), - group(_('Material', 'footstrap'), (lbl) => segCtl(axes.material.current(), [ - [ 'flat', _('Flat', 'footstrap') ], - [ 'frosted', _('Frosted', 'footstrap') ], - [ 'acrylic', _('Acrylic', 'footstrap') ] - ], bump(axes.material.apply), lbl), { cls: 'fs-ap-glass fs-ap-material' }), - group(_('Blur', 'footstrap'), - (lbl) => sliderCtl(axes.blur, bump(axes.blur.apply), lbl, { step: 10 }), { cls: 'fs-ap-glass fs-ap-material fs-ap-blur' }) - ]; - - return pane('background', _('Background', 'footstrap'), [ - /* the canvas colour that tells routers apart; CSS hides it under a photo, which covers it */ - colourGroup(_('Tint', 'footstrap'), axes.tint, 'var(--fs-bg)', { - fg: 'var(--fs-text)', bg: 'var(--fs-bg)', label: _('on the canvas', 'footstrap') - }, { cls: 'fs-ap-tint', desc: _('Colours the page so you can tell routers apart.', 'footstrap') }), - - /* the strength half of the Tint, meaningful only in hue mode (a hex canvas has no chroma of - * ours to scale); CSS hides it otherwise. Not named "Density": the string is also the - * aria-label, so a screen reader would announce two rows under one name. */ - group(_('Tint strength', 'footstrap'), - (label) => sliderCtl(axes.tintStrength, bump(repaint(axes.tintStrength.apply)), label, { - step: 5 - }), { cls: 'fs-ap-tint fs-ap-tintstr' }), - - group(_('Wallpaper', 'footstrap'), (label) => segCtl(axes.wallpaper.current(), [ - [ 'off', _('Off', 'footstrap') ], - [ 'pattern', _('Pattern', 'footstrap') ], - [ 'file', _('File', 'footstrap') ] - ], setWallpaper, label)) - ].concat(patRows, fileRows, glassRows)); - })(); - - /* ---- the colours nobody touches twice: the last tab ---- - * The three status colours, then the surfaces: the sheet the UI is drawn on. Body text is read on - * each surface, so each reports --fs-text against itself; no ink is derived, since moving - * --fs-text would recolour what is measured against. The hairline takes the 3:1 UI-component - * threshold (a border is a shape). The rows are data because a string literal repeated eight - * times is not mangled by the minifier. */ - const colourRows = [ - ...[ - [ _('Good', 'footstrap'), axes.good, 'var(--fs-good)' ], - [ _('Warning', 'footstrap'), axes.warn, 'var(--fs-warn)' ], - [ _('Danger', 'footstrap'), axes.danger, 'var(--fs-danger)' ] - ].map(([ label, axis, ink ]) => - colourGroup(label, axis, ink, { fg: ink, bg: CARD_BG, label: ON_CARD })), - sub(_('Surfaces', 'footstrap')), - ...[ - [ _('Cards', 'footstrap'), axes.card, CARD_BG, INK, CARD_BG, ON_CARD ], - [ _('Controls', 'footstrap'), axes.control, 'var(--fs-panel2)', INK, 'var(--fs-panel2)', _('on a control', 'footstrap') ], - [ _('Sidebar and bar', 'footstrap'), axes.bar, 'var(--fs-bar-bg)', INK, 'var(--fs-bar-bg)', _('in the sidebar', 'footstrap') ], - [ _('Borders', 'footstrap'), axes.line, 'var(--fs-border)', 'var(--fs-border)', CARD_BG, ON_CARD, 'shape' ] - ].map(([ label, axis, probe, fg, bg, where, kind ]) => - colourGroup(label, axis, probe, { fg, bg, label: where, kind })) - ]; - - /* ---- saving: the router default and the version ---- - * Save writes the current look to /etc/config/footstrap over the scoped uci ACL. It does not - * change this browser (localStorage keeps overriding), so it only shows on a fresh browser. */ - /* each label is said twice in build(); hoisted, a msgid is paid once (24 B x2 -> 30 B) */ - const SAVE_TO_ROUTER = _('Save to router', 'footstrap'); - const RESET_TO_ROUTER = _('Reset to router', 'footstrap'); - const RESET_TO_BUILTIN = _('Reset to built-in', 'footstrap'); - const saveBtn = E('button', { 'class': 'btn cbi-button-action', 'type': 'button' }, [ SAVE_TO_ROUTER ]); - /* Two resets, because two things sit underneath a browser's tweaks: "Reset to router" clears them - * and lets every axis fall back to the router, "Reset to built-in" writes the theme's defaults - * explicitly. Neither touches /etc/config/footstrap. "Default" is not a label: it named the - * router's look in "Save as default" and the theme's in "Reset to default", three names for two - * states (forum topic 251930, post 92). */ - const resetSavedBtn = E('button', { 'class': 'btn', 'type': 'button' }, [ RESET_TO_ROUTER ]); - const resetBtn = E('button', { 'class': 'btn cbi-button-negative', 'type': 'button' }, [ RESET_TO_BUILTIN ]); - /* Save's only visible failure surface: the rpc rejecting (expired session 403, missing ACL, ubus - * down). A deleted config is not caught: commit no-ops and returns success (measured on the - * router); the package owns that file and the read side falls back to built-in defaults. */ - const saveErr = E('div', { 'class': 'fs-ap-err', 'role': 'alert', 'hidden': '' }); - - /* The Save button is the status: matching disables it, diverging enables it. A browser that - * refuses storage keeps its choices in fs-prefs's in-memory map, so the comparison stays honest; - * the note waits while a failed Save's error shows, or the .finally below would overwrite it. */ - function refreshSave() { - if (prefs.storageBroken() && saveErr.hidden) { - saveErr.textContent = _('This browser is not storing preferences (site data is blocked), so a change here lasts until you reload. Saving to the router still works and applies to every browser.', 'footstrap'); - saveErr.hidden = false; - } - const saved = axes.matchesSavedDefault(); - saveBtn.disabled = saved; - saveBtn.textContent = saved ? _('Saved to router', 'footstrap') : SAVE_TO_ROUTER; - } - saveBtn.addEventListener('click', () => { - saveBtn.disabled = true; - saveErr.hidden = true; - saveErr.title = ''; /* a stale rpc error would tooltip the storage note (measured: 'ubus code 6' over it) */ - axes.saveAsDefault() - /* on failure refreshSave re-enables the button so the user can retry; the usual cause - * is a stale session, which a reload fixes. The raw rpc error stays in a title - * tooltip. */ - .catch((e) => { - saveErr.textContent = _('Could not save to the router. Reload the page and try again.', 'footstrap'); - saveErr.title = String((e && e.message) || e); - saveErr.hidden = false; - }) - .finally(refreshSave); - }); - /* Two-click confirm on both: discarding local tweaks is destructive and a native confirm() is - * banned in this UI. Arming one disarms the other, so a primed button cannot be fired by a - * click meant for its neighbour. */ - let armed = null; - function disarm() { - if (!armed) return; - armed.btn.textContent = armed.label; - armed.btn.classList.remove('fs-ap-armed'); - armed = null; - } - function twoClick(btn, label, run) { - btn.addEventListener('click', () => { - if (armed && armed.btn === btn) { - disarm(); - run(); - location.reload(); - return; - } - disarm(); - armed = { btn: btn, label: label }; - btn.textContent = _('Confirm reset', 'footstrap'); - btn.classList.add('fs-ap-armed'); - }); - } - twoClick(resetSavedBtn, RESET_TO_ROUTER, axes.resetToSaved); - twoClick(resetBtn, RESET_TO_BUILTIN, axes.resetToBuiltin); - refreshSave(); /* correct label and enabled state before the first paint */ - - const versionLink = E('a', { - 'class': 'fs-ap-version', - 'href': ver.REPO_URL, - 'target': '_blank', - /* `noreferrer` alone: it implies noopener, and the theme's other outward links spell it - * that way */ - 'rel': 'noreferrer' - }, [ ver.label() ]); - - /* The theme's translations are their own packages, and nothing in a package manager can read `uci - * luci.main.lang` to fetch the right one. */ - const lang = (document.documentElement.getAttribute('lang') || '').trim(); - const untranslated = lang && lang !== 'en' && lang !== 'auto' && - _('Layout', 'footstrap') === 'Layout'; - const missing = untranslated ? E('div', { 'class': 'fs-ap-verrow fs-ap-i18n' }, [ - E('span', {}, [ _('This theme is not translated on this router yet.', 'footstrap') + ' ' ]), - E('code', {}, [ 'luci-i18n-footstrap-' + lang ]) - ]) : ''; - - const saving = card(_('Saving', 'footstrap'), [ - E('p', { 'class': 'fs-ap-lead' }, [ _('Footstrap theme settings apply at once and are stored permanently in this browser.', 'footstrap') ]), - E('div', { 'class': 'fs-ap-actrow' }, [ saveBtn, E('span', { 'class': 'fs-ap-actgap', 'aria-hidden': 'true' }), resetSavedBtn, resetBtn ]), - saveErr, - E('ul', { 'class': 'fs-ap-help' }, [ - E('li', {}, [ _('Save to router — store this look on the router. Browsers with a look of their own keep it.', 'footstrap') ]), - E('li', {}, [ _('Reset to router — drop this browser\'s changes and follow the router.', 'footstrap') ]), - E('li', {}, [ _('Reset to built-in — go back to the look the theme ships with.', 'footstrap') ]) - ]), - E('div', { 'class': 'fs-ap-footer' }, [ - E('div', { 'class': 'fs-ap-verrow' }, [ versionLink ]), - missing - ]) - ]); - - /* LuCI's own tab group, as on every tabbed form: it puts the strip before the panes and keeps - * the open tab for the session. A pane shown again re-reads its colours, which a palette or - * dark-mode change may have moved while it was hidden. */ - const panes = E('div', { 'class': 'fs-ap-panes' }, [ - scheme, - iface, - background, - pane('colours', _('Colours', 'footstrap'), colourRows) - ]); - const page = E('div', { 'class': 'fs-ap' }, [ panes, saving ]); - ui.tabs.initTabGroup(panes.childNodes); - panes.addEventListener('cbi-tab-active', refreshColours); - - Promise.resolve().then(refreshColours); - - watchColours(refreshColours); - return page; -} - -return baseclass.extend({ - /* the dispatched page's whole render(), called by view/footstrap/appearance.js */ - renderStandalone: () => Promise.resolve().then(build), -}); diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js deleted file mode 100644 index 988c07e6..00000000 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js +++ /dev/null @@ -1,291 +0,0 @@ -'use strict'; -'require baseclass'; -'require rpc'; -'require ui'; -'require fs-axes as axes'; - -/* fs-assets — putting a file on the router and taking it off again: the pattern and login-photo uploads, - * the SVG sanitizer, the rollback. A module of its own: only the Appearance page needs these ~4 KB. */ - -/* Hoisted: a string literal is not mangled, so each repeat is paid on flash; msgid stays literal - * for update-po.sh. */ -const MSG_UPLOAD_FAILED = _('Upload failed.', 'footstrap'); -const MSG_NOT_SVG = _('That file is not an SVG image.', 'footstrap'); -const MSG_BAD_IMAGE = _('Could not process the image.', 'footstrap'); -const MSG_PICK_SVG = _('Please choose an SVG file.', 'footstrap'); -/* One template, two numbers: a translator places both %d in one sentence instead of agreeing - * plurals. */ -const MSG_SANITIZED = _('Cleaned before upload: removed %d unsafe element(s) and %d external reference(s).', 'footstrap'); -const MSG_NOTHING_LEFT = _('That SVG had nothing left once the unsafe parts were removed.', 'footstrap'); - -/* ---- the pattern: an SVG the admin uploads, tiled and recoloured ---- - * Fixed path matched by the rpcd ACL, under /etc so an upgrade keeps it. Not the security boundary: a - * `curl` to cgi-upload skips every check below; a direct open is stopped by the CGI's - * `Content-Security-Policy: default-src 'none'; sandbox`. */ -const PAT_PATH = '/etc/footstrap/pattern.svg'; /* cgi-upload target; the ACL grants exactly this */ -const PAT_MAX = 512 * 1024; /* a tile that has to reach a router's flash and then every page load */ -/* What `_sanitizeSvg` removes, judged on the parsed document, not the text. Painted as an image nothing - * loads; these rules cover a direct open of the file: - * - elements: only the SVG namespace stays (`` ran same-origin), minus - * script, foreignObject, a ` - {% endif %} - - - {# - Lets an admin install this page as an app (icon, own window, no address bar), worth more - than a tab on the phone LuCI is opened from when the network is broken. Both files are - static, a theme having no dispatcher node to render them per request: start_url is - /cgi-bin/luci/, uhttpd's default (a moved LuCI opens the standard path), and - background/theme_color is the default palette's page colour, read once at install time. - .json, not .webmanifest: uhttpd's compiled-in MIME map has no entry for that extension, - so it answers application/octet-stream. - #} - - {# - iOS reads this link and never the manifest's icons array, and it scales the 192 square the - manifest also uses down to its own 180. One raster serves both. - #} - - {% if (dispatched?.css): %} - {# - The `css` property of the dispatched menu.d node: a stylesheet the server links for the - page instead of the view injecting one at module eval. `dispatched`, not `node`: the - dispatcher exports the resolved node as `dispatched` and never binds `node`, so no ucode - theme emitted this link until openwrt/luci#8920. That commit also adds `css: 'string'` to - the node schema; without it the property is filtered out and this stays false, as on 24.10 - and 25.12. Versioned, as uhttpd sends no Cache-Control. Not `data-fs-shell`: the sheet - belongs to one page, so the SPA router must judge and contain it like any app sheet. - #} - - {% endif %} - {% if (css): %} - {# - no `title` attribute: on a - {% endif %} - {# - The catalogue is a CGI spawn on the critical path (30 ms on the router) that delivers 13 - bytes at lang=en, where no en catalogue exists because the msgids already are English. - The process is the cost, so emit those bytes inline when the language has no catalogue - and keep the tag when it has. The probe mirrors the server: load_catalog() reads every - `*..lmo` in /usr/lib/lua/luci/i18n, so a router shipping an en catalogue still gets - the tag; deciding by language NAME would drop it. Fails open: a throwing probe keeps the - tag, as a missing catalogue renders English and reports nothing. - #} - {%- let has_catalog = true; - try { has_catalog = length(require('fs').glob(`/usr/lib/lua/luci/i18n/*.${dispatcher.lang}.lmo`)) > 0; } catch (e) {} %} - {% if (has_catalog): %} - - {% else %} - - {% endif %} - {# - the one asset stock LuCI leaves without a cache key. Must stay synchronous and un-deferred: - luci.js captures `window.cbi_init` in its constructor, and `defer` is not an option for the - catalogue above either — footer.ut runs `L.require('menu-footstrap')` inline while the - parser is still running, so a module's `_()` would race a deferred window.TR. - #} - - diff --git a/luci-theme-footstrap/ucode/template/themes/footstrap/partials/notice.ut b/luci-theme-footstrap/ucode/template/themes/footstrap/partials/notice.ut deleted file mode 100644 index c6227eab..00000000 --- a/luci-theme-footstrap/ucode/template/themes/footstrap/partials/notice.ut +++ /dev/null @@ -1,16 +0,0 @@ -{# - One top-of-content warning notice: heading, sentence and an optional "go fix it" button. The - `dispatcher.lookup` guard must not drift: `path` is looked up before it is linked, so a build - without that page, or a user without rights to it, gets the warning and no dead link. - include(…, { title, text }) for a plain notice; add { path, label } for the button. - title/text are emitted unescaped: they are `_()` results over strings this theme ships, not - request data. - Licensed to the public under the Apache License 2.0. --#} -
-

{{ title }}

-

{{ text }}

- {% if (path && dispatcher.lookup(path)): %} - - {% endif %} -
diff --git a/luci-theme-footstrap/ucode/template/themes/footstrap/sysauth.ut b/luci-theme-footstrap/ucode/template/themes/footstrap/sysauth.ut deleted file mode 100644 index 429ea5e5..00000000 --- a/luci-theme-footstrap/ucode/template/themes/footstrap/sysauth.ut +++ /dev/null @@ -1,185 +0,0 @@ -{# - The LOGIN page. - - Copyright 2008 Steven Barth - Copyright 2008-2012 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. - - It exists because the generic template includes the header WITHOUT `blank_page: true`: with no - theme-local sysauth the whole chrome renders around the login form with every control dead. - No departure here may drop a field the backend posts. - Do not re-introduce a `