mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-04 04:43:56 +08:00
🤞 Sync 2026-08-10 09:21:08
This commit is contained in:
1 parent
c463113ea9
commit
2f0ec626c4
17 files changed
+392
-118
No files matched your search
+1
-1
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=gecoosac
|
PKG_NAME:=gecoosac
|
||||||
PKG_VERSION:=2.2.20251015
|
PKG_VERSION:=2.2.20251015
|
||||||
PKG_RELEASE:=19
|
PKG_RELEASE:=20
|
||||||
|
|
||||||
PKG_MAINTAINER:=Roc Lai <laipeng668@qq.com>
|
PKG_MAINTAINER:=Roc Lai <laipeng668@qq.com>
|
||||||
PKG_LICENSE:=AGPL-3.0-only
|
PKG_LICENSE:=AGPL-3.0-only
|
||||||
|
|||||||
@@ -86,50 +86,62 @@ normalize_path() {
|
|||||||
printf '%s\n' "$normalized"
|
printf '%s\n' "$normalized"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path_has_clear_stage_component() {
|
||||||
|
local path="$1" part rest
|
||||||
|
|
||||||
|
path="$(normalize_path "$path")" || return 1
|
||||||
|
rest="${path#/}"
|
||||||
|
while [ -n "$rest" ]; do
|
||||||
|
part="${rest%%/*}"
|
||||||
|
if [ "$part" = "$rest" ]; then
|
||||||
|
rest=""
|
||||||
|
else
|
||||||
|
rest="${rest#*/}"
|
||||||
|
fi
|
||||||
|
case "$part" in
|
||||||
|
.gecoosac-clear.*) return 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
path_uses_clear_stage() {
|
path_uses_clear_stage() {
|
||||||
local path="$1" resolved
|
local path="$1" resolved
|
||||||
|
|
||||||
path="$(normalize_path "$path")" || return 1
|
path_has_clear_stage_component "$path" && return 0
|
||||||
case "$path" in
|
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||||
/.gecoosac-clear.*|/.gecoosac-clear.*/*|*/.gecoosac-clear.*|*/.gecoosac-clear.*/*)
|
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||||
return 0
|
path_has_clear_stage_component "$resolved" && return 0
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -e "$1" ] || [ -L "$1" ]; then
|
|
||||||
resolved="$(readlink -f "$1" 2>/dev/null)" || return 1
|
|
||||||
resolved="$(normalize_path "$resolved")" || return 1
|
|
||||||
case "$resolved" in
|
|
||||||
/.gecoosac-clear.*|/.gecoosac-clear.*/*|*/.gecoosac-clear.*|*/.gecoosac-clear.*/*)
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_supported_upload_path() {
|
||||||
|
local path="$1" storage
|
||||||
|
|
||||||
|
path="$(normalize_path "$path")" || return 1
|
||||||
|
path_uses_clear_stage "$path" && return 1
|
||||||
|
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||||
|
|
||||||
|
case "$path" in
|
||||||
|
/mnt/*/gecoosac/upload)
|
||||||
|
storage="${path#/mnt/}"
|
||||||
|
storage="${storage%/gecoosac/upload}"
|
||||||
|
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||||
|
;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
is_safe_upload_dir() {
|
is_safe_upload_dir() {
|
||||||
local path physical
|
local path physical
|
||||||
|
|
||||||
path="$(normalize_path "$1")" || return 1
|
path="$(normalize_path "$1")" || return 1
|
||||||
physical="$(managed_dir_path upload "$path")" || return 1
|
physical="$(managed_dir_path upload "$path")" || return 1
|
||||||
case "$path" in
|
is_supported_upload_path "$path" || return 1
|
||||||
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
|
is_supported_upload_path "$physical"
|
||||||
esac
|
|
||||||
case "$physical" in
|
|
||||||
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
*/gecoosac/upload) ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
case "$physical" in
|
|
||||||
*/gecoosac/upload) return 0 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is_path_in_dir() {
|
is_path_in_dir() {
|
||||||
@@ -614,18 +626,35 @@ managed_dir_path() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_upload_storage_root() {
|
||||||
|
local path="$1" root
|
||||||
|
|
||||||
|
is_supported_upload_path "$path" || return 1
|
||||||
|
case "$path" in
|
||||||
|
"$DEFAULT_UPLOAD_DIR") root=/tmp ;;
|
||||||
|
/mnt/*/gecoosac/upload) root="${path%/gecoosac/upload}" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -d "$root" ] && [ ! -L "$root" ] || return 1
|
||||||
|
is_secure_upload_dir "$root"
|
||||||
|
}
|
||||||
|
|
||||||
ensure_managed_dir() {
|
ensure_managed_dir() {
|
||||||
local role="$1" path physical
|
local role="$1" path physical
|
||||||
|
|
||||||
path="$(normalize_path "$2")" || return 1
|
path="$(normalize_path "$2")" || return 1
|
||||||
|
physical="$(managed_dir_path "$role" "$path")" || return 1
|
||||||
case "$role" in
|
case "$role" in
|
||||||
upload) is_safe_upload_dir "$path" || return 1 ;;
|
upload)
|
||||||
|
is_safe_upload_dir "$path" || return 1
|
||||||
|
ensure_upload_storage_root "$physical" || return 1
|
||||||
|
;;
|
||||||
db) is_safe_db_dir "$path" "$upload_dir" || return 1 ;;
|
db) is_safe_db_dir "$path" "$upload_dir" || return 1 ;;
|
||||||
pid) is_safe_pid_dir "$path" "$upload_dir" || return 1 ;;
|
pid) is_safe_pid_dir "$path" "$upload_dir" || return 1 ;;
|
||||||
tls) [ "$path" = "/etc/gecoosac/tls" ] || return 1 ;;
|
tls) [ "$path" = "/etc/gecoosac/tls" ] || return 1 ;;
|
||||||
*) return 1 ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
physical="$(managed_dir_path "$role" "$path")" || return 1
|
|
||||||
ensure_secure_dir_tree "$physical" || return 1
|
ensure_secure_dir_tree "$physical" || return 1
|
||||||
[ -d "$path" ]
|
[ -d "$path" ]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,27 +90,62 @@ managed_dir_path() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path_has_clear_stage_component() {
|
||||||
|
local path="$1" part rest
|
||||||
|
|
||||||
|
path="$(normalize_path "$path")" || return 1
|
||||||
|
rest="${path#/}"
|
||||||
|
while [ -n "$rest" ]; do
|
||||||
|
part="${rest%%/*}"
|
||||||
|
if [ "$part" = "$rest" ]; then
|
||||||
|
rest=""
|
||||||
|
else
|
||||||
|
rest="${rest#*/}"
|
||||||
|
fi
|
||||||
|
case "$part" in
|
||||||
|
.gecoosac-clear.*) return 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
path_uses_clear_stage() {
|
||||||
|
local path="$1" resolved
|
||||||
|
|
||||||
|
path_has_clear_stage_component "$path" && return 0
|
||||||
|
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||||
|
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||||
|
path_has_clear_stage_component "$resolved" && return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_supported_upload_path() {
|
||||||
|
local path="$1" storage
|
||||||
|
|
||||||
|
path="$(normalize_path "$path")" || return 1
|
||||||
|
path_uses_clear_stage "$path" && return 1
|
||||||
|
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||||
|
|
||||||
|
case "$path" in
|
||||||
|
/mnt/*/gecoosac/upload)
|
||||||
|
storage="${path#/mnt/}"
|
||||||
|
storage="${storage%/gecoosac/upload}"
|
||||||
|
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||||
|
;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
is_safe_upload_dir() {
|
is_safe_upload_dir() {
|
||||||
local path physical
|
local path physical
|
||||||
|
|
||||||
path="$(normalize_path "$1")" || return 1
|
path="$(normalize_path "$1")" || return 1
|
||||||
physical="$(managed_dir_path upload "$path")" || return 1
|
physical="$(managed_dir_path upload "$path")" || return 1
|
||||||
case "$path" in
|
is_supported_upload_path "$path" || return 1
|
||||||
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
|
is_supported_upload_path "$physical"
|
||||||
esac
|
|
||||||
case "$physical" in
|
|
||||||
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
*/gecoosac/upload) ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
case "$physical" in
|
|
||||||
*/gecoosac/upload) return 0 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is_path_in_dir() {
|
is_path_in_dir() {
|
||||||
@@ -247,7 +282,7 @@ cleanup_legacy_upload_dir() {
|
|||||||
|
|
||||||
normalize_dir_option() {
|
normalize_dir_option() {
|
||||||
local option="$1"
|
local option="$1"
|
||||||
local validator="$3"
|
local validator="$2"
|
||||||
local value normalized upload_dir
|
local value normalized upload_dir
|
||||||
|
|
||||||
value="$(uci -q get "gecoosac.config.${option}")"
|
value="$(uci -q get "gecoosac.config.${option}")"
|
||||||
@@ -320,8 +355,8 @@ migrate_config() {
|
|||||||
ensure_section || exit 1
|
ensure_section || exit 1
|
||||||
migrate_config || exit 1
|
migrate_config || exit 1
|
||||||
normalize_upload_dir || exit 1
|
normalize_upload_dir || exit 1
|
||||||
normalize_dir_option db_dir "$DEFAULT_DB_DIR" is_safe_db_dir || exit 1
|
normalize_dir_option db_dir is_safe_db_dir || exit 1
|
||||||
normalize_dir_option piddir "$DEFAULT_PID_DIR" is_safe_pid_dir || exit 1
|
normalize_dir_option piddir is_safe_pid_dir || exit 1
|
||||||
set_default enabled 0 || exit 1
|
set_default enabled 0 || exit 1
|
||||||
set_default port 60650 || exit 1
|
set_default port 60650 || exit 1
|
||||||
set_default isonlyoneprot 1 || exit 1
|
set_default isonlyoneprot 1 || exit 1
|
||||||
|
|||||||
+11
-2
@@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk
|
|||||||
PKG_ARCH_ghttpd:=$(ARCH)
|
PKG_ARCH_ghttpd:=$(ARCH)
|
||||||
|
|
||||||
PKG_NAME:=ghttpd
|
PKG_NAME:=ghttpd
|
||||||
PKG_VERSION:=0.0.2
|
PKG_VERSION:=0.0.3
|
||||||
PKG_RELEASE:=4
|
PKG_RELEASE:=5
|
||||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/Carseason/openwrt-packages/releases/download/prebuilt/
|
PKG_SOURCE_URL:=https://github.com/Carseason/openwrt-packages/releases/download/prebuilt/
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
@@ -49,6 +49,15 @@ if [ -z "$${IPKG_INSTROOT}" ]; then
|
|||||||
fi
|
fi
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/postrm
|
||||||
|
#!/bin/sh
|
||||||
|
for service in uhttpd nginx; do
|
||||||
|
[ -x "/etc/init.d/$$service" ] || continue
|
||||||
|
"/etc/init.d/$$service" enabled && "/etc/init.d/$$service" restart
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
define Build/Configure
|
define Build/Configure
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ stop_default_httpd_services() {
|
|||||||
start_default_httpd_services() {
|
start_default_httpd_services() {
|
||||||
for service in uhttpd nginx; do
|
for service in uhttpd nginx; do
|
||||||
[ -x "/etc/init.d/$service" ] || continue
|
[ -x "/etc/init.d/$service" ] || continue
|
||||||
"/etc/init.d/$service" enabled && "/etc/init.d/$service" start
|
"/etc/init.d/$service" enabled && "/etc/init.d/$service" restart
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-gecoosac
|
PKG_NAME:=luci-app-gecoosac
|
||||||
PKG_VERSION:=2.2
|
PKG_VERSION:=2.2
|
||||||
PKG_RELEASE:=19
|
PKG_RELEASE:=20
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI Support for gecoosac
|
LUCI_TITLE:=LuCI Support for gecoosac
|
||||||
LUCI_DEPENDS:=+luci-base +gecoosac +gecoosac-files
|
LUCI_DEPENDS:=+luci-base +gecoosac +gecoosac-files
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const DEFAULT_DB_DIR = '/etc/gecoosac';
|
|||||||
const DEFAULT_CRT_FILE = '/etc/gecoosac/tls/gecoosac.crt';
|
const DEFAULT_CRT_FILE = '/etc/gecoosac/tls/gecoosac.crt';
|
||||||
const DEFAULT_KEY_FILE = '/etc/gecoosac/tls/gecoosac.key';
|
const DEFAULT_KEY_FILE = '/etc/gecoosac/tls/gecoosac.key';
|
||||||
const DEFAULT_PID_DIR = '/var/run';
|
const DEFAULT_PID_DIR = '/var/run';
|
||||||
const CONFIG_BACKUP_DIR = '/etc/gecoosac';
|
|
||||||
const DB_DIR_PREFIXES = [ '/etc/gecoosac', '/tmp/gecoosac', '/var/lib/gecoosac' ];
|
const DB_DIR_PREFIXES = [ '/etc/gecoosac', '/tmp/gecoosac', '/var/lib/gecoosac' ];
|
||||||
const PID_DIR_PREFIXES = [ '/var/run', '/tmp/gecoosac' ];
|
const PID_DIR_PREFIXES = [ '/var/run', '/tmp/gecoosac' ];
|
||||||
const CLEAR_STAGE_PATH_ERROR = _('Paths under .gecoosac-clear.* are reserved for upload cleanup.');
|
const CLEAR_STAGE_PATH_ERROR = _('Paths under .gecoosac-clear.* are reserved for upload cleanup.');
|
||||||
@@ -181,13 +180,26 @@ function usesManagedPath(value) {
|
|||||||
(path !== null && (path.indexOf('/var/') === 0 || path.indexOf('/var/run/') === 0));
|
(path !== null && (path.indexOf('/var/') === 0 || path.indexOf('/var/run/') === 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uploadStorageRoot(path) {
|
||||||
|
if (path === DEFAULT_UPLOAD_DIR)
|
||||||
|
return '/tmp';
|
||||||
|
|
||||||
|
const segments = path === null ? [] : path.split('/');
|
||||||
|
|
||||||
|
if (segments.length === 5 && segments[1] === 'mnt' && segments[2] &&
|
||||||
|
segments[3] === 'gecoosac' && segments[4] === 'upload')
|
||||||
|
return '/mnt/' + segments[2];
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function validUploadDir(value, policy) {
|
function validUploadDir(value, policy) {
|
||||||
const path = normalizePath(value);
|
const path = normalizePath(value);
|
||||||
const physical = managedPath(value, policy);
|
const physical = managedPath(value, policy);
|
||||||
|
const storageRoot = uploadStorageRoot(path);
|
||||||
|
const physicalStorageRoot = uploadStorageRoot(physical);
|
||||||
|
|
||||||
return path !== null && physical !== null && path.endsWith('/gecoosac/upload') &&
|
return !usesClearStagePath(value) && storageRoot !== null && storageRoot === physicalStorageRoot;
|
||||||
physical.endsWith('/gecoosac/upload') && !pathInDir(path, CONFIG_BACKUP_DIR) &&
|
|
||||||
!pathInDir(physical, CONFIG_BACKUP_DIR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validPathPrefix(value, prefixes) {
|
function validPathPrefix(value, prefixes) {
|
||||||
@@ -451,7 +463,7 @@ return view.extend({
|
|||||||
httpsOption.onchange = revalidateProtocolOptions;
|
httpsOption.onchange = revalidateProtocolOptions;
|
||||||
|
|
||||||
o = s.option(form.Value, 'upload_dir', _('Upload dir path'),
|
o = s.option(form.Value, 'upload_dir', _('Upload dir path'),
|
||||||
_('Upload AP upgrade firmware here. Use an absolute path ending with /gecoosac/upload, for example /tmp/gecoosac/upload.<br />Do not place it under /etc/gecoosac because that directory is backed up during sysupgrade.'));
|
_('Upload AP upgrade firmware here. Use /tmp/gecoosac/upload or /mnt/storage-name/gecoosac/upload. The /mnt/storage-name directory must already exist and be root-owned and private.'));
|
||||||
uploadDirOption = o;
|
uploadDirOption = o;
|
||||||
o.placeholder = DEFAULT_UPLOAD_DIR;
|
o.placeholder = DEFAULT_UPLOAD_DIR;
|
||||||
o.default = DEFAULT_UPLOAD_DIR;
|
o.default = DEFAULT_UPLOAD_DIR;
|
||||||
@@ -465,7 +477,7 @@ return view.extend({
|
|||||||
|
|
||||||
return validUploadDir(value, pathPolicy)
|
return validUploadDir(value, pathPolicy)
|
||||||
? true
|
? true
|
||||||
: _('Upload directory must be an absolute path ending with /gecoosac/upload and must not be under /etc/gecoosac.');
|
: _('Upload directory must be /tmp/gecoosac/upload or /mnt/storage-name/gecoosac/upload.');
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.option(form.Value, 'db_dir', _('Database dir path'),
|
o = s.option(form.Value, 'db_dir', _('Database dir path'),
|
||||||
|
|||||||
@@ -55,17 +55,15 @@ msgid "Upload dir path"
|
|||||||
msgstr "上传目录"
|
msgstr "上传目录"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Upload AP upgrade firmware here. Use an absolute path ending with "
|
"Upload AP upgrade firmware here. Use /tmp/gecoosac/upload or /mnt/storage-"
|
||||||
"/gecoosac/upload, for example /tmp/gecoosac/upload.<br />Do not place it "
|
"name/gecoosac/upload. The /mnt/storage-name directory must already exist "
|
||||||
"under /etc/gecoosac because that directory is backed up "
|
"and be root-owned and private."
|
||||||
"during sysupgrade."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"AP 升级固件会上传到此目录。请使用以 /gecoosac/upload 结尾的绝对路径,例如 "
|
"AP 升级固件会上传到此目录。请使用 /tmp/gecoosac/upload 或 /mnt/storage-"
|
||||||
"/tmp/gecoosac/upload。<br />不要放在 /etc/gecoosac 下,因为该目录会在 "
|
"name/gecoosac/upload。/mnt/storage-name 目录必须已存在、归 root 所有且权限安全。"
|
||||||
"sysupgrade 时被备份。"
|
|
||||||
|
|
||||||
msgid "Upload directory must be an absolute path ending with /gecoosac/upload and must not be under /etc/gecoosac."
|
msgid "Upload directory must be /tmp/gecoosac/upload or /mnt/storage-name/gecoosac/upload."
|
||||||
msgstr "上传目录必须是以 /gecoosac/upload 结尾的绝对路径,且不能位于 /etc/gecoosac 下。"
|
msgstr "上传目录必须是 /tmp/gecoosac/upload 或 /mnt/storage-name/gecoosac/upload。"
|
||||||
|
|
||||||
msgid "Database dir path"
|
msgid "Database dir path"
|
||||||
msgstr "数据库目录"
|
msgstr "数据库目录"
|
||||||
|
|||||||
@@ -55,17 +55,15 @@ msgid "Upload dir path"
|
|||||||
msgstr "上傳目錄"
|
msgstr "上傳目錄"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Upload AP upgrade firmware here. Use an absolute path ending with "
|
"Upload AP upgrade firmware here. Use /tmp/gecoosac/upload or /mnt/storage-"
|
||||||
"/gecoosac/upload, for example /tmp/gecoosac/upload.<br />Do not place it "
|
"name/gecoosac/upload. The /mnt/storage-name directory must already exist "
|
||||||
"under /etc/gecoosac because that directory is backed up "
|
"and be root-owned and private."
|
||||||
"during sysupgrade."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"AP 升級韌體會上傳到此目錄。請使用以 /gecoosac/upload 結尾的絕對路徑,例如 "
|
"AP 升級韌體會上傳到此目錄。請使用 /tmp/gecoosac/upload 或 /mnt/storage-"
|
||||||
"/tmp/gecoosac/upload。<br />不要放在 /etc/gecoosac 下,因為該目錄會在 "
|
"name/gecoosac/upload。/mnt/storage-name 目錄必須已存在、由 root 擁有且權限安全。"
|
||||||
"sysupgrade 時被備份。"
|
|
||||||
|
|
||||||
msgid "Upload directory must be an absolute path ending with /gecoosac/upload and must not be under /etc/gecoosac."
|
msgid "Upload directory must be /tmp/gecoosac/upload or /mnt/storage-name/gecoosac/upload."
|
||||||
msgstr "上傳目錄必須是以 /gecoosac/upload 結尾的絕對路徑,且不能位於 /etc/gecoosac 下。"
|
msgstr "上傳目錄必須是 /tmp/gecoosac/upload 或 /mnt/storage-name/gecoosac/upload。"
|
||||||
|
|
||||||
msgid "Database dir path"
|
msgid "Database dir path"
|
||||||
msgstr "資料庫目錄"
|
msgstr "資料庫目錄"
|
||||||
|
|||||||
@@ -58,6 +58,55 @@ normalize_path() {
|
|||||||
printf '%s\n' "$normalized"
|
printf '%s\n' "$normalized"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path_has_clear_stage_component() {
|
||||||
|
local path="$1" part rest
|
||||||
|
|
||||||
|
path="$(normalize_path "$path")" || return 1
|
||||||
|
rest="${path#/}"
|
||||||
|
while [ -n "$rest" ]; do
|
||||||
|
part="${rest%%/*}"
|
||||||
|
if [ "$part" = "$rest" ]; then
|
||||||
|
rest=""
|
||||||
|
else
|
||||||
|
rest="${rest#*/}"
|
||||||
|
fi
|
||||||
|
case "$part" in
|
||||||
|
.gecoosac-clear.*) return 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
path_uses_clear_stage() {
|
||||||
|
local path="$1" resolved
|
||||||
|
|
||||||
|
path_has_clear_stage_component "$path" && return 0
|
||||||
|
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||||
|
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||||
|
path_has_clear_stage_component "$resolved" && return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_supported_upload_path() {
|
||||||
|
local path="$1" storage
|
||||||
|
|
||||||
|
path="$(normalize_path "$path")" || return 1
|
||||||
|
path_uses_clear_stage "$path" && return 1
|
||||||
|
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||||
|
|
||||||
|
case "$path" in
|
||||||
|
/mnt/*/gecoosac/upload)
|
||||||
|
storage="${path#/mnt/}"
|
||||||
|
storage="${storage%/gecoosac/upload}"
|
||||||
|
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||||
|
;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
managed_dir_path() {
|
managed_dir_path() {
|
||||||
local role="$1" path anchor
|
local role="$1" path anchor
|
||||||
|
|
||||||
@@ -106,16 +155,7 @@ safe_upload_path() {
|
|||||||
local path
|
local path
|
||||||
|
|
||||||
path="$(normalize_path "$1")" || return 1
|
path="$(normalize_path "$1")" || return 1
|
||||||
case "$path" in
|
is_supported_upload_path "$path"
|
||||||
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
|
|
||||||
esac
|
|
||||||
[ "$path" = "/tmp/gecoosac/upload" ] && return 0
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
*/gecoosac/upload) return 0 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
path_in_dir() {
|
path_in_dir() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall
|
PKG_NAME:=luci-app-passwall
|
||||||
PKG_VERSION:=26.8.1
|
PKG_VERSION:=26.8.1
|
||||||
PKG_RELEASE:=210
|
PKG_RELEASE:=211
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -103,30 +103,7 @@ config nodes 'myshunt'
|
|||||||
config shunt_rules 'DirectGame'
|
config shunt_rules 'DirectGame'
|
||||||
option remarks 'DirectGame'
|
option remarks 'DirectGame'
|
||||||
option network 'tcp,udp'
|
option network 'tcp,udp'
|
||||||
option domain_list '# steam直连域名获取国内CDN走国内线路下载
|
option domain_list '# 国内游戏geosite域名
|
||||||
cm.steampowered.com
|
|
||||||
steamserver.net
|
|
||||||
|
|
||||||
# steam国内CDN华为云
|
|
||||||
steampipe.steamcontent.tnkjmec.com
|
|
||||||
# steam国内CDN白山云
|
|
||||||
st.dl.eccdnx.com
|
|
||||||
st.dl.bscstorage.net
|
|
||||||
st.dl.pinyuncloud.com
|
|
||||||
# steam国内CDN新流云(原金山云)(支持ipv6)
|
|
||||||
dl.steam.clngaa.com
|
|
||||||
# steam国内CDN网宿
|
|
||||||
cdn.mileweb.cs.steampowered.com.8686c.com
|
|
||||||
cdn-ws.content.steamchina.com
|
|
||||||
# steam国内CDN腾讯云 (蒸汽中国独占)
|
|
||||||
cdn-qc.content.steamchina.com
|
|
||||||
# steam国内CDN阿里云(支持ipv6)
|
|
||||||
cdn-ali.content.steamchina.com
|
|
||||||
xz.pphimalayanrt.com
|
|
||||||
lv.queniujq.cn
|
|
||||||
alibaba.cdn.steampipe.steamcontent.com
|
|
||||||
|
|
||||||
# 国内游戏geosite域名
|
|
||||||
geosite:category-games@cn'
|
geosite:category-games@cn'
|
||||||
option ip_list '# steam直连IP
|
option ip_list '# steam直连IP
|
||||||
45.121.184.0/24
|
45.121.184.0/24
|
||||||
@@ -196,6 +173,9 @@ steamcloud-sydney.storage.googleapis.com
|
|||||||
steamcloud-taiwan.storage.googleapis.com
|
steamcloud-taiwan.storage.googleapis.com
|
||||||
steamcloud-eu.storage.googleapis.com
|
steamcloud-eu.storage.googleapis.com
|
||||||
domain:blizzard.com
|
domain:blizzard.com
|
||||||
|
#Steam支付
|
||||||
|
domain:checkout.steampowered.com
|
||||||
|
domain:globalapi.smart2pay.com
|
||||||
geosite:category-games'
|
geosite:category-games'
|
||||||
|
|
||||||
config shunt_rules 'AIGC'
|
config shunt_rules 'AIGC'
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
|
|||||||
FOOTSTRAP_VERSION?=
|
FOOTSTRAP_VERSION?=
|
||||||
ifneq ($(FOOTSTRAP_VERSION),)
|
ifneq ($(FOOTSTRAP_VERSION),)
|
||||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||||
PKG_RELEASE:=8
|
PKG_RELEASE:=9
|
||||||
endif
|
endif
|
||||||
|
|
||||||
LUCI_TITLE:=Footstrap Theme
|
LUCI_TITLE:=Footstrap Theme
|
||||||
|
|||||||
@@ -281,9 +281,36 @@
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
/* overflow-wrap dropped — base/40-tables.css breaks every cell; the copy that stood here
|
/* overflow-wrap dropped — base/40-tables.css breaks every cell; the copy that stood here
|
||||||
* moved nothing against 267 matching cells. */
|
* moved nothing against 267 matching cells. */
|
||||||
padding-inline-end: min(200px, 50%);
|
padding-inline-end: min(230px, 50%);
|
||||||
font-size: var(--fs-type); font-weight: var(--fs-weight); color: var(--fs-text);
|
font-size: var(--fs-type); font-weight: var(--fs-weight); color: var(--fs-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ON A NARROW CARD THE VALUE TAKES ITS OWN LINE — a reserve cannot be made to fit there.
|
||||||
|
*
|
||||||
|
* The value is out of flow (the bar's ::after, pinned over its far edge), so the only thing
|
||||||
|
* keeping it off the label is that padding, and the two are sized by different things: the
|
||||||
|
* padding is a fraction of the CARD, the value is however wide LuCI's formatter made it.
|
||||||
|
* `16.66 GiB / 16.66 GiB (100%)` measures 218px in the mono face at --fs-type-xs, and 50% of a
|
||||||
|
* 380px phone card is 190 — so the figure printed straight through the end of the label:
|
||||||
|
* "Дисковое пространство330.54 MiB / 7.98 GiB (4%)", reported from a phone. Measured across the
|
||||||
|
* range: the reserve is at its 230px cap and clear of every value from a ~460px card up, and
|
||||||
|
* short of the longest ones below that.
|
||||||
|
*
|
||||||
|
* Below the threshold the label keeps the whole line and the bar drops by one --fs-type-xs
|
||||||
|
* line, which is the band the value then sits in — right-aligned, on its own line, above its
|
||||||
|
* own bar. It costs one line per row on a phone and cannot collide at any label length.
|
||||||
|
* `fs-view` rather than a container of its own: the Memory/Storage cards are already single
|
||||||
|
* column at 800px (the grid collapse above), so below 560px the card IS the view minus its
|
||||||
|
* gutters, and adding `container-type` to the card would size-contain a box the port tiles
|
||||||
|
* deliberately keep uncontained. */
|
||||||
|
@container fs-view (max-width: 560px) {
|
||||||
|
body[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td:first-child {
|
||||||
|
padding-inline-end: 0;
|
||||||
|
}
|
||||||
|
body[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .cbi-progressbar {
|
||||||
|
margin-block-start: calc(var(--fs-type-xs) * var(--fs-leading));
|
||||||
|
}
|
||||||
|
}
|
||||||
/* bar cell — spans full width on row 2 */
|
/* bar cell — spans full width on row 2 */
|
||||||
body[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td:nth-child(2) {
|
body[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td:nth-child(2) {
|
||||||
grid-column: 1 / -1; grid-row: 2;
|
grid-column: 1 / -1; grid-row: 2;
|
||||||
|
|||||||
@@ -1,4 +1,65 @@
|
|||||||
@layer page {
|
@layer page {
|
||||||
|
/* THE FILTER BAR, which LuCI writes as a run of inline text.
|
||||||
|
*
|
||||||
|
* `tools/views.js` (LogreadBox, shared by System Log, Kernel Log and every third-party page
|
||||||
|
* built on it) emits two bare `<div style="margin-bottom:10px">`, each holding label / control
|
||||||
|
* pairs with nothing around a pair: `<label>Не</label><input type=checkbox><label>объект:</label>
|
||||||
|
* <select>…`. Inline flow then breaks wherever the line runs out, and on a phone that is between
|
||||||
|
* a label and the control it names — reported from a 390px screen, where the second `Не` sat
|
||||||
|
* alone at the end of one line, `уровень:` opened the next one without it, and `Макс. строк:`
|
||||||
|
* broke INSIDE the label, leaving `Макс.` on one line and `строк:` on the next with the field it
|
||||||
|
* belongs to.
|
||||||
|
*
|
||||||
|
* Flex with wrapping fixes both halves: a wrap can now only happen BETWEEN items, and a label is
|
||||||
|
* one unbreakable item, so a pair still separates at worst onto two lines instead of a label
|
||||||
|
* cutting in half. The fields shrink instead of forcing that: `min-width: 0` undoes the field
|
||||||
|
* box in theme/60-inputs.css for this row only.
|
||||||
|
*
|
||||||
|
* Keyed on the controls' OWN ids rather than on the page: `data-page` differs between the system
|
||||||
|
* and the kernel log, and a third-party LogreadBox has neither, while ui.js gives these two rows
|
||||||
|
* the same ids wherever the widget is rendered. The inline `margin: 0 5px` LuCI writes on the
|
||||||
|
* labels stays as the horizontal spacing — only the ROW gap is ours, so nothing here has to
|
||||||
|
* fight an inline style. */
|
||||||
|
div:has(> #logFacilitySelect),
|
||||||
|
div:has(> #logTextFilter) {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
row-gap: var(--fs-space-2);
|
||||||
|
}
|
||||||
|
div:has(> #logFacilitySelect) > label,
|
||||||
|
div:has(> #logTextFilter) > label { white-space: nowrap; }
|
||||||
|
#logFacilitySelect, #logSeveritySelect, #logTextFilter, #logMaxRows {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
/* both halves are needed and they answer different layouts: `min-width: 0` lets a FLEX item
|
||||||
|
* shrink past its content, `max-width: 100%` clamps a GRID item to its track. A <select>
|
||||||
|
* carries the theme's own 210px field width, which a `minmax(0, 1fr)` track does not
|
||||||
|
* override — measured at 360px, where both selects hung 3px past the view. */
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
/* the two numeric-ish fields keep a sane ceiling: a max-rows box as wide as the filter reads as
|
||||||
|
* a second search field. */
|
||||||
|
#logMaxRows { flex-basis: 6ch; max-width: 12ch; }
|
||||||
|
|
||||||
|
/* ON A PHONE, ONE FILTER PER LINE. Wrapping alone still fills each line greedily, so the second
|
||||||
|
* `Не` rode the end of the first line while the `уровень:` it inverts opened the next one. The
|
||||||
|
* first row is a fixed alternation of exactly two filters — invert label, checkbox, name label,
|
||||||
|
* control, twice — so a four-column grid puts one filter on each line.
|
||||||
|
*
|
||||||
|
* The SECOND row is deliberately left to the wrapping above. It holds two filters of different
|
||||||
|
* shapes (a checkbox filter and a bare `Макс. строк:` pair), so the same four tracks would be
|
||||||
|
* sized by items that are not each other's counterparts: measured, the max-rows box defined
|
||||||
|
* column 2 and pushed `содержит:` and its field off the card. Wrapping puts that row's two
|
||||||
|
* filters on two lines by itself, which is all the grid was for. */
|
||||||
|
@container fs-view (max-width: 560px) {
|
||||||
|
div:has(> #logFacilitySelect) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto auto minmax(0, 1fr);
|
||||||
|
column-gap: var(--fs-space-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* System Log / Kernel Log (luci-mod-status): the whole log is a readonly
|
/* System Log / Kernel Log (luci-mod-status): the whole log is a readonly
|
||||||
* <textarea id="syslog"> in a bare div, NOT in a .cbi-value-field. Stock bootstrap pairs
|
* <textarea id="syslog"> in a bare div, NOT in a .cbi-value-field. Stock bootstrap pairs
|
||||||
* its generic `input, textarea { width: 210px }` with `#syslog { width: 100% }` and wins
|
* its generic `input, textarea { width: 210px }` with `#syslog { width: 100% }` and wins
|
||||||
|
|||||||
@@ -26,6 +26,33 @@
|
|||||||
color: var(--fs-dim); white-space: nowrap; text-shadow: none;
|
color: var(--fs-dim); white-space: nowrap; text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* THE BAR IS THE FIELD OF A LABELLED ROW — put the value BESIDE it, not above it.
|
||||||
|
*
|
||||||
|
* The placement above the bar (and the stock one it replaced: bootstrap floats the same title
|
||||||
|
* at `top: -1.4em; left: 0`) assumes the shape the Overview has, where the bar owns a full-width
|
||||||
|
* line and its name sits on the line above — there the value lands at the far end of the name's
|
||||||
|
* own line and the two read as one row. A `.cbi-value` row is the other shape: the name is
|
||||||
|
* already in the LEFT column, on the bar's own line, so a value floating above the bar has
|
||||||
|
* nothing to pair with. It lands against the row divider instead, nearer the row above than the
|
||||||
|
* bar it belongs to — reported against luci-app-modeminfo's Antennas page, three of these rows
|
||||||
|
* per card and every dBm figure reading as though it belonged to the meter above it.
|
||||||
|
*
|
||||||
|
* Inline, then: the row becomes `name | meter | value`, one line, values aligned to a single
|
||||||
|
* column because the reserve is fixed rather than shrink-wrapped. The reserve is
|
||||||
|
* `2 x --fs-space-10` because the value can be `-140.0 dBm`, half again as wide as the
|
||||||
|
* `100% (1.2 GiB)` a single step covers in the data-table rule below.
|
||||||
|
*
|
||||||
|
* Not the same selector as that rule and deliberately so: this is the CBI FORM row (an app
|
||||||
|
* building a `.cbi-value` with ui.js), that one is a multi-column data table. Both end up
|
||||||
|
* inline-right; neither can be expressed as the other, because CSS cannot ask "does this row
|
||||||
|
* carry its own label". The Overview and the package manager keep the value above the bar:
|
||||||
|
* their meters sit in a `.td` and a bare `.controls` div, neither of which is a field. */
|
||||||
|
.cbi-value-field > .cbi-progressbar { margin-inline-end: calc(var(--fs-space-10) * 2); }
|
||||||
|
.cbi-value-field > .cbi-progressbar::after {
|
||||||
|
inset-inline: calc(100% + var(--fs-space-2)) auto; bottom: auto; top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
/* Progressbar rows in a section table (luci-app-cpu-status): keep the native label | bar
|
/* Progressbar rows in a section table (luci-app-cpu-status): keep the native label | bar
|
||||||
* row, just centre the cells vertically. */
|
* row, just centre the cells vertically. */
|
||||||
.cbi-section .table:not([id]):not(.cbi-section-table) .tr:has(.cbi-progressbar) .td { vertical-align: middle; }
|
.cbi-section .table:not([id]):not(.cbi-section-table) .tr:has(.cbi-progressbar) .td { vertical-align: middle; }
|
||||||
|
|||||||
@@ -249,6 +249,23 @@
|
|||||||
* the thing the scale exists to stop. Absorbed with the machinery above. */
|
* the thing the scale exists to stop. Absorbed with the machinery above. */
|
||||||
min-height: var(--fs-space-5);
|
min-height: var(--fs-space-5);
|
||||||
}
|
}
|
||||||
|
/* …and the row's CONTENTS, which is where that font was being lost. A MULTI-select wraps every
|
||||||
|
* item in `<form><label><input type=checkbox> text</label></form>` (ui.js builds the checkbox
|
||||||
|
* that way), and `.cbi-value-field *` in theme/60-inputs.css hands mono to every descendant of a
|
||||||
|
* field — it matches that form and that label DIRECTLY, while the rule above only reaches the
|
||||||
|
* `<li>`, whose font the label then overrides rather than inherits. So the same widget drew two
|
||||||
|
* fonts depending on one option: a single-select list, whose text sits in the `<li>` itself,
|
||||||
|
* came out sans; every multi-select list came out mono, in the open menu AND in the closed
|
||||||
|
* control that lists what is selected. Measured on the router with ui.Dropdown: `<li>` Manrope,
|
||||||
|
* its `<form>`/`<label>` JetBrains Mono.
|
||||||
|
*
|
||||||
|
* It is a LABEL, not a value — the same distinction `<strong>` gets in theme/45-misc.css — and
|
||||||
|
* mono spacing is what makes a wordy option list look stretched (`3G Band 1`, `LTE B1`, reported
|
||||||
|
* against luci-app-modeminfo's Bands page). `inherit`, not the token again: the row above is the
|
||||||
|
* one place the menu's font is chosen. The filter/create `<input>` inside the list is left
|
||||||
|
* alone: what the user TYPES there is a value. */
|
||||||
|
.cbi-dropdown > ul > li > form,
|
||||||
|
.cbi-dropdown > ul > li label { font-family: inherit; }
|
||||||
.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[display]:not([display="0"]) { border-inline-start: 0; }
|
.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[display]:not([display="0"]) { border-inline-start: 0; }
|
||||||
.cbi-dropdown > ul > li[placeholder] { color: var(--fs-faint); }
|
.cbi-dropdown > ul > li[placeholder] { color: var(--fs-faint); }
|
||||||
/* the multiselect's inline filter/create inputs — moved from base with the generic `input`
|
/* the multiselect's inline filter/create inputs — moved from base with the generic `input`
|
||||||
@@ -348,6 +365,47 @@
|
|||||||
background: var(--fs-panel2); color: var(--fs-accent); box-shadow: inset 2px 0 0 var(--fs-accent);
|
background: var(--fs-panel2); color: var(--fs-accent); box-shadow: inset 2px 0 0 var(--fs-accent);
|
||||||
/* @endmirror */
|
/* @endmirror */
|
||||||
}
|
}
|
||||||
|
/* THE FOCUSED ROW WEARS THE THEME'S RING, not the browser's.
|
||||||
|
*
|
||||||
|
* ui.js focuses a row as it opens the menu (the first selected one, or the first row), and the
|
||||||
|
* UA answers a programmatic focus with `outline: auto` — Chrome's 1px system-blue ring, drawn
|
||||||
|
* OUTSIDE the row and following its 8px radius, so opening a dropdown that already has a value
|
||||||
|
* flashed a loud blue capsule around the top row on every single-select and multi-select on the
|
||||||
|
* router. Every other control in this theme wears --fs-focus-ring; this row wore whatever the
|
||||||
|
* browser shipped.
|
||||||
|
*
|
||||||
|
* Inset rather than the token's outer 3px: the menu clips its own overflow, so an outer ring is
|
||||||
|
* cut on the first and last rows. The rail is restated INSIDE this ring so a focused row that is
|
||||||
|
* also selected keeps its rail — a shorthand cannot merge with the one below.
|
||||||
|
*
|
||||||
|
* Both selectors carry `[selected]`-level weight on purpose: the selected-row paint below is
|
||||||
|
* (0,4,1) and a bare `li:focus-visible` ties with it, which would leave the focus ring to source
|
||||||
|
* order. `docs/conventions.md`: win on specificity, never on order. */
|
||||||
|
.cbi-dropdown[open] > ul.dropdown > li[selected]:focus-visible,
|
||||||
|
.cbi-dropdown[open] > ul.dropdown > li:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 2px 0 0 var(--fs-accent), inset 0 0 0 2px var(--fs-accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A MULTI-select row says "chosen" with its CHECKBOX, so it does not also need the rail.
|
||||||
|
*
|
||||||
|
* The rail is `inset 2px 0 0` on a row with an 8px radius, so it is drawn as an ARC, not a bar —
|
||||||
|
* fine as the single-select cue it was designed for, one row at a time, and wrong down a column
|
||||||
|
* of them: three chosen bands rendered three blue crescents stacked against the menu's edge and
|
||||||
|
* read as a broken frame (reported against luci-app-modeminfo's Bands page). The checkbox is a
|
||||||
|
* non-colour cue in its own right, which is what WCAG 2.2 SC 1.4.1 asks of "selected" — a
|
||||||
|
* single-select row has none, and keeps the rail.
|
||||||
|
*
|
||||||
|
* Written as an override rather than by narrowing the paint below, because those three
|
||||||
|
* declarations are pinned byte-identical to `select option:checked` in theme/60-inputs.css
|
||||||
|
* (tools/mirror.mjs) — a native `<select multiple>` option has no checkbox, so the rail is still
|
||||||
|
* the only cue there and the two must not diverge. `:not(:focus-visible)` keeps this off the
|
||||||
|
* focused row, so the ring above owns that state alone instead of tying with it. */
|
||||||
|
.cbi-dropdown[multiple][open] > ul.dropdown > li[selected]:not(:focus-visible) { box-shadow: none; }
|
||||||
|
.cbi-dropdown[multiple][open] > ul.dropdown > li[selected]:focus-visible {
|
||||||
|
box-shadow: inset 0 0 0 2px var(--fs-accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
.cbi-dropdown[open] > ul.dropdown > li.create-item { border-top: 1px solid var(--fs-border); margin-top: var(--fs-space-1); }
|
.cbi-dropdown[open] > ul.dropdown > li.create-item { border-top: 1px solid var(--fs-border); margin-top: var(--fs-space-1); }
|
||||||
/* The dropdown's "add item" field is a text input and takes the same radius as every other
|
/* The dropdown's "add item" field is a text input and takes the same radius as every other
|
||||||
* one (theme/60-inputs.css). It said 8px here, and won or lost on source order. */
|
* one (theme/60-inputs.css). It said 8px here, and won or lost on source order. */
|
||||||
|
|||||||
Reference in new issue
Block a user