🦄 Sync 2026-06-17 03:41:10

This commit is contained in:
github-actions[bot] committed 2026-06-17 03:41:10 +08:00
1 parent aa76a8f79d
commit 08b7eb4ffb
79 files changed
+1354 -486

No files matched your search

+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede
PKG_VERSION:=1.12.0
PKG_RELEASE:=11
PKG_VERSION:=1.13.0
PKG_RELEASE:=12
PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
@@ -13,6 +13,22 @@ const DATA_PATHS = {
geosite: '/usr/share/v2ray/geosite.dat'
};
// Geo data source presets. `loyalsoldier` keeps both URLs empty so the actual
// default lives in one place — update-geo.sh — avoiding UI/script drift.
const GEO_PRESETS = {
loyalsoldier: { geoip: '', geosite: '' },
v2fly: {
geoip: 'https://github.com/v2fly/geoip/releases/latest/download/geoip.dat',
geosite: 'https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat'
}
};
function currentPreset(gi, gs) {
if (!gi && !gs) return 'loyalsoldier';
if (gi === GEO_PRESETS.v2fly.geoip && gs === GEO_PRESETS.v2fly.geosite) return 'v2fly';
return 'custom';
}
const HEALTH_PATHS = {
btf: '/sys/kernel/btf/vmlinux',
netns: '/run/netns/daens'
@@ -36,6 +52,11 @@ const CSS = [
'.dd-up-btn:hover{background:rgba(128,128,128,.12)}',
'.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}',
'.dd-up-btn-primary{border-color:#4aa065;color:#4aa065}',
'.dd-geo-row{display:grid;grid-template-columns:96px 1fr;gap:10px;align-items:center;font-size:12px;padding:6px 0}',
'.dd-geo-row label{opacity:.75;font-weight:600}',
'.dd-geo-row input[type=text],.dd-geo-row select{font-size:12px;padding:4px 8px;border:1px solid rgba(128,128,128,.35);border-radius:5px;background:transparent;color:inherit;width:100%}',
'.dd-geo-chk{display:flex;align-items:center;gap:6px}',
'.dd-geo-actions{margin-top:10px;display:flex;gap:10px;align-items:center}',
'.dd-up-log{margin-top:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;padding:10px;border:1px solid rgba(128,128,128,.14);border-radius:8px;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-all;display:none;background:inherit;color:#4a8c63}',
'.dd-up-log.show{display:block}',
'body.dark .dd-up-log,html[data-theme="dark"] .dd-up-log,html[data-bs-theme="dark"] .dd-up-log{color:#a3d9ad}',
@@ -102,13 +123,20 @@ function probePkg(pkg) {
});
}
// YYYYMMDD-HHMM stamp for backup filenames
function stamp() {
const d = new Date(), p = n => (n < 10 ? '0' : '') + n;
return '' + d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '-' + p(d.getHours()) + p(d.getMinutes());
}
return view.extend({
load: function() {
// background apk update so new versions show on next poll
fs.exec('/usr/share/luci-app-daede/refresh-index.sh', []).catch(function() {});
return Promise.all([
backend.detectBackend(),
uci.load('daed').catch(function() {})
uci.load('daed').catch(function() {}),
uci.load('daede').catch(function() {})
]).then(function(r) {
return r[0];
});
@@ -172,6 +200,49 @@ return view.extend({
return runJob('update-pkg.sh', pkg, btn, '/tmp/luci-app-daede.pkg-' + pkg + '.log');
};
// === Config backup (export / import the whole daede config) ===
const exportBtn = E('button', { 'class': 'dd-up-btn' }, _('Export'));
const importBtn = E('button', { 'class': 'dd-up-btn' }, _('Import'));
const fileInput = E('input', { 'type': 'file', 'accept': '.tar.gz,.gz,application/gzip', 'style': 'display:none' });
exportBtn.addEventListener('click', function() {
const orig = exportBtn.textContent;
exportBtn.disabled = true; exportBtn.textContent = '...';
fs.exec('/usr/share/luci-app-daede/config-backup.sh', ['export']).then(function(res) {
if (res.code !== 0 || !res.stdout) {
logPane.textContent = String(res.stderr || res.stdout || 'export failed').trim();
logPane.classList.add('show');
return;
}
const bin = atob(res.stdout.trim());
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
const url = URL.createObjectURL(new Blob([arr], { 'type': 'application/gzip' }));
const a = E('a', { 'href': url, 'download': 'daede-config-' + stamp() + '.tar.gz' });
document.body.appendChild(a); a.click(); document.body.removeChild(a);
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
}).catch(function() {}).finally(function() {
exportBtn.disabled = false; exportBtn.textContent = orig;
});
});
importBtn.addEventListener('click', function() { fileInput.click(); });
fileInput.addEventListener('change', function(ev) {
const file = ev.target.files && ev.target.files[0];
if (!file) return;
if (!confirm(_('Import overwrites the current daede config and restarts the backend. Continue?'))) {
fileInput.value = ''; return;
}
const reader = new FileReader();
reader.onload = function(e) {
const b64 = String(e.target.result).split(',')[1] || '';
fs.write('/tmp/daede-import.b64', b64).then(function() {
return runJob('config-backup.sh', 'import', importBtn, '/tmp/luci-app-daede.backup.log');
}).catch(function() {}).finally(function() { fileInput.value = ''; });
};
reader.readAsDataURL(file);
});
const refresh = function() {
const probes = [
probeFile(DATA_PATHS.geoip),
@@ -309,16 +380,113 @@ return view.extend({
poll.add(refresh);
refresh();
// === Geo data source (preset / custom URL + auto-update) ===
const geoSettings = (function() {
const gi0 = uci.get('daede', 'config', 'geoip_url') || '';
const gs0 = uci.get('daede', 'config', 'geosite_url') || '';
const auto0 = uci.get('daede', 'config', 'geo_auto') === '1';
const freq0 = uci.get('daede', 'config', 'geo_auto_freq') || 'daily';
const preset0 = currentPreset(gi0, gs0);
const presetSel = E('select', {}, [
E('option', { 'value': 'loyalsoldier' }, 'Loyalsoldier'),
E('option', { 'value': 'v2fly' }, 'v2fly'),
E('option', { 'value': 'custom' }, _('Custom'))
]);
presetSel.value = preset0;
const giInput = E('input', { 'type': 'text', 'placeholder': 'https://…/geoip.dat' });
const gsInput = E('input', { 'type': 'text', 'placeholder': 'https://…/geosite.dat' });
giInput.value = preset0 === 'custom' ? gi0 : '';
gsInput.value = preset0 === 'custom' ? gs0 : '';
const customRows = E('div', {}, [
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, 'GeoIP URL'), giInput ]),
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, 'GeoSite URL'), gsInput ])
]);
const syncCustom = function() {
customRows.style.display = presetSel.value === 'custom' ? '' : 'none';
};
presetSel.addEventListener('change', syncCustom);
syncCustom();
const autoCb = E('input', { 'type': 'checkbox' });
autoCb.checked = auto0;
const freqSel = E('select', {}, [
E('option', { 'value': 'daily' }, _('Daily')),
E('option', { 'value': 'weekly' }, _('Weekly'))
]);
freqSel.value = freq0;
const saveBtn = E('button', { 'class': 'dd-up-btn dd-up-btn-primary' }, _('Save'));
saveBtn.addEventListener('click', function() {
const p = presetSel.value;
let gi = '', gs = '';
if (p === 'v2fly') { gi = GEO_PRESETS.v2fly.geoip; gs = GEO_PRESETS.v2fly.geosite; }
else if (p === 'custom') { gi = giInput.value.trim(); gs = gsInput.value.trim(); }
uci.set('daede', 'config', 'geoip_url', gi);
uci.set('daede', 'config', 'geosite_url', gs);
uci.set('daede', 'config', 'geo_auto', autoCb.checked ? '1' : '0');
uci.set('daede', 'config', 'geo_auto_freq', freqSel.value);
const orig = saveBtn.textContent;
saveBtn.disabled = true; saveBtn.textContent = '...';
uci.save().then(function() { return uci.apply(); }).then(function() {
return fs.exec('/usr/share/luci-app-daede/geo-cron.sh', [autoCb.checked ? 'enable' : 'disable']);
}).then(function() {
logPane.textContent = _('Geo data source saved.');
logPane.classList.add('show');
ui.changes.init();
}).catch(function(e) {
logPane.textContent = _('Save failed') + ': ' + (e && e.message ? e.message : e);
logPane.classList.add('show');
}).finally(function() {
saveBtn.disabled = false; saveBtn.textContent = orig;
});
});
const adv = E('div', { 'class': 'dd-adv dd-closed' }, [
E('div', { 'class': 'dd-adv-bar' }, [
E('span', {}, _('Data Source')),
E('span', { 'class': 'dd-adv-chevron' }, '›')
]),
E('div', { 'class': 'dd-adv-body' }, [
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, _('Source')), presetSel ]),
customRows,
E('div', { 'class': 'dd-geo-row' }, [
E('label', {}, _('Auto-update')),
E('div', { 'class': 'dd-geo-chk' }, [ autoCb, freqSel ])
]),
E('div', { 'class': 'dd-geo-actions' }, [ saveBtn ])
])
]);
adv.firstChild.addEventListener('click', function() {
adv.classList.toggle('dd-closed');
});
return adv;
})();
return E('div', { 'class': 'dd-up-wrap' }, [
E('style', {}, CSS),
E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Data Updates')),
dataBody
dataBody,
geoSettings
]),
E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Package Updates')),
pkgBody
]),
E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Config Backup')),
E('div', { 'class': 'dd-up-row' }, [
E('span', { 'class': 'dd-up-icon dd-up-new' }, '⤓'),
E('span', { 'class': 'dd-up-name' }, _('dae + daed')),
E('span', { 'class': 'dd-up-meta' }, _('Back up / restore the whole daede config (kernels excluded)')),
exportBtn,
importBtn
]),
fileInput
]),
(function() {
const adv = E('div', { 'class': 'dd-adv dd-closed' }, [
E('div', { 'class': 'dd-adv-bar' }, [
+53
View File
@@ -738,3 +738,56 @@ msgstr ""
msgid "Node group \"%s\" is available on %s. Select nodes above to update it."
msgstr ""
msgid "Config Backup"
msgstr ""
msgid "Export"
msgstr ""
msgid "Import"
msgstr ""
msgid "dae + daed"
msgstr ""
msgid "Back up / restore the whole daede config (kernels excluded)"
msgstr ""
msgid "Import overwrites the current daede config and restarts the backend. Continue?"
msgstr ""
msgid "Data Source"
msgstr ""
msgid "Custom"
msgstr ""
msgid "Auto-update"
msgstr ""
msgid "Daily"
msgstr ""
msgid "Weekly"
msgstr ""
msgid "Save"
msgstr ""
msgid "Geo data source saved."
msgstr ""
msgid "Save failed"
msgstr ""
msgid "latest version unknown"
msgstr ""
msgid "Delete the daens netns?"
msgstr ""
msgid ""
"%s is running. Deleting the daens netns now will break its networking until "
"you restart it. Continue?"
msgstr ""
+50
View File
@@ -946,3 +946,53 @@ msgstr "将所选节点作为一个命名节点组导入,避免机场节点较
msgid "Node group \"%s\" is available on %s. Select nodes above to update it."
msgstr "节点组“%s”已导入 %s。重新选择上方节点即可更新此组。"
msgid "Config Backup"
msgstr "配置备份"
msgid "Export"
msgstr "导出"
msgid "Import"
msgstr "导入"
msgid "Back up / restore the whole daede config (kernels excluded)"
msgstr "备份 / 还原整个 daede 配置(不含内核)"
msgid "Import overwrites the current daede config and restarts the backend. Continue?"
msgstr "导入会覆盖当前 daede 配置并重启后端,确定继续?"
msgid "Data Source"
msgstr "数据源"
msgid "Custom"
msgstr "自定义"
msgid "Auto-update"
msgstr "自动更新"
msgid "Daily"
msgstr "每天"
msgid "Weekly"
msgstr "每周"
msgid "Save"
msgstr "保存"
msgid "Geo data source saved."
msgstr "数据源已保存。"
msgid "Save failed"
msgstr "保存失败"
msgid "latest version unknown"
msgstr "最新版本未知"
msgid "Delete the daens netns?"
msgstr "删除 daens netns?"
msgid ""
"%s is running. Deleting the daens netns now will break its networking until "
"you restart it. Continue?"
msgstr "%s 正在运行。现在删除 daens netns 会中断其网络,直到重启它。确定继续?"
+50
View File
@@ -946,3 +946,53 @@ msgstr "将所选节点作为一个命名节点组导入,避免机场节点较
msgid "Node group \"%s\" is available on %s. Select nodes above to update it."
msgstr "节点组“%s”已导入 %s。重新选择上方节点即可更新此组。"
msgid "Config Backup"
msgstr "配置备份"
msgid "Export"
msgstr "导出"
msgid "Import"
msgstr "导入"
msgid "Back up / restore the whole daede config (kernels excluded)"
msgstr "备份 / 还原整个 daede 配置(不含内核)"
msgid "Import overwrites the current daede config and restarts the backend. Continue?"
msgstr "导入会覆盖当前 daede 配置并重启后端,确定继续?"
msgid "Data Source"
msgstr "数据源"
msgid "Custom"
msgstr "自定义"
msgid "Auto-update"
msgstr "自动更新"
msgid "Daily"
msgstr "每天"
msgid "Weekly"
msgstr "每周"
msgid "Save"
msgstr "保存"
msgid "Geo data source saved."
msgstr "数据源已保存。"
msgid "Save failed"
msgstr "保存失败"
msgid "latest version unknown"
msgstr "最新版本未知"
msgid "Delete the daens netns?"
msgstr "删除 daens netns?"
msgid ""
"%s is running. Deleting the daens netns now will break its networking until "
"you restart it. Continue?"
msgstr "%s 正在运行。现在删除 daens netns 会中断其网络,直到重启它。确定继续?"
@@ -0,0 +1,71 @@
#!/bin/sh
# config-backup.sh export|import — back up/restore the whole daede config
# (dae + daed + active backend). export: prints base64(tar.gz) to stdout.
# import: decodes /tmp/daede-import.b64, validates, restores, restarts backend.
set -eu
ACTION="${1:-}"
LOG="/tmp/luci-app-daede.backup.log"
IMPORT_B64="/tmp/daede-import.b64"
MAX_TAR=184320 # 180 KiB tar.gz -> ~240 KiB base64, under the ubus limit
# relative paths (no leading /) so tar entries match the extraction whitelist
WHITELIST="etc/config/dae etc/config/daed etc/config/daede etc/dae/config.dae etc/daed/wing.db"
b64enc() { ucode -e 'let f=require("fs"); print(b64enc(f.open(ARGV[0],"r").read("all")));' -- "$1"; }
b64dec() { ucode -e 'let f=require("fs"); let r=b64dec(trim(f.open(ARGV[0],"r").read("all"))); if(!r)exit(1); let o=f.open(ARGV[1],"w"); o.write(r); o.close();' -- "$1" "$2"; }
case "$ACTION" in
export)
exist=""
for p in $WHITELIST; do [ -e "/$p" ] && exist="$exist $p"; done
[ -n "$exist" ] || { echo "no config to back up" >&2; exit 1; }
tmp="$(mktemp)"
( cd / && tar -czf "$tmp" $exist ) 2>/dev/null || { rm -f "$tmp"; echo "tar failed" >&2; exit 1; }
if [ "$(wc -c < "$tmp")" -gt "$MAX_TAR" ]; then
rm -f "$tmp"; echo "config too large; use System Backup instead" >&2; exit 2
fi
b64enc "$tmp"
rm -f "$tmp"
;;
import)
[ -f "$IMPORT_B64" ] || { echo "no upload found" >&2; exit 1; }
(
exec >"$LOG" 2>&1
echo "$(date '+%F %T') begin import"
rc=0
tmp="$(mktemp)"
if ! b64dec "$IMPORT_B64" "$tmp"; then
echo "decode failed"; rc=1
elif ! gzip -t "$tmp" 2>/dev/null; then
echo "not a valid backup archive"; rc=1
else
# reject any entry outside the whitelist (path-traversal guard)
bad="$(tar -tzf "$tmp" 2>/dev/null | grep -vxE 'etc/config/dae|etc/config/daed|etc/config/daede|etc/dae/config\.dae|etc/daed/wing\.db' | head -1)"
if [ -n "$bad" ]; then
echo "rejected: unexpected entry '$bad'"; rc=1
else
ab="$(uci -q get daede.config.active_backend || echo dae)"
[ -x "/etc/init.d/$ab" ] && /etc/init.d/"$ab" stop 2>/dev/null || true
if ( cd / && tar -xzf "$tmp" ); then
echo "restored config"
else
echo "extract failed"; rc=1
fi
ab="$(uci -q get daede.config.active_backend || echo "$ab")"
[ -x "/etc/init.d/$ab" ] && { /etc/init.d/"$ab" enabled || /etc/init.d/"$ab" enable; /etc/init.d/"$ab" restart 2>/dev/null || /etc/init.d/"$ab" start 2>/dev/null; } || true
fi
fi
rm -f "$tmp" "$IMPORT_B64"
if [ "$rc" = 0 ]; then echo "result: config restored, backend restarted"; else echo "result: import failed"; fi
echo "$(date '+%F %T') done (rc=$rc)"
) </dev/null >/dev/null 2>&1 &
echo "started in background, see $LOG"
;;
*)
echo "usage: $0 export|import" >&2
exit 64
;;
esac
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
# geo-cron.sh enable|disable
# Manage the geo auto-update crontab entry. enable reads the schedule from
# daede.config.geo_auto_freq (daily|weekly); both actions are idempotent.
ACTION="$1"
CRONTAB="/etc/crontabs/root"
TAG="# luci-app-daede geo-update"
SCRIPT="/usr/share/luci-app-daede/update-geo.sh"
# Drop any previous daede geo entry (tag line + its command line).
clean() {
[ -f "$CRONTAB" ] || return 0
grep -vF -e "$TAG" -e "$SCRIPT" "$CRONTAB" > "$CRONTAB.tmp" 2>/dev/null
mv "$CRONTAB.tmp" "$CRONTAB"
}
clean
if [ "$ACTION" = "enable" ]; then
case "$(uci -q get daede.config.geo_auto_freq)" in
weekly) sched="17 4 * * 0" ;; # Sunday 04:17
*) sched="17 4 * * *" ;; # every day 04:17
esac
mkdir -p /etc/crontabs
{
echo "$TAG"
echo "$sched $SCRIPT geoip; $SCRIPT geosite"
} >> "$CRONTAB"
/etc/init.d/cron enable >/dev/null 2>&1
fi
/etc/init.d/cron restart >/dev/null 2>&1
exit 0
@@ -5,13 +5,18 @@
# in /tmp prevents concurrent runs.
TYPE="$1"
# Default source: Loyalsoldier/v2ray-rules-dat — daily-built, China-optimized,
# ships both geoip.dat and geosite.dat. Users can override per-type via
# daede.config.geoip_url / geosite_url (empty falls back to the default below).
case "$TYPE" in
geoip)
URL="https://github.com/v2fly/geoip/releases/latest/download/geoip.dat"
DEF_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
URL="$(uci -q get daede.config.geoip_url)"
DEST="/usr/share/v2ray/geoip.dat"
;;
geosite)
URL="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"
DEF_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
URL="$(uci -q get daede.config.geosite_url)"
DEST="/usr/share/v2ray/geosite.dat"
;;
*)
@@ -19,6 +24,7 @@ case "$TYPE" in
exit 64
;;
esac
[ -n "$URL" ] || URL="$DEF_URL"
LOCK="/tmp/luci-app-daede.${TYPE}.lock"
LOG="/tmp/luci-app-daede.${TYPE}.log"
@@ -19,6 +19,7 @@
"/tmp/luci-app-daede.pkg-dae.log": [ "read" ],
"/tmp/luci-app-daede.pkg-daed.log": [ "read" ],
"/tmp/luci-app-daede.pkg-luci-app-daede.log": [ "read" ],
"/tmp/luci-app-daede.backup.log": [ "read" ],
"/bin/pidof dae": [ "exec" ],
"/bin/pidof daed": [ "exec" ],
"/usr/bin/dae validate -c /tmp/dae-validate.dae": [ "exec" ],
@@ -28,7 +29,8 @@
"/usr/share/luci-app-daede/pkg-info.sh daed": [ "exec" ],
"/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "exec" ],
"/usr/share/luci-app-daede/refresh-index.sh": [ "exec" ],
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ]
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ],
"/usr/share/luci-app-daede/config-backup.sh export": [ "exec" ]
},
"ubus": {
"service": [ "list" ]
@@ -39,6 +41,8 @@
"file": {
"/etc/dae/config.dae": [ "write" ],
"/tmp/dae-validate.dae": [ "write" ],
"/tmp/daede-import.b64": [ "write" ],
"/usr/share/luci-app-daede/config-backup.sh import": [ "exec" ],
"/etc/config/daede": [ "write" ],
"/var/log/dae/dae.log": [ "write" ],
"/var/log/daed/daed.log": [ "write" ],
@@ -63,6 +67,8 @@
"/sbin/ip netns del daens": [ "exec" ],
"/usr/share/luci-app-daede/update-geo.sh geoip": [ "exec" ],
"/usr/share/luci-app-daede/update-geo.sh geosite": [ "exec" ],
"/usr/share/luci-app-daede/geo-cron.sh enable": [ "exec" ],
"/usr/share/luci-app-daede/geo-cron.sh disable": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh dae": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh daed": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh luci-app-daede": [ "exec" ],
@@ -893,17 +893,12 @@ function renderListeners(s, uciconfig, isClient) {
// @ 下面支持填写针对server-url的TLS配置(sni, skip-cert-verify, fingerprint, certificate, private-key, alpn)
/* TLS fields */
o = s.taboption('field_general', form.Flag, 'allow_insecure', _('Allow insecure connections'),
_('Only applicable when %s are used as a frontend.').format('nginx/caddy'));
o.default = o.disabled;
o.depends({type: /^(vless|trojan|anytls)$/});
o.modalonly = true;
o = s.taboption('field_general', form.Flag, 'tls', _('TLS'));
o.default = o.disabled;
o.validate = function(section_id, value) {
const type = this.section.getOption('type').formvalue(section_id);
let tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input');
let allow_insecure = this.section.getUIElement(section_id, 'allow_insecure').node.querySelector('input');
let tls_alpn = this.section.getUIElement(section_id, 'tls_alpn');
let tls_reality = this.section.getUIElement(section_id, 'tls_reality').node.querySelector('input');
@@ -935,6 +930,12 @@ function renderListeners(s, uciconfig, isClient) {
}
// Force disabled
if (!['vless', 'trojan', 'anytls'].includes(type)) {
allow_insecure.checked = false;
} else if (allow_insecure.checked) {
tls.checked = false;
tls.disabled = true;
}
if (['trusttunnel'].includes(type)) {
tls_alpn.node.querySelector('input').disabled = true;
tls_alpn.setValue('');
@@ -950,7 +951,13 @@ function renderListeners(s, uciconfig, isClient) {
return true;
}
o.depends({allow_insecure: '0', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|tuic|hysteria2|hysteria2-realm|trusttunnel)$/});
o.depends({type: /^(http|socks|mixed|vmess|vless|trojan|anytls|tuic|hysteria2|hysteria2-realm|trusttunnel)$/});
o.modalonly = true;
o = s.taboption('field_general', form.Flag, 'allow_insecure', _('Allow insecure connections'),
_('Only applicable when %s are used as a frontend.').format('nginx/caddy'));
o.default = o.disabled;
o.depends({type: /^(vless|trojan|anytls)$/});
o.modalonly = true;
o = s.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'),
@@ -211,7 +211,7 @@ return view.extend({
render(data) {
let m, s, o, ss, so;
m = new form.Map('fchomo', _('Edit node'));
m = new form.Map('fchomo', _('Edit outbound'));
s = m.section(form.NamedSection, 'global', 'fchomo');
@@ -1050,7 +1050,7 @@ return view.extend({
so = ss.taboption('field_tls', form.Value, 'tls_sni', _('TLS SNI'),
_('Used to verify the hostname on the returned certificates.'));
so.depends({tls: '1', type: /^(http|vmess|vless|trojan|anytls|hysteria|hysteria2|trusttunnel)$/});
so.depends({tls: '1', tls_disable_sni: '0', type: /^(tuic)$/});
so.depends({tls: '1', type: /^(tuic)$/, tls_disable_sni: '0'});
so.modalonly = true;
so = ss.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'),
+4
View File
@@ -805,6 +805,10 @@ msgstr ""
msgid "Edit node"
msgstr ""
#: htdocs/luci-static/resources/view/fchomo/node.js:214
msgid "Edit outbound"
msgstr ""
#: htdocs/luci-static/resources/view/fchomo/ruleset.js:138
msgid "Edit ruleset"
msgstr ""
+9 -5
View File
@@ -798,7 +798,7 @@ msgstr "强制覆盖 ECS"
#: htdocs/luci-static/resources/view/fchomo/client.js:1605
msgid "EDNS Client Subnet"
msgstr ""
msgstr "EDNS 客户端子网"
#: htdocs/luci-static/resources/view/fchomo/global.js:433
msgid "ETag support"
@@ -820,6 +820,10 @@ msgstr "编辑入站"
msgid "Edit node"
msgstr "编辑节点"
#: htdocs/luci-static/resources/view/fchomo/node.js:214
msgid "Edit outbound"
msgstr "编辑出站"
#: htdocs/luci-static/resources/view/fchomo/ruleset.js:138
msgid "Edit ruleset"
msgstr "编辑规则集"
@@ -1104,7 +1108,7 @@ msgstr "兜底 DNS 服务器 (用于已被投毒污染的域名)"
#: htdocs/luci-static/resources/fchomo/listeners.js:137
msgid "Firewall"
msgstr ""
msgstr "防火墙"
#: htdocs/luci-static/resources/fchomo/listeners.js:535
#: htdocs/luci-static/resources/view/fchomo/node.js:660
@@ -1335,7 +1339,7 @@ msgstr "握手超时"
#: htdocs/luci-static/resources/fchomo/listeners.js:244
msgid "Header to read real client IP from (e.g. X-Forwarded-For)"
msgstr ""
msgstr "用于读取客户端真实 IP 的 Header(例如 X-Forwarded-For)"
#: htdocs/luci-static/resources/view/fchomo/node.js:751
msgid "Health check"
@@ -1525,7 +1529,7 @@ msgstr "单位为毫秒。留空则使用 <code>%s</code>。"
#: htdocs/luci-static/resources/view/fchomo/node.js:1350
msgid "In milliseconds."
msgstr ""
msgstr "单位为毫秒。"
#: htdocs/luci-static/resources/fchomo/listeners.js:418
#: htdocs/luci-static/resources/fchomo/listeners.js:487
@@ -3882,7 +3886,7 @@ msgstr "包含 %d 个字符的有效 base64 密钥"
#: htdocs/luci-static/resources/fchomo.js:1579
#: htdocs/luci-static/resources/fchomo.js:1585
msgid "valid format: 2x, 2p, 4v"
msgstr ""
msgstr "有效格式: 2x, 2p, 4v"
#: htdocs/luci-static/resources/fchomo.js:1566
msgid "valid key length with %d characters"
+9 -5
View File
@@ -798,7 +798,7 @@ msgstr "強制覆蓋 ECS"
#: htdocs/luci-static/resources/view/fchomo/client.js:1605
msgid "EDNS Client Subnet"
msgstr ""
msgstr "EDNS 客戶端子網"
#: htdocs/luci-static/resources/view/fchomo/global.js:433
msgid "ETag support"
@@ -820,6 +820,10 @@ msgstr "編輯入站"
msgid "Edit node"
msgstr "編輯節點"
#: htdocs/luci-static/resources/view/fchomo/node.js:214
msgid "Edit outbound"
msgstr "編輯出站"
#: htdocs/luci-static/resources/view/fchomo/ruleset.js:138
msgid "Edit ruleset"
msgstr "編輯規則集"
@@ -1104,7 +1108,7 @@ msgstr "兜底 DNS 伺服器 (用於已被投毒汙染的網域)"
#: htdocs/luci-static/resources/fchomo/listeners.js:137
msgid "Firewall"
msgstr ""
msgstr "防火牆"
#: htdocs/luci-static/resources/fchomo/listeners.js:535
#: htdocs/luci-static/resources/view/fchomo/node.js:660
@@ -1335,7 +1339,7 @@ msgstr "握手超時"
#: htdocs/luci-static/resources/fchomo/listeners.js:244
msgid "Header to read real client IP from (e.g. X-Forwarded-For)"
msgstr ""
msgstr "用於讀取客戶端真實 IP 的 Header(例如 X-Forwarded-For)"
#: htdocs/luci-static/resources/view/fchomo/node.js:751
msgid "Health check"
@@ -1525,7 +1529,7 @@ msgstr "單位為毫秒。留空則使用 <code>%s</code>。"
#: htdocs/luci-static/resources/view/fchomo/node.js:1350
msgid "In milliseconds."
msgstr ""
msgstr "單位為毫秒。"
#: htdocs/luci-static/resources/fchomo/listeners.js:418
#: htdocs/luci-static/resources/fchomo/listeners.js:487
@@ -3882,7 +3886,7 @@ msgstr "包含 %d 個字元的有效 base64 金鑰"
#: htdocs/luci-static/resources/fchomo.js:1579
#: htdocs/luci-static/resources/fchomo.js:1585
msgid "valid format: 2x, 2p, 4v"
msgstr ""
msgstr "有效格式: 2x, 2p, 4v"
#: htdocs/luci-static/resources/fchomo.js:1566
msgid "valid key length with %d characters"
+1 -1
View File
@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.2.1
PKG_RELEASE:=6
PKG_RELEASE:=7
LUCI_TITLE:=LuCI Support for momo
LUCI_DEPENDS:=+luci-base +momo
@@ -19,6 +19,12 @@ const callRCInit = rpc.declare({
expect: { '': {} }
});
const callFileWrite = rpc.declare({
object: 'file',
method: 'write',
params: ['path', 'data', 'append', 'mode']
});
const callMomoGetPaths = rpc.declare({
object: 'luci.momo',
method: 'get_paths',
@@ -81,6 +87,31 @@ return baseclass.extend({
return callRCInit('momo', 'restart');
},
writefile: function (path, data, mode) {
data = (data != null) ? String(data) : '';
mode = (mode != null) ? mode : 0o644;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const chunkSize = 8 * 1024;
const bytes = encoder.encode(data);
if (bytes.length <= chunkSize) {
return callFileWrite(path, data, false, mode);
}
let promise = Promise.resolve();
for(let offset = 0; offset < bytes.length; offset += chunkSize) {
const chunkBytes = bytes.slice(offset, Math.min(offset + chunkSize, bytes.length));
const chunk = decoder.decode(chunkBytes);
const append = offset > 0;
promise = promise.then(() => callFileWrite(path, chunk, append, mode));
}
return promise;
},
version: function () {
return callMomoVersion();
},
@@ -138,12 +169,12 @@ return baseclass.extend({
clearAppLog: async function () {
const paths = await this.getPaths();
return fs.write(paths.app_log_path);
return this.writefile(paths.app_log_path);
},
clearCoreLog: async function () {
const paths = await this.getPaths();
return fs.write(paths.core_log_path);
return this.writefile(paths.core_log_path);
},
debug: function () {
@@ -51,11 +51,11 @@ return view.extend({
o.wrap = false;
o.write = function (section_id, formvalue) {
const path = m.lookupOption('_file', section_id)[0].formvalue(section_id);
return fs.write(path, formvalue);
return momo.writefile(path, formvalue);
};
o.remove = function (section_id) {
const path = m.lookupOption('_file', section_id)[0].formvalue(section_id);
return fs.write(path);
return momo.writefile(path);
};
return m.render();
@@ -2,8 +2,8 @@
'use strict';
import { access, popen, readfile } from 'fs';
import { get_paths, merge_exists, get_users, get_groups, get_cgroups, load_profile, save_profile } from '/etc/momo/ucode/include.uc';
import { popen } from 'fs';
import { get_paths, merge_exists, get_users, get_groups, get_cgroups, load_profile } from '/etc/momo/ucode/include.uc';
const methods = {
get_paths: {
+1 -1
View File
@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.26.1
PKG_RELEASE:=5
PKG_RELEASE:=6
LUCI_TITLE:=LuCI Support for nikki
LUCI_DEPENDS:=+luci-base +nikki
@@ -19,6 +19,12 @@ const callRCInit = rpc.declare({
expect: { '': {} }
});
const callFileWrite = rpc.declare({
object: 'file',
method: 'write',
params: ['path', 'data', 'append', 'mode']
});
const callNikkiVersion = rpc.declare({
object: 'luci.nikki',
method: 'version',
@@ -98,6 +104,31 @@ return baseclass.extend({
return callRCInit('nikki', 'restart');
},
writefile: function (path, data, mode) {
data = (data != null) ? String(data) : '';
mode = (mode != null) ? mode : 0o644;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const chunkSize = 8 * 1024;
const bytes = encoder.encode(data);
if (bytes.length <= chunkSize) {
return callFileWrite(path, data, false, mode);
}
let promise = Promise.resolve();
for(let offset = 0; offset < bytes.length; offset += chunkSize) {
const chunkBytes = bytes.slice(offset, Math.min(offset + chunkSize, bytes.length));
const chunk = decoder.decode(chunkBytes);
const append = offset > 0;
promise = promise.then(() => callFileWrite(path, chunk, append, mode));
}
return promise;
},
version: function () {
return callNikkiVersion();
},
@@ -183,11 +214,11 @@ return baseclass.extend({
},
clearAppLog: function () {
return fs.write(this.appLogPath);
return this.writefile(this.appLogPath, '');
},
clearCoreLog: function () {
return fs.write(this.coreLogPath);
return this.writefile(this.coreLogPath, '');
},
debug: function () {
@@ -62,11 +62,11 @@ return view.extend({
o.wrap = false;
o.write = function (section_id, formvalue) {
const path = m.lookupOption('_file', section_id)[0].formvalue(section_id);
return fs.write(path, formvalue);
return nikki.writefile(path, formvalue);
};
o.remove = function (section_id) {
const path = m.lookupOption('_file', section_id)[0].formvalue(section_id);
return fs.write(path);
return nikki.writefile(path);
};
return m.render();
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.6.3
PKG_RELEASE:=54
PKG_VERSION:=26.6.16
PKG_RELEASE:=55
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -320,15 +320,6 @@ o.rewrite_option = "method"
for a, t in ipairs(ss_method_list) do o:value(t) end
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(Flag, _n("iv_check"), translate("IV Check"))
o:depends({ [_n("protocol")] = "shadowsocks", [_n("ss_method")] = "aes-128-gcm" })
o:depends({ [_n("protocol")] = "shadowsocks", [_n("ss_method")] = "aes-256-gcm" })
o:depends({ [_n("protocol")] = "shadowsocks", [_n("ss_method")] = "chacha20-poly1305" })
o:depends({ [_n("protocol")] = "shadowsocks", [_n("ss_method")] = "xchacha20-poly1305" })
o = s:option(Flag, _n("uot"), translate("UDP over TCP"))
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(ListValue, _n("flow"), translate("flow"))
o.default = ""
o:value("", translate("Disable"))
@@ -420,7 +411,6 @@ o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
-- o = s:option(Value, _n("minversion"), translate("minversion"))
-- o.default = "1.3"
@@ -772,11 +762,6 @@ o.default = 0
o = s:option(Flag, _n("tcpMptcp"), "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration."))
o.default = 0
o = s:option(Value, _n("preconns"), translate("Pre-connections"), translate("Number of early established connections to reduce latency."))
o.datatype = "uinteger"
o.placeholder = 0
o:depends({ [_n("protocol")] = "vless" })
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution."))
o:value("", translate("Auto"))
o:value("tcp", "TCP")
@@ -97,9 +97,6 @@ o.rewrite_option = "method"
for a, t in ipairs(x_ss_method_list) do o:value(t) end
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(Flag, _n("iv_check"), translate("IV Check"))
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(ListValue, _n("ss_network"), translate("Transport"))
o.default = "tcp,udp"
o:value("tcp", "TCP")
@@ -232,7 +229,6 @@ o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Flag, _n("use_mldsa65Seed"), translate("ML-DSA-65"))
o.default = "0"
+10 -14
View File
@@ -1611,27 +1611,23 @@ end
function parse_realm_uri(uri)
if type(uri) ~= "string" then return nil end
-- realm://token@server/realm_id?query
local token, server_url, realm_id, query = trim(uri):match("^realm://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not token or not server_url or not realm_id then return nil end
-- realm[+http]://token@server/realm_id?query
local scheme, token, server_url, realm_id, query = trim(uri):match("^(realm%+http|realm)://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not scheme or not token or not server_url or not realm_id then return nil end
realm_id = realm_id:gsub("/+$", "")
local realm = {
scheme = scheme,
token = token,
server_url = server_url,
realm_id = realm_id
}
-- 解析 query 中的 stun=
if query and query ~= "" then
local stun_servers = {}
for key, value in query:gmatch("([^&=?]+)=([^&]+)") do
if key == "stun" and value ~= "" then
stun_servers[#stun_servers + 1] = value
end
end
if #stun_servers > 0 then
realm.stun_servers = stun_servers
end
-- Parse stun= in the query
local stun_servers
for value in (query or ""):gmatch("[?&]?[Ss][Tt][Uu][Nn]=([^&]+)") do
if not stun_servers then stun_servers = {} end
stun_servers[#stun_servers + 1] = value
end
realm.stun_servers = stun_servers
return realm
end
@@ -64,7 +64,8 @@ function gen_config(var)
server_host = api.get_ipv6_full(server_host)
end
local server = server_host .. ":" .. ((server_port or "") .. "," .. (node.hysteria2_hop or "")):gsub("^[%s,]+", ""):gsub("[%s,]+$", ""):gsub(":", "-")
local port_hop = ((server_port or "") .. "," .. (node.hysteria2_hop or "")):gsub("^[%s,]+", ""):gsub("[%s,]+$", ""):gsub(":", "-")
local server = server_host .. ":" .. (port_hop ~= "" and port_hop or "443")
local config = {
server = (function()
@@ -617,8 +617,9 @@ function gen_outbound(flag, node, tag, proxy_table)
result.server_port = nil
local realm = api.parse_realm_uri(node.hysteria2_realm_url)
if realm then
realm.server_url = realm.server_url and "https://" .. realm.server_url or nil
realm.server_url = (realm.scheme == "realm+http" and "http://" or "https://") .. realm.server_url
realm.stun_servers = realm.stun_servers or node.hysteria2_realm_stun
realm.scheme = nil
return realm
end
return nil
@@ -959,8 +960,9 @@ function gen_config_server(node)
realm = node.hysteria2_realms and (function()
local realm = api.parse_realm_uri(node.hysteria2_realm_url)
if realm then
realm.server_url = realm.server_url and "https://" .. realm.server_url or nil
realm.server_url = (realm.scheme == "realm+http" and "http://" or "https://") .. realm.server_url
realm.stun_servers = realm.stun_servers or node.hysteria2_realm_stun
realm.scheme = nil
realm.stun_domain_resolver = "direct"
return realm
end
@@ -1797,13 +1799,12 @@ function gen_config(var)
end
end
table.insert(route.rules, {
action = "route",
ip_is_private = true,
outbound = "direct"
})
if COMMON.default_outbound_tag then
table.insert(route.rules, {
action = "route",
port_range = { "0:65535" },
outbound = COMMON.default_outbound_tag
})
route.final = COMMON.default_outbound_tag
end
@@ -285,21 +285,22 @@ function gen_outbound(flag, node, tag, proxy_table)
local finalmask = {}
local TP = node.transport
if TP == "mkcp" then
local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat",
dtls = "dtls", wireguard = "wireguard", dns = "dns"}
local udp = {}
if node.mkcp_guise and node.mkcp_guise ~= "none" then
local g = { type = map[node.mkcp_guise] }
local g = { type = "mkcp-legacy" }
g.settings = { header = map[node.mkcp_guise] }
if node.mkcp_guise == "dns" and node.mkcp_domain and node.mkcp_domain ~= "" then
g.settings = { domain = node.mkcp_domain }
g.settings.value = node.mkcp_domain
end
udp[#udp+1] = g
end
local c = { type = (node.mkcp_seed and node.mkcp_seed ~= "") and "mkcp-aes128gcm" or "mkcp-original" }
local s = { type = "mkcp-legacy" }
if node.mkcp_seed and node.mkcp_seed ~= "" then
c.settings = { password = node.mkcp_seed }
s.settings = { value = node.mkcp_seed }
end
udp[#udp+1] = c
udp[#udp+1] = s
finalmask.udp = udp
elseif TP == "hysteria" then
local udp = {}
@@ -317,9 +318,7 @@ function gen_outbound(flag, node, tag, proxy_table)
local realm = api.parse_realm_uri(node.hysteria2_realm_url)
local url, stun
if realm then
if realm.token and realm.server_url and realm.realm_id then
url = "realm://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
end
url = realm.scheme .. "://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
stun = realm.stun_servers or node.hysteria2_realm_stun
end
local r = {
@@ -386,43 +385,18 @@ function gen_outbound(flag, node, tag, proxy_table)
end)()
} or nil,
settings = {
vnext = (node.protocol == "vmess" or node.protocol == "vless") and {
{
address = node.address,
port = tonumber(node.port),
users = {
{
id = node.uuid,
level = 0,
security = (node.protocol == "vmess") and node.security or nil,
testpre = (node.protocol == "vless") and tonumber(node.preconns) or nil,
encryption = (node.protocol == "vless") and ((node.encryption and node.encryption ~= "") and node.encryption or "none") or nil,
flow = (node.protocol == "vless"
and (node.tls == "1" or (node.encryption and node.encryption ~= "" and node.encryption ~= "none"))
and node.flow and node.flow ~= "") and node.flow or nil
}
}
}
} or nil,
servers = (node.protocol == "socks" or node.protocol == "http" or node.protocol == "shadowsocks" or node.protocol == "trojan") and {
{
address = node.address,
port = tonumber(node.port),
method = (node.method == "chacha20-ietf-poly1305" and "chacha20-poly1305") or
(node.method == "xchacha20-ietf-poly1305" and "xchacha20-poly1305") or
(node.method ~= "" and node.method) or nil,
ivCheck = (node.protocol == "shadowsocks") and node.iv_check == "1" or nil,
uot = (node.protocol == "shadowsocks") and node.uot == "1" or nil,
password = node.password or "",
users = (node.username and node.password) and {
{
user = node.username,
pass = node.password
}
} or nil
}
} or nil,
address = (node.protocol == "wireguard" and node.wireguard_local_address) or (node.protocol == "hysteria" and node.address) or nil,
address = (node.protocol == "wireguard") and node.wireguard_local_address or node.address,
port = tonumber(node.port),
id = (node.protocol == "vmess" or node.protocol == "vless") and node.uuid or nil,
encryption = (node.protocol == "vless") and ((node.encryption and node.encryption ~= "") and node.encryption or "none") or nil,
flow = (node.protocol == "vless" and (node.tls == "1" or (node.encryption and node.encryption ~= "" and node.encryption ~= "none"))
and node.flow and node.flow ~= "") and node.flow or nil,
security = (node.protocol == "vmess") and node.security or nil,
user = (node.protocol == "socks" or node.protocol == "http") and node.username or nil,
pass = (node.protocol == "socks" or node.protocol == "http") and node.password or nil,
password = (node.protocol == "shadowsocks" or node.protocol == "trojan") and node.password or nil,
method = (node.protocol == "shadowsocks") and ((node.method == "chacha20-ietf-poly1305" and "chacha20-poly1305") or
(node.method == "xchacha20-ietf-poly1305" and "xchacha20-poly1305") or (node.method ~= "" and node.method) or nil) or nil,
secretKey = (node.protocol == "wireguard") and node.wireguard_secret_key or nil,
peers = (node.protocol == "wireguard") and {
{
@@ -434,13 +408,13 @@ function gen_outbound(flag, node, tag, proxy_table)
} or nil,
mtu = (node.protocol == "wireguard" and node.wireguard_mtu) and tonumber(node.wireguard_mtu) or nil,
reserved = (node.protocol == "wireguard" and node.wireguard_reserved) and node.wireguard_reserved or nil,
port = (node.protocol == "hysteria" and node.port) and tonumber(node.port) or nil,
version = node.protocol == "hysteria" and 2 or nil
version = (node.protocol == "hysteria") and 2 or nil,
level = 0
}
}
if node.protocol == "wireguard" then
result.settings.kernelMode = false
result.settings.noKernelTun = true
if node.finalmask and node.finalmask ~= "" then
local ok, fm = pcall(jsonc.parse, api.base64Decode(node.finalmask))
if ok and type(fm) == "table" then
@@ -561,7 +535,6 @@ function gen_config_server(node)
settings = {
method = node.method,
password = node.password,
ivCheck = ("1" == node.iv_check) and true or false,
network = node.ss_network or "TCP,UDP"
}
elseif node.protocol == "trojan" then
@@ -740,21 +713,22 @@ function gen_config_server(node)
finalmask = (function()
local finalmask = {}
if node.transport == "mkcp" then
local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat",
dtls = "dtls", wireguard = "wireguard", dns = "dns"}
local udp = {}
if node.mkcp_guise and node.mkcp_guise ~= "none" then
local g = { type = map[node.mkcp_guise] }
local g = { type = "mkcp-legacy" }
g.settings = { header = map[node.mkcp_guise] }
if node.mkcp_guise == "dns" and node.mkcp_domain and node.mkcp_domain ~= "" then
g.settings = { domain = node.mkcp_domain }
g.settings.value = node.mkcp_domain
end
udp[#udp+1] = g
end
local c = { type = (node.mkcp_seed and node.mkcp_seed ~= "") and "mkcp-aes128gcm" or "mkcp-original" }
local s = { type = "mkcp-legacy" }
if node.mkcp_seed and node.mkcp_seed ~= "" then
c.settings = { password = node.mkcp_seed }
s.settings = { value = node.mkcp_seed }
end
udp[#udp+1] = c
udp[#udp+1] = s
finalmask.udp = udp
elseif node.transport == "hysteria" then
local udp = {}
@@ -772,9 +746,7 @@ function gen_config_server(node)
local realm = api.parse_realm_uri(node.hysteria2_realm_url)
local url, stun
if realm then
if realm.token and realm.server_url and realm.realm_id then
url = "realm://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
end
url = realm.scheme .. "://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
stun = realm.stun_servers or node.hysteria2_realm_stun
end
local r = {
@@ -1524,7 +1496,7 @@ function gen_config(var)
outboundTag = default_outboundTag,
}
if node.domainStrategy == "IPIfNonMatch" then
rule.ip = { "0.0.0.0/0", "::/0" }
rule.port = "1-65535"
else
rule.network = "tcp,udp"
end
@@ -1718,7 +1690,8 @@ function gen_config(var)
})
end
end
local dns_outbound
if dns_listen_port then
table.insert(inbounds, {
listen = "127.0.0.1",
@@ -1775,12 +1748,13 @@ function gen_config(var)
}
}
local type_dns = direct_type_dns
table.insert(outbounds, {
dns_outbound = {
tag = "dns-out",
protocol = "dns",
proxySettings = type_dns.proxySettings,
settings = type_dns.settings
})
}
table.insert(outbounds, dns_outbound)
table.insert(routing.rules, 1, {
inboundTag = {
"dns-in"
@@ -1815,6 +1789,7 @@ function gen_config(var)
end
-- Shunt rule DNS logic
local dns_out_rules = {}
if dns_domain_rules and #dns_domain_rules > 0 then
for index, value in ipairs(dns_domain_rules) do
if value.domain and value.outboundTag then
@@ -1834,19 +1809,19 @@ function gen_config(var)
end
end
end
local dns_block = true -- If you do not want the blackhole rule to use DNS routing, please change it to `nil` or do not assign a value.
if dns_outboundTag == "blackhole" then
if dns_block then
local dns_block_mode = "Hosts" -- use the Hosts mode to block.
if dns_block_mode == "Hosts" then
for d_i, d_k in ipairs(value.domain) do
dns.hosts[d_k] = "0.0.0.0"
end
dns_server = nil
end
else
dns_server = nil
end
table.insert(dns_out_rules, {
action = "return",
rCode = 0,
domain = api.clone(value.domain)
})
dns_server = nil
else
table.insert(dns_out_rules, {
action = "hijack",
qType = "1,28",
domain = api.clone(value.domain)
})
end
if dns_server then
dns_server.finalQuery = true
@@ -1861,6 +1836,11 @@ function gen_config(var)
end
end
end
if dns_outbound and #dns_out_rules > 0 then
for i = #dns_out_rules, 1, -1 do
table.insert(dns_outbound.settings.rules, 1, dns_out_rules[i])
end
end
end
end
-9
View File
@@ -1251,9 +1251,6 @@ msgstr "پاک کردن گزارش‌ها"
msgid "Password"
msgstr "رمز عبور"
msgid "IV Check"
msgstr "بررسی IV"
msgid "UDP over TCP"
msgstr "UDP بر روی TCP"
@@ -1266,12 +1263,6 @@ msgstr "پورت محلی"
msgid "Fast Open"
msgstr "بازگشایی سریع (Fast Open)"
msgid "Pre-connections"
msgstr "پیش‌اتصالات"
msgid "Number of early established connections to reduce latency."
msgstr "تعداد اتصالات از پیش برقرار شده برای کاهش تاخیر."
msgid "plugin"
msgstr "افزونه"
-9
View File
@@ -1252,9 +1252,6 @@ msgstr "Очистить журнал"
msgid "Password"
msgstr "Пароль"
msgid "IV Check"
msgstr "Проверка IV"
msgid "UDP over TCP"
msgstr "UDP через TCP"
@@ -1267,12 +1264,6 @@ msgstr "Локальный порт"
msgid "Fast Open"
msgstr "Fast Open"
msgid "Pre-connections"
msgstr "Предварительные соединения"
msgid "Number of early established connections to reduce latency."
msgstr "Количество заранее установленных соединений для снижения задержки."
msgid "plugin"
msgstr "Плагин"
-9
View File
@@ -1243,9 +1243,6 @@ msgstr "清空日志"
msgid "Password"
msgstr "密码"
msgid "IV Check"
msgstr "IV 检查"
msgid "UDP over TCP"
msgstr "TCP 封装 UDP"
@@ -1258,12 +1255,6 @@ msgstr "本地端口"
msgid "Fast Open"
msgstr "快速打开"
msgid "Pre-connections"
msgstr "预连接"
msgid "Number of early established connections to reduce latency."
msgstr "预连接的数量,用于降低延迟。"
msgid "plugin"
msgstr "插件"
-9
View File
@@ -1243,9 +1243,6 @@ msgstr "清空日誌"
msgid "Password"
msgstr "密碼"
msgid "IV Check"
msgstr "IV 檢查"
msgid "UDP over TCP"
msgstr "TCP 封装 UDP"
@@ -1258,12 +1255,6 @@ msgstr "本地端口"
msgid "Fast Open"
msgstr "快速打開"
msgid "Pre-connections"
msgstr "預連接"
msgid "Number of early established connections to reduce latency."
msgstr "預連接的數量,用於降低延遲。"
msgid "plugin"
msgstr "插件"
@@ -2,6 +2,15 @@
START=99
boot_func() {
sleep 90
restart
}
boot() {
boot_func >/dev/null 2>&1 &
}
start() {
lua /usr/lib/lua/luci/passwall2/server_app.lua start
}
@@ -88,6 +88,23 @@ config shunt_rules 'Russia'
option ip_list 'geoip:ru
#rule-set:remote:https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/refs/heads/release/sing-box/rule-set-geoip/geoip-ru.srs'
config shunt_rules 'PrivateIP'
option remarks 'PrivateIP'
option network 'tcp,udp'
option ip_list '10.0.0.0/8
127.0.0.0/8
198.51.100.0/24
192.0.0.0/24
169.254.0.0/16
224.0.0.0/3
192.168.0.0/16
192.88.99.0/24
100.64.0.0/10
172.16.0.0/12
0.0.0.0/8
192.0.2.0/24
203.0.113.0/24'
config nodes 'examplenode'
option remarks 'Example'
option type 'Xray'
@@ -104,6 +121,7 @@ config nodes 'rulenode'
option remarks 'rulenode'
option type 'Xray'
option protocol '_shunt'
option PrivateIP '_direct'
option default_node 'examplenode'
option domainStrategy 'IPOnDemand'
option domainMatcher 'hybrid'
@@ -1937,7 +1937,6 @@ local function curl(url, file, ua, mode)
ua = (ua == "passwall2") and ("passwall2/" .. api.get_version()) or ua
curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"'
end
curl_args[#curl_args + 1] = get_headers()
local return_code, result
if mode == "direct" then
return_code, result = api.curl_direct(url, file, curl_args)
@@ -1949,57 +1948,6 @@ local function curl(url, file, ua, mode)
return return_code, tonumber(result)
end
function get_headers()
local cache_file = "/tmp/etc/" .. appname .. "_tmp/sub_curl_headers"
if fs.access(cache_file) then
return luci.sys.exec("cat " .. cache_file)
end
local headers = {}
local function readfile(path)
local f = io.open(path, "r")
if not f then return nil end
local c = f:read("*a")
f:close()
return api.trim(c)
end
headers[#headers + 1] = "x-device-os: OpenWrt"
local rel = readfile("/etc/openwrt_release")
local os_ver = rel and rel:match("DISTRIB_RELEASE='([^']+)'")
if os_ver then
headers[#headers + 1] = "x-ver-os: " .. os_ver
end
local model = readfile("/tmp/sysinfo/model")
if model then
headers[#headers + 1] = "x-device-model: " .. model
end
local mac = readfile("/sys/class/net/eth0/address")
if mac and model then
local raw = mac .. "-" .. model
local p = io.popen("printf '%s' '" .. raw:gsub("'", "'\\''") .. "' | sha256sum")
if p then
local hash = p:read("*l")
p:close()
hash = hash and hash:match("^%w+")
if hash then
headers[#headers + 1] = "x-hwid: " .. hash
end
end
end
local out = {}
for i = 1, #headers do
out[i] = "-H '" .. headers[i]:gsub("'", "'\\''") .. "'"
end
local headers_str = table.concat(out, " ")
local f = io.open(cache_file, "w"); if f then f:write(headers_str); f:close() end
return headers_str
end
local function truncate_nodes(group)
for _, config in pairs(CONFIG) do
if config.currentNodes and #config.currentNodes > 0 then
+8 -5
View File
@@ -5,17 +5,19 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI based ipk store
LUCI_DESCRIPTION:=luci-app-store is a ipk store developed by LinkEase team
LUCI_DEPENDS:=@(x86_64||aarch64) +curl +opkg +luci-lib-ipkg +tar +libuci-lua +mount-utils +luci-lib-taskd
LUCI_TITLE:=LuCI based ipk/apk store
LUCI_DESCRIPTION:=luci-app-store is a ipk/apk store developed by LinkEase team
LUCI_DEPENDS:=@(x86_64||aarch64) +curl +tar +libuci-lua +mount-utils +luci-lib-taskd
LUCI_DEPENDS+=$(if $(CONFIG_USE_APK),+apk +luci-compat,+opkg)
LUCI_EXTRA_DEPENDS:=luci-lib-taskd (>=1.0.19)
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.32-1
PKG_VERSION:=0.2.0-r2
# PKG_RELEASE MUST be empty for luci.mk
PKG_RELEASE:=
ISTORE_UI_VERSION:=0.1.32
ISTORE_UI_VERSION:=0.2.0
ISTORE_UI_RELEASE:=1
PKG_HASH:=skip
@@ -27,6 +29,7 @@ PKG_MAINTAINER:=jjm2473 <jjm2473@gmail.com>
TARGET_CONFIGURE_OPTS= FRONTEND_DIST="$(BUILD_DIR)/istore-ui-$(ISTORE_UI_VERSION)-$(ISTORE_UI_RELEASE)/app-store-ui/src/dist" APP_STORE_VERSION="$(PKG_VERSION)"
TARGET_CONFIGURE_OPTS+= SED="$(SED)"
TARGET_CONFIGURE_OPTS+= USE_APK="$(CONFIG_USE_APK)"
define Package/luci-app-store/conffiles
/etc/.app_store.id
+23 -21
View File
@@ -166,6 +166,11 @@ end
function store_index()
local fs = require "nixio.fs"
local features = { "_lua_force_array_" }
if fs.access("/etc/apk/arch") then
features[#features+1] = "apk"
else
features[#features+1] = "opkg"
end
if fs.access("/usr/libexec/istore/backup") then
features[#features+1] = "backup"
end
@@ -251,13 +256,11 @@ function validate_pkgname(val)
end
local function get_installed_and_cache()
local metadir = "/usr/lib/opkg/meta"
local metadir = "/tmp/run/istore-meta/meta"
local cachedir = "/tmp/cache/istore"
local cachefile = cachedir .. "/installed.json"
local metapkgpre = "app-meta-"
local nixio = require "nixio"
local fs = require "nixio.fs"
local ipkg = require "luci.model.ipkg"
local jsonc = require "luci.jsonc"
local result = {}
local lock, msg = flock("/var/lock/istore-installed.lock", "lock")
@@ -274,7 +277,8 @@ local function get_installed_and_cache()
local pkg
for pkg in itr do
if pkg:match("^.*%.json$") then
local metadata = fs.readfile(metadir .. "/" .. pkg)
local metafile = metadir .. "/" .. pkg
local metadata = fs.readfile(metafile)
if metadata ~= nil then
local meta = jsonc.parse(metadata)
if meta == nil then
@@ -291,15 +295,10 @@ local function get_installed_and_cache()
}
end
local time = nil
local metapkg = metapkgpre .. meta.name
local status = ipkg.status(metapkg)
if next(status) ~= nil then
time = status[metapkg]["Installed-Time"]
local istat = fs.stat(metafile)
if istat ~= nil then
time = istat["mtime"]
else
local istat = fs.stat("/usr/lib/opkg/info/" .. metapkg .. ".list")
if istat ~= nil then
time = istat["mtime"]
end
cacheable = false
end
if time ~= nil then
@@ -313,7 +312,7 @@ local function get_installed_and_cache()
result = data
if cacheable then
fs.mkdirr(cachedir)
local oflags = nixio.open_flags("rdwr", "creat")
local oflags = nixio.open_flags("wronly", "creat", "trunc")
local mfile, code, msg = nixio.open(cachefile, oflags)
mfile:writeall(jsonc.stringify(result))
mfile:close()
@@ -327,11 +326,10 @@ local function get_installed_and_cache()
end
function store_action(param)
local metadir = "/usr/lib/opkg/meta"
local metadir = "/tmp/run/istore-meta/meta"
local metapkgpre = "app-meta-"
local code, out, err, ret
local fs = require "nixio.fs"
local ipkg = require "luci.model.ipkg"
local jsonc = require "luci.jsonc"
local json_parse = jsonc.parse
local action = param.action or ""
@@ -342,18 +340,22 @@ function store_action(param)
luci.http.status(400, "Bad Request")
return
end
local metapkg = metapkgpre .. pkg
local meta = {}
local metadata = fs.readfile(metadir .. "/" .. pkg .. ".json")
local metafile = metadir .. "/" .. pkg .. ".json"
local metadata = fs.readfile(metafile)
if metadata ~= nil then
meta = json_parse(metadata) or {}
end
meta.installed = false
local status = ipkg.status(metapkg)
if next(status) ~= nil then
local time = nil
local istat = fs.stat(metafile)
if istat ~= nil then
time = istat["mtime"]
end
if time ~= nil then
meta.installed=true
meta.time=tonumber(status[metapkg]["Installed-Time"])
meta.time=tonumber(time)
end
ret = meta
@@ -571,7 +573,7 @@ function entrysh()
errors[#errors+1] = {app=meta.name, code=500, msg="json parse failed: " .. o}
else
results[#results+1] = status
local oflags = nixio.open_flags("rdwr", "creat")
local oflags = nixio.open_flags("wronly", "creat", "trunc")
local mfile, code, msg = nixio.open(cachefile, oflags)
mfile:writeall(jsonc.stringify(status))
mfile:close()
+221 -56
View File
@@ -10,25 +10,46 @@ if [ "${action:0:9}" = "AUTOCONF=" ]; then
fi
IS_ROOT=/tmp/is-root
DL_DIR=${IS_ROOT}/tmp/dl
LISTS_DIR_O=/tmp/opkg-lists
LISTS_DIR=${IS_ROOT}${LISTS_DIR_O}
OPKG_CONF_DIR=${IS_ROOT}/etc/opkg
OPKG_CONF_DIR_M=${IS_ROOT}/etc/opkg_m
FEEDS_SERVER=https://istore.istoreos.com/repo
FEEDS_SERVER_MIRRORS="https://repo.istoreos.com/repo"
TMP_SELF_COPY=/var/run/cloned-is-opkg
USE_APK=false
DISABLE_MIRROR=false
ARCH=`sed -n -e 's/^Architecture: *\([^ ]\+\) *$/\1/p' /rom/usr/lib/opkg/info/libc.control /usr/lib/opkg/info/libc.control 2>/dev/null | head -1`
if [[ -s /etc/apk/arch ]]; then
USE_APK=true
fi
if $USE_APK; then
LISTS_DIR=${IS_ROOT}/apk
APK_CONF=${IS_ROOT}/etc/apk.conf
FEEDS_SERVER=https://istore.istoreos.com/repo-apk
FEEDS_SERVER_MIRRORS="https://repo.istoreos.com/repo-apk"
ISTORE_INDEX=$FEEDS_SERVER/all/store/packages.adb
META_DIR=/lib/apk
ARCH=`cat /rom/etc/apk/arch /etc/apk/arch 2>/dev/null | head -1`
else
LISTS_DIR_O=/tmp/opkg-lists
LISTS_DIR=${IS_ROOT}${LISTS_DIR_O}
OPKG_CONF_DIR=${IS_ROOT}/etc/opkg
OPKG_CONF_DIR_M=${IS_ROOT}/etc/opkg_m
FEEDS_SERVER=https://istore.istoreos.com/repo
FEEDS_SERVER_MIRRORS="https://repo.istoreos.com/repo"
ISTORE_INDEX=$FEEDS_SERVER/all/store/Packages.gz
META_DIR=/usr/lib/opkg
ARCH=`sed -n -e 's/^Architecture: *\([^ ]\+\) *$/\1/p' /rom/usr/lib/opkg/info/libc.control /usr/lib/opkg/info/libc.control 2>/dev/null | head -1`
fi
# for istore self upgrade
ISTORE_PKG=luci-app-store
ISTORE_DEP_PKGS="luci-lib-taskd luci-lib-xterm taskd"
ISTORE_INDEX=https://istore.istoreos.com/repo/all/store/Packages.gz
NEWLINE=$'\n'
is_init() {
mkdir -p ${DL_DIR} ${LISTS_DIR} ${IS_ROOT}/etc ${IS_ROOT}/var
mkdir -p ${LISTS_DIR} ${IS_ROOT}/etc ${IS_ROOT}/var
if $USE_APK; then
return 0
fi
cat /etc/opkg.conf | grep -Fv lists_dir | grep -Fv check_signature > ${IS_ROOT}/etc/opkg.conf
@@ -44,35 +65,71 @@ is_init() {
[ -e ${IS_ROOT}/var/lock ] || ln -s /var/lock ${IS_ROOT}/var/lock
}
apk_wrap() {
APK_CONFIG=${APK_CONF} apk "$@"
}
opkg_wrap() {
OPKG_CONF_DIR=${OPKG_CONF_DIR} opkg -f ${IS_ROOT}/etc/opkg.conf "$@"
}
opkg_wrap_mirrors() {
simple_apk_opkg() {
local action="$1"
shift
if $USE_APK; then
case "$action" in
"install")
action=add
;;
"remove")
action=del
;;
esac
apk_wrap "$action" "$@"
else
opkg_wrap "$action" "$@"
fi
}
do_in_mirrors() {
local server
local file
if ! $DISABLE_MIRROR; then
local newpath="/usr/libexec/istore/mirror-bin:$PATH"
for server in $FEEDS_SERVER_MIRRORS ; do
rm -rf "${OPKG_CONF_DIR_M}" 2>/dev/null
mkdir -p "${OPKG_CONF_DIR_M}" 2>/dev/null
ls "${OPKG_CONF_DIR}/" | while read; do
file="$REPLY"
if [ -f "${OPKG_CONF_DIR}/$file" -a "${file: -5}" = ".conf" ]; then
sed "s#$FEEDS_SERVER/#$server/#g" "${OPKG_CONF_DIR}/$file" >"${OPKG_CONF_DIR_M}/$file"
touch -r "${OPKG_CONF_DIR}/$file" "${OPKG_CONF_DIR_M}/$file" 2>/dev/null
else
cp -a "${OPKG_CONF_DIR}/$file" "${OPKG_CONF_DIR_M}/"
fi
done
echo "Try mirror server $server"
OPKG_CONF_DIR=${OPKG_CONF_DIR_M} opkg -f ${IS_ROOT}/etc/opkg.conf "$@" && return 0
PATH="$newpath" IS_ORIG_SERVER=$FEEDS_SERVER IS_MIRROR_SERVER=$server "$@" && return 0
done
DISABLE_MIRROR=true
fi
echo "Try origin server $FEEDS_SERVER"
OPKG_CONF_DIR=${OPKG_CONF_DIR} opkg -f ${IS_ROOT}/etc/opkg.conf "$@"
"$@"
}
apk_wrap_mirrors() {
do_in_mirrors apk_wrap "$@"
}
opkg_wrap_mirrors() {
do_in_mirrors opkg_wrap "$@"
}
do_install_in_mirrors() {
if $USE_APK; then
apk_wrap_mirrors add "$@"
else
opkg_wrap_mirrors install "$@"
fi
}
do_upgrade_in_mirrors() {
if $USE_APK; then
apk_wrap_mirrors upgrade "$@"
else
opkg_wrap_mirrors upgrade "$@"
fi
}
alias fcurl='curl -L --fail --show-error'
check_space() {
@@ -91,14 +148,41 @@ update() {
fi
echo "Fetch feed list for ${ARCH}"
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/meta.conf "${FEEDS_SERVER}/all/meta.conf" && \
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/all.conf "${FEEDS_SERVER}/all/isfeeds.conf" && \
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/arch.conf "${FEEDS_SERVER}/${ARCH}/isfeeds.conf" || \
return 1
if $USE_APK; then
rm -rf ${IS_ROOT}/var/tmp ${IS_ROOT}/etc/arch ${IS_ROOT}/etc/istore.pem
mkdir -p ${IS_ROOT}/var/tmp
fcurl --no-progress-meter -o ${IS_ROOT}/var/tmp/meta.conf "${FEEDS_SERVER}/all/meta.conf" && \
fcurl --no-progress-meter -o ${IS_ROOT}/var/tmp/all.conf "${FEEDS_SERVER}/all/isfeeds.conf" && \
fcurl --no-progress-meter -o ${IS_ROOT}/var/tmp/arch.conf "${FEEDS_SERVER}/${ARCH}/isfeeds.conf" && \
fcurl --no-progress-meter -o ${IS_ROOT}/etc/arch "${FEEDS_SERVER}/${ARCH}/archlist.conf" || \
return 1
echo "cache-dir $LISTS_DIR" > ${APK_CONF}
cat ${IS_ROOT}/var/tmp/arch.conf ${IS_ROOT}/var/tmp/all.conf ${IS_ROOT}/var/tmp/meta.conf >> ${APK_CONF}
local line
while read -r line; do
if [[ -n "$line" ]]; then
grep -Fxq "$line" /etc/apk/arch || echo "$line" >> /etc/apk/arch
fi
done < ${IS_ROOT}/etc/arch
sync /etc/apk/arch
if [[ ! -s /etc/apk/keys/istore.pem && ! -s /etc/apk/keys/istore-tmp.pem ]]; then
fcurl --no-progress-meter -o ${IS_ROOT}/etc/istore.pem "${FEEDS_SERVER}/istore-apk.pem" && \
cp -a ${IS_ROOT}/etc/istore.pem /etc/apk/keys/istore-tmp.pem || \
return 1
fi
else
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/meta.conf "${FEEDS_SERVER}/all/meta.conf" && \
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/all.conf "${FEEDS_SERVER}/all/isfeeds.conf" && \
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/arch.conf "${FEEDS_SERVER}/${ARCH}/isfeeds.conf" || \
return 1
fi
echo "Update feeds index"
opkg -f ${IS_ROOT}/etc/opkg_o.conf --offline-root ${IS_ROOT} update
if $USE_APK; then
apk_wrap update
else
opkg -f ${IS_ROOT}/etc/opkg_o.conf --offline-root ${IS_ROOT} update
fi
return 0
}
@@ -110,9 +194,30 @@ update_if_outdate() {
return 0
}
get_online_version() {
local index="$1"
local pkg="$2"
if $USE_APK; then
apk --cache-dir $LISTS_DIR --cache-max-age 0 --repositories-file /dev/null --repository "$index" update >/dev/null 2>&1
local ver=$(apk --cache-dir $LISTS_DIR --cache-max-age 0 --repositories-file /dev/null --repository "$index" query --available --fields version "$pkg" 2>/dev/null | grep -Fm1 'Version: ' | sed 's/^Version: //')
echo "${ver:-0.2.0-r1}"
else
curl --connect-timeout 2 --max-time 5 -s "$index" | gunzip | grep -FA10 "Package: $pkg" | grep -Fm1 'Version: ' | sed 's/^Version: //'
fi
}
get_local_version() {
local pkg="$1"
if $USE_APK; then
apk query --installed --fields version "$pkg" 2>/dev/null | grep -Fm1 'Version: ' | sed 's/^Version: //'
else
grep -Fm1 'Version: ' /usr/lib/opkg/info/$pkg.control 2>/dev/null | sed 's/^Version: //'
fi
}
check_self_upgrade() {
local newest=`curl --connect-timeout 2 --max-time 5 -s ${ISTORE_INDEX} | gunzip | grep -FA10 "Package: ${ISTORE_PKG}" | grep -Fm1 'Version: ' | sed 's/^Version: //'`
local current=`grep -Fm1 'Version: ' /usr/lib/opkg/info/${ISTORE_PKG}.control | sed 's/^Version: //'`
local newest=`get_online_version "$ISTORE_INDEX" "$ISTORE_PKG"`
local current=`get_local_version "$ISTORE_PKG"`
if [ "v$newest" = "v" -o "v$current" = "v" ]; then
echo "Check version failed!" >&2
exit 255
@@ -124,13 +229,13 @@ check_self_upgrade() {
}
do_self_upgrade_0() {
opkg_wrap upgrade ${ISTORE_DEP_PKGS} && opkg_wrap upgrade ${ISTORE_PKG}
simple_apk_opkg upgrade ${ISTORE_DEP_PKGS} && simple_apk_opkg upgrade ${ISTORE_PKG}
}
do_self_upgrade() {
check_mtime || return 1
local newest=`curl --connect-timeout 2 --max-time 5 -s ${ISTORE_INDEX} | gunzip | grep -FA10 "Package: ${ISTORE_PKG}" | grep -Fm1 'Version: ' | sed 's/^Version: //'`
local current=`grep -Fm1 'Version: ' /usr/lib/opkg/info/${ISTORE_PKG}.control | sed 's/^Version: //'`
local newest=`get_online_version "$ISTORE_INDEX" "$ISTORE_PKG"`
local current=`get_local_version "$ISTORE_PKG"`
if [ "v$newest" = "v" -o "v$current" = "v" ]; then
echo "Check version failed!" >&2
return 1
@@ -139,7 +244,7 @@ do_self_upgrade() {
echo "Already the latest version!" >&2
return 1
fi
if opkg_wrap info ${ISTORE_PKG} | grep -qFm1 "Version: $newest"; then
if $USE_APK || opkg_wrap info ${ISTORE_PKG} | grep -qFm1 "Version: $newest"; then
do_self_upgrade_0 && return 0
update_if_outdate || return 1
do_self_upgrade_0
@@ -150,7 +255,13 @@ do_self_upgrade() {
}
check_mtime() {
find ${OPKG_CONF_DIR}/arch.conf -mtime -1 2>/dev/null | grep -q . || update
local file
if $USE_APK; then
file=${APK_CONF}
else
file=${OPKG_CONF_DIR}/arch.conf
fi
find "$file" -mtime -1 2>/dev/null | grep -q . || update
}
wrapped_in_update() {
@@ -160,6 +271,16 @@ wrapped_in_update() {
eval "$@"
}
refresh_installed() {
local pkg
for pkg in $@; do
[[ $pkg == app-meta-* ]] || continue
pkg="$META_DIR/meta/${pkg#app-meta-}.json"
[[ -f "$pkg" ]] && touch "$pkg"
done
return 0
}
step_upgrade() {
local pkg
local pkgs=""
@@ -172,10 +293,11 @@ step_upgrade() {
fi
done
if [ -n "$pkgs" ]; then
opkg_wrap_mirrors upgrade $pkgs || return 1
do_upgrade_in_mirrors $pkgs || return 1
fi
if [ -n "$metapkg" ]; then
opkg_wrap_mirrors upgrade $metapkg || return 1
do_upgrade_in_mirrors $metapkg || return 1
refresh_installed $metapkg
fi
return 0
}
@@ -183,7 +305,7 @@ step_upgrade() {
new_upgrade() {
check_mtime || return 1
local metapkg=`echo "$@" | sed 's/ /\n/g' | grep -F app-meta-`
if [ -z "$metapkg" ] || opkg_wrap info $metapkg | grep -qF not-installed ; then
if [[ -z "$metapkg" ]] || [[ -z "$(get_local_version $metapkg)" ]]; then
true
else
update_if_outdate
@@ -192,7 +314,11 @@ new_upgrade() {
}
remove() {
opkg_wrap --autoremove --force-removal-of-dependent-packages remove "$@"
if $USE_APK; then
apk_wrap del -r "$@"
else
opkg_wrap --autoremove --force-removal-of-dependent-packages remove "$@"
fi
}
autoconf_to_env() {
@@ -224,16 +350,29 @@ try_autoconf() {
PATH="$CLEANPATH" /usr/libexec/istorea/${ISTORE_AUTOCONF}.sh
}
try_upgrade_depends() {
local pkg="$1"
if [[ $pkg == app-meta-* ]]; then
local deps=$(grep '^Depends: ' /usr/lib/opkg/info/$pkg.control | busybox sed -e 's/^Depends: //' -e 's/,/\n/g' -e 's/ //g' | grep -vFw libc | xargs echo)
[ -z "$deps" ] || opkg_wrap_mirrors install $deps
local deps
if $USE_APK; then
deps=$(apk --repositories-file /dev/null info -R "$pkg" | tail +2 | grep -vFw libc | xargs echo)
else
deps=$(grep '^Depends: ' "/usr/lib/opkg/info/$pkg.control" | busybox sed -e 's/^Depends: //' -e 's/,/\n/g' -e 's/ //g' | grep -vFw libc | xargs echo)
fi
[ -z "$deps" ] || do_install_in_mirrors $deps
fi
return 0
}
CMD_INSTALLED="opkg list-installed | cut -d' ' -f1 | sort -u"
if $USE_APK; then
CMD_INSTALLED="apk --repositories-file /dev/null info | cut -d' ' -f1 | sort -u"
CMD_INSTALL="apk add --allow-untrusted"
else
CMD_INSTALLED="opkg list-installed | cut -d' ' -f1 | sort -u"
CMD_INSTALL="opkg install"
fi
dotrun() {
local path="$1"
[ -f "$path" ] || { echo "file not found: $path" >&2; return 1; }
@@ -248,8 +387,8 @@ dotrun() {
echo "Executing .run file"
chmod 0755 "$path" && "$path"
else
echo "Installing .ipk file"
opkg install "$path"
echo "Installing pkg file"
$CMD_INSTALL "$path"
fi
local RET=$?
rm -f "$path"
@@ -286,6 +425,13 @@ run_records() {
echo "]"
}
if $USE_APK; then
CMD_REMOVE="apk del"
else
CMD_REMOVE="opkg --autoremove remove"
fi
unrun() {
local id="$1"
[ -s "/usr/share/istore/run-records/$id.txt" ] || { echo "record not found: $id" >&2; return 1; }
@@ -295,9 +441,9 @@ unrun() {
local round max_round=6
for round in `seq 1 $max_round`; do
if [ $round == $max_round ]; then
opkg --autoremove remove `cat "/tmp/is-root/tmp/to_unrun_$id.txt"`
$CMD_REMOVE `cat "/tmp/is-root/tmp/to_unrun_$id.txt"`
else
opkg --autoremove remove `cat "/tmp/is-root/tmp/to_unrun_$id.txt"` 2>/dev/null && break
$CMD_REMOVE `cat "/tmp/is-root/tmp/to_unrun_$id.txt"` 2>/dev/null && break
fi
done
sh -c "$CMD_INSTALLED" > "/tmp/is-root/tmp/post_unrun_$id.txt"
@@ -327,13 +473,19 @@ usage() {
echo " do_self_upgrade Upgrade iStore"
echo " arch Show libc architecture"
echo " opkg sys opkg wrap"
echo " dotrun {path} install .run/.ipk"
echo " run_records list .run/.ipk install records"
echo " del_record {id} delete .run/.ipk install record"
echo " unrun {id} Remove installed packages by .run/.ipk"
echo " dotrun {path} install .run/.ipk/.apk"
echo " run_records list .run/.ipk/.apk install records"
echo " del_record {id} delete .run/.ipk/.apk install record"
echo " unrun {id} Remove installed packages by .run/.ipk/.apk"
exit 1
}
if [[ "$action" = "do_self_upgrade" && "$0" != "$TMP_SELF_COPY" ]]; then
echo "copy self $0 to $TMP_SELF_COPY when self upgrade"
cp -af "$0" "$TMP_SELF_COPY" || exit 1
exec "$TMP_SELF_COPY" do_self_upgrade "$@" || exit 1
fi
is_init >/dev/null 2>&1
CLEANPATH="$PATH"
@@ -352,7 +504,7 @@ case $action in
;;
"install")
check_space
wrapped_in_update opkg_wrap_mirrors install "$@" && try_upgrade_depends "$1" && try_autoconf
wrapped_in_update do_install_in_mirrors "$@" && refresh_installed "$1" && try_upgrade_depends "$1" && try_autoconf
;;
"autoconf")
try_autoconf
@@ -364,10 +516,18 @@ case $action in
remove "$@" || remove "$@"
;;
"info")
opkg_wrap info "$@"
if $USE_APK; then
apk_wrap query "$@"
else
opkg_wrap info "$@"
fi
;;
"list-upgradable")
opkg_wrap list-upgradable
if $USE_APK; then
apk_wrap query --upgradable --summarize package '*'
else
opkg_wrap list-upgradable
fi
;;
"check_self_upgrade")
check_self_upgrade
@@ -394,6 +554,11 @@ case $action in
"unrun")
unrun "$1"
;;
"link_meta")
[[ -d "$META_DIR/meta" ]] || mkdir -p "$META_DIR/meta"
mkdir -p /tmp/run/istore
ln -sf "$META_DIR" /tmp/run/istore-meta
;;
*)
usage
;;
+1
View File
@@ -7,6 +7,7 @@ boot() {
local ARCH=`/bin/is-opkg arch`
generate_store_id $ARCH
generate_super_arch $ARCH
/bin/is-opkg link_meta
}
generate_store_id() {
@@ -9,4 +9,7 @@ if [ -z "`uci -q get istore.istore.hide_docker`" ] && ! which docker >/dev/null;
EOF
fi
# remove tmp keys that used for self-installation, which is not needed after installation
rm -f /etc/apk/keys/istore-tmp.pem
rm -rf /tmp/luci-indexcache /tmp/luci-modulecache
+23 -18
View File
@@ -5,11 +5,12 @@
IS_ROOT=/tmp/is-backup
APP_LIST_FILE=/etc/istore/app.list
BACKUP_CONFIG_FILE=/etc/config/istore
USE_APK=false
[[ -s /etc/apk/arch ]] && USE_APK=true
action=${1}
shift
is_init() {
mkdir -p ${IS_ROOT}
}
@@ -90,23 +91,23 @@ ipk_build() {
# if arg is NULL, use light backup, otherwise use local backup
backup() {
[ -n "$1" ] && BACKUP_PATH=$1
if [ -n "$1" ]; then
if $USE_APK; then
echo "does not support apk package manager" >&2
exit 1
fi
BACKUP_PATH=$1
fi
#1.add all istore self data to sysupgrade config file,
#sysupgrade will backup/restore it auto when flash new firmware
echo "/etc/.app_store.id" > /lib/upgrade/keep.d/luci-app-store
cat /usr/lib/opkg/info/luci-app-store.list >> /lib/upgrade/keep.d/luci-app-store
echo "/etc/rc.d/S45istore" >> /lib/upgrade/keep.d/luci-app-store
echo "/etc/istore/uci-defaults_bak" >> /lib/upgrade/keep.d/luci-app-store
echo "${APP_LIST_FILE}" >> /lib/upgrade/keep.d/luci-app-store
echo "${BACKUP_CONFIG_FILE}" >> /lib/upgrade/keep.d/luci-app-store
#write user installed package list to file
opkg_list_installed_packages "userinstalled" 2>/dev/null | sort -u > ${IS_ROOT}/user_installed_package.list
#write installed package list by istore feed to file
cat ${IS_ROOT}/user_installed_package.list | \
grep '^app-meta-' > ${IS_ROOT}/istore_installed_package.list
if $USE_APK; then
( cd /lib/apk/packages && ls *.list ) | grep '^app-meta-' | sed 's/.list$//' | sort -u > ${IS_ROOT}/istore_installed_package.list
else
#write user installed package list to file
opkg_list_installed_packages "userinstalled" 2>/dev/null | sort -u > ${IS_ROOT}/user_installed_package.list
#write installed package list by istore feed to file
cat ${IS_ROOT}/user_installed_package.list | \
grep '^app-meta-' > ${IS_ROOT}/istore_installed_package.list
fi
#if no input backup path, only back app.list
mkdir -p /etc/istore
@@ -186,6 +187,10 @@ backup() {
# if arg is NULL, use light backup, otherwise use local backup
restore() {
if [ -n "$1" ]; then
if $USE_APK; then
echo "does not support apk package manager" >&2
exit 1
fi
BACKUP_PATH_FILE=$1
else
echo "install package by ${APP_LIST_FILE}"
@@ -233,7 +238,7 @@ restore() {
get_support_backup_features() {
echo "light_backup"
#istore custom img mean support local_backup
if [ -f /etc/istore_img_flag ];then
if ! $USE_APK; then
echo "local_backup"
fi
}
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
newpath=$(echo ":$PATH:" | sed -e 's#:/usr/libexec/istore/mirror-bin:#:#g' -e 's/^:*//' -e 's/:*$//')
[ -z "$newpath" ] && newpath="/usr/sbin:/usr/bin:/sbin:/bin"
export PATH="$newpath"
# if url starts with $IS_ORIG_SERVER, replace it with $IS_MIRROR_SERVER
if [ -n "$IS_ORIG_SERVER" ] && [ -n "$IS_MIRROR_SERVER" ]; then
n=$#
while [ "$n" -gt 0 ]; do
i="$1"
shift
case "$i" in
"$IS_ORIG_SERVER"/*.apk|\
"$IS_ORIG_SERVER"/*.ipk)
i="$IS_MIRROR_SERVER/${i#$IS_ORIG_SERVER/}"
;;
esac
set -- "$@" "$i"
n=$((n - 1))
done
fi
export -n IS_ORIG_SERVER IS_MIRROR_SERVER
exec curl "$@"
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
newpath=$(echo ":$PATH:" | sed -e 's#:/usr/libexec/istore/mirror-bin:#:#g' -e 's/^:*//' -e 's/:*$//')
[ -z "$newpath" ] && newpath="/usr/sbin:/usr/bin:/sbin:/bin"
export PATH="$newpath"
# if url starts with $IS_ORIG_SERVER, replace it with $IS_MIRROR_SERVER
if [ -n "$IS_ORIG_SERVER" ] && [ -n "$IS_MIRROR_SERVER" ]; then
n=$#
while [ "$n" -gt 0 ]; do
i="$1"
shift
case "$i" in
"$IS_ORIG_SERVER"/*.apk|\
"$IS_ORIG_SERVER"/*.ipk)
i="$IS_MIRROR_SERVER/${i#$IS_ORIG_SERVER/}"
;;
esac
set -- "$@" "$i"
n=$((n - 1))
done
fi
export -n IS_ORIG_SERVER IS_MIRROR_SERVER
exec wget "$@"
+7 -3
View File
@@ -10,13 +10,17 @@ include $(TOPDIR)/feeds/luci/luci.mk
install:
[ -n "$(APP_STORE_VERSION)" ]
mkdir -p "$(DESTDIR)/www/luci-static"
mkdir -p "$(DESTDIR)/www/luci-static" "$(DESTDIR)/etc"
cp -a "$(FRONTEND_DIST)/luci-static/istore" "$(DESTDIR)/www/luci-static/"
$(SED) 's#\.js"#.js?v=$(APP_STORE_VERSION)"#g' "$(DESTDIR)/www/luci-static/istore/index.js"
mkdir -p "$(DESTDIR)/etc/opkg/keys"
echo "$(APP_STORE_VERSION)" > "$(DESTDIR)/etc/.app_store.version"
cp -a ./compat.conf "$(DESTDIR)/etc/opkg/compatfeeds.conf"
ifneq ($(USE_APK),)
cp -pR ./root-apk/* "$(DESTDIR)/"
else
cp -pR ./root-opkg/* "$(DESTDIR)/"
mkdir -p "$(DESTDIR)/etc/opkg/keys"
cp -a ./key-build.pub $(DESTDIR)/etc/opkg/keys/`$(STAGING_DIR_HOST)/bin/usign -F -p ./key-build.pub`
endif
mkdir -p "$(DESTDIR)$(LUCI_LIBRARYDIR)/i18n"
$(foreach lang,$(LUCI_LANGUAGES),$(foreach po,$(wildcard ${CURDIR}/po/$(lang)/*.po), \
po2lmo $(po) \
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEybK5eaXsrK06Thc1dbA3FC45Tlp8
8X5Z831vU6JggiV6tJiwI/B05IsdklTaZE8SYh0XCHcnXSjzRMLtarBisg==
-----END PUBLIC KEY-----
@@ -0,0 +1 @@
https://istore.istoreos.com/repo-apk/all/compat/packages.adb
+2 -2
View File
@@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.17-r1
PKG_RELEASE:=1
PKG_VERSION:=1.0.18-r1
PKG_RELEASE:=2
LUCI_TITLE:=LuCI support for SysTools
LUCI_PKGARCH:=all
@@ -4,8 +4,12 @@ FCURL="curl --fail --show-error"
curl -V >/dev/null 2>&1 || {
echo "prereq: install curl"
opkg info curl | grep -Fqm1 curl || opkg update
opkg install curl
if [ -f /etc/apk/arch ]; then
apk add curl
else
opkg info curl | grep -Fqm1 curl || opkg update
opkg install curl
fi
}
IPK=`$FCURL "$ISTORE_REPO/Packages.gz" | zcat | grep -m1 '^Filename: luci-app-store.*\.ipk$' | sed -n -e 's/^Filename: \(.\+\)$/\1/p'`
@@ -19,7 +23,7 @@ $FCURL "$ISTORE_REPO/$IPK" | tar -xzO ./data.tar.gz | tar -xzO ./bin/is-opkg > /
chmod 755 /tmp/is-opkg
/tmp/is-opkg update
# /tmp/is-opkg install taskd
/tmp/is-opkg opkg install --force-reinstall luci-lib-taskd luci-lib-xterm
/tmp/is-opkg opkg install --force-reinstall luci-app-store || exit $?
[ -s "/etc/init.d/tasks" ] || /tmp/is-opkg opkg install --force-reinstall taskd
[ -s "/usr/lib/lua/luci/cbi.lua" ] || /tmp/is-opkg opkg install luci-compat >/dev/null 2>&1
/tmp/is-opkg install --force-reinstall luci-lib-taskd luci-lib-xterm
/tmp/is-opkg install --force-reinstall luci-app-store || exit $?
[ -s "/etc/init.d/tasks" ] || /tmp/is-opkg install --force-reinstall taskd
[ -s "/usr/lib/lua/luci/cbi.lua" ] || /tmp/is-opkg install luci-compat >/dev/null 2>&1
+1 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
LUCI_NAME:=luci-app-tinyfilemanager
PKG_VERSION:=2.6
PKG_RELEASE:=2
PKG_RELEASE:=3
LUCI_TITLE:=LuCI Tiny File Manager
LUCI_DEPENDS:=+php8 +php8-cgi +php8-fastcgi +php8-fpm +php8-mod-session +php8-mod-ctype +php8-mod-fileinfo +php8-mod-zip +php8-mod-iconv +php8-mod-mbstring +coreutils-stat +zoneinfo-asia +bash +curl +tar +inotifywait
@@ -38,8 +38,6 @@ return view.extend({
return fs.exec('/etc/init.d/tinyfilemanager', ['reload'])
.catch((e) => { ui.addNotification(null, E('p', e.message), 'error') });
};
if (! has_location)
o.description = _('To enable SSL support, you may need to install <b>%s</b><br/>').format(['php-nginx']);
o = s.option(form.Flag, 'use_auth', _('Enable Authentication'));
o.rmempty = false;
+16 -12
View File
@@ -725,20 +725,24 @@ const methods = {
getCPUInfo: {
call: function() {
if (!access('/sbin/cpuinfo'))
return {};
if (access('/sbin/cpuinfo')) {
const fd = popen('/sbin/cpuinfo');
if (fd) {
let cpuinfo = fd.read('all');
if (!cpuinfo)
cpuinfo = '?';
fd.close();
const fd = popen('/sbin/cpuinfo');
if (fd) {
let cpuinfo = fd.read('all');
if (!cpuinfo)
cpuinfo = '?';
fd.close();
return { cpuinfo: cpuinfo };
} else {
return { cpuinfo: error() };
return { cpuinfo: cpuinfo };
} else {
return { cpuinfo: error() };
}
}
const cpuinfo = readfile('/proc/cpuinfo');
const m = cpuinfo ? match(cpuinfo, /(^|\n)(Model Name|model name|Hardware|Processor)[ \t]*:[ \t]*([^\n]+)/) : null;
return m?.[3] ? { cpuinfo: trim(m[3]) } : {};
}
},
+30 -6
View File
@@ -1,16 +1,36 @@
header {
@apply bg-bg sticky top-0 z-40 mb-2;
@apply bg-bg sticky top-0 z-40 mb-2 transition-colors duration-[300ms] ease-[cubic-bezier(0.4,0,0.6,1)];
& .header-content {
@apply relative flex h-14 items-center justify-between px-6 py-3 max-md:px-4 max-md:py-2;
}
[data-nav-type="mega-menu"] & {
/* Apple's layer split: the panel that animates GEOMETRY must not carry a
backdrop-filter, or every height frame re-rasterises the blur (the old
clip-path flicker). So this is a fully-opaque SOLID surface that retracts
via height — the drawer close — while the frost lives entirely on the
separate .desktop-menu-overlay curtain (opacity/visibility only). Height
is driven by --mega-menu-height (set on open, dropped on close in
menu-aurora.js); overflow-hidden clips the absolutely-positioned content
as the canvas grows and collapses.
Opacity is NOT animated — it's gated by `visibility` (like the curtain).
Fading the panel in left it half-transparent for the whole open, so the
header brand/icons showed THROUGH it until opacity settled (the flash).
Opaque-from-the-first-frame means the growing height wipes solidly down
over the header instead. The closing state keeps the panel visible and
above the curtain while height animates down, without re-enabling input. */
& .desktop-menu-container {
@apply border-hairline bg-mega-menu-bg pointer-events-none absolute inset-x-0 top-0 z-30 h-(--mega-menu-height,0) w-full overflow-hidden border-b opacity-0 shadow-xl backdrop-blur-lg backdrop-saturate-150 transition-[clip-path,opacity] duration-[250ms] ease-out [will-change:clip-path,opacity] [clip-path:inset(0_0_100%_0)] max-md:hidden;
@apply border-hairline bg-mega-menu-bg pointer-events-none invisible absolute inset-x-0 top-0 z-30 h-(--mega-menu-height,0) w-full overflow-hidden border-b shadow-xl transition-[height,visibility] duration-[300ms] ease-[cubic-bezier(0.4,0,0.6,1)] [will-change:height] motion-reduce:transition-none max-md:hidden;
&.active,
&.closing {
@apply visible;
}
&.active {
@apply pointer-events-auto opacity-100 [clip-path:inset(0)];
@apply pointer-events-auto;
}
/* Category panels live inside the container, not the nav items: the
@@ -31,7 +51,7 @@ header {
is a pure cross-fade. Below lg the side tracks drop and only the
links column remains (tablet → just the submenu). The panel scrolls
internally when its content exceeds the viewport below the header. */
@apply pointer-events-none absolute inset-x-0 top-14 mx-auto grid max-h-[calc(100dvh-3.5rem)] max-w-[72rem] translate-y-1.5 grid-cols-[12rem_minmax(0,1fr)_13rem] gap-x-8 overflow-y-auto overscroll-contain px-10 pt-7 pb-8 opacity-0 transition-[opacity,transform] duration-[250ms] ease-out max-lg:max-w-[44rem] max-lg:grid-cols-1 max-lg:px-8;
@apply pointer-events-none absolute inset-x-0 top-14 mx-auto grid max-h-[calc(100dvh-3.5rem)] max-w-[72rem] translate-y-1.5 grid-cols-[12rem_minmax(0,1fr)_13rem] gap-x-8 overflow-y-auto overscroll-contain px-10 pt-13 pb-8 opacity-0 transition-[opacity,transform] duration-[250ms] ease-out max-lg:max-w-[44rem] max-lg:grid-cols-1 max-lg:px-8;
&.active {
@apply pointer-events-auto translate-y-0 opacity-100;
@@ -136,8 +156,12 @@ header {
}
}
[data-nav-type="mega-menu"] &:has(.desktop-menu-container.active) {
@apply z-70;
/* When a category is open the bar lifts to the panel's opened colour so the
header and the mega-menu read as one continuous surface — no tonal seam
where the bar (bg) meets the panel (mega-menu-bg). */
[data-nav-type="mega-menu"]
&:has(.desktop-menu-container:is(.active, .closing)) {
@apply bg-mega-menu-bg z-70;
}
.brand {
+7 -4
View File
@@ -34,7 +34,8 @@
--danger-surface: oklch(94% 0.05 25);
--danger-surface-hover: oklch(90% 0.05 25);
--scrim: oklch(0% 0 0 / 0.6);
--mega-menu-bg: oklch(98.3% 0.003 264 / 0.66);
--mega-menu-bg: oklch(98.3% 0.003 264);
--mega-menu-scrim: oklch(0% 0 0 / 0.32);
--app-shadow-sm: 0 1px 3px oklch(0 0 0 / 0.06), 0 1px 2px oklch(0 0 0 / 0.04);
--app-shadow-md: 0 4px 16px oklch(0 0 0 / 0.08), 0 1px 3px oklch(0 0 0 / 0.04);
--app-shadow-lg: 0 12px 32px oklch(0 0 0 / 0.12);
@@ -67,15 +68,16 @@
--brand-subtle: oklch(20.52% 0.0287 219.4);
--brand-subtle-hover: oklch(24.52% 0.0287 219.4);
--focus-ring: oklch(60% 0.13 188.7 / 0.6);
--progress-start: oklch(43.18% 0.0865 166.9);
--progress-end: oklch(62.1% 0.145 189.6);
--progress-start: oklch(44.78% 0.0865 193.2);
--progress-end: oklch(0.6 0.13 188.745);
--info-surface: oklch(32% 0.05 255);
--warning-surface: oklch(33% 0.06 80);
--success-surface: oklch(30% 0.05 158);
--danger-surface: oklch(32% 0.08 22);
--danger-surface-hover: oklch(36% 0.08 22);
--scrim: oklch(0% 0 0 / 0.6);
--mega-menu-bg: oklch(23% 0.02 264 / 0.62);
--mega-menu-bg: oklch(16.5% 0.02 264);
--mega-menu-scrim: oklch(0% 0 0 / 0.5);
--app-shadow-sm: 0 4px 12px oklch(0 0 0 / 0.3);
--app-shadow-md: 0 10px 28px oklch(0 0 0 / 0.42);
--app-shadow-lg: 0 20px 48px oklch(0 0 0 / 0.55);
@@ -111,6 +113,7 @@
--color-danger-surface-hover: var(--danger-surface-hover);
--color-scrim: var(--scrim);
--color-mega-menu-bg: var(--mega-menu-bg);
--color-mega-menu-scrim: var(--mega-menu-scrim);
--shadow-sm: var(--app-shadow-sm);
--shadow-md: var(--app-shadow-md);
@@ -69,11 +69,18 @@ body.mobile-navigation-open > header {
}
[data-nav-type="mega-menu"] {
/* Apple's curtain technique: backdrop-filter is DECLARED here permanently and
never toggled. Gating the blur on `.active` snapped the page from blurred
to sharp the instant the menu closed, while the dim was still fading — the
flash. Instead opacity + visibility carry it: closing keeps the layer
visible (visibility flips only at the end of the transition) so the blur
fades out together with the dim; once hidden, visibility:hidden fully
disables the filter so an invisible full-screen blur costs nothing. */
& .desktop-menu-overlay {
@apply bg-scrim pointer-events-none fixed inset-0 z-60 opacity-0 transition-opacity duration-[250ms] max-md:hidden;
@apply bg-mega-menu-scrim pointer-events-none invisible fixed inset-0 z-60 opacity-0 backdrop-blur-lg backdrop-saturate-150 transition-[opacity,visibility] delay-[80ms] duration-[300ms] ease-[cubic-bezier(0.4,0,0.6,1)] max-md:hidden;
&.active {
@apply pointer-events-auto opacity-100 backdrop-blur-lg backdrop-saturate-150;
@apply pointer-events-auto visible opacity-100;
}
}
}
@@ -605,6 +605,44 @@ return baseclass.extend({
resizeTimer = setTimeout(applyCanvasHeight, 150);
});
// Pointer trajectory: keep a sample from ~60ms ago so a pill's mouseenter
// can tell whether the cursor is diving down into the open panel (heading
// for a submenu link) versus scanning along the pill row. Diving across
// the row would otherwise hijack the open category on every pill it grazes.
let agedPos = { x: 0, y: 0 };
let agedAt = 0;
let livePos = { x: 0, y: 0 };
header.addEventListener("mousemove", (e) => {
if (e.timeStamp - agedAt > 60) {
agedPos = livePos;
agedAt = e.timeStamp;
}
livePos = { x: e.clientX, y: e.clientY };
});
// menu-aim / "safe area": instead of the cursor's instantaneous slope
// (which reads near-horizontal exactly when it grazes the neighbouring
// pills at the very start of a dive), project the travel vector forward and
// ask whether it lands inside the OPEN panel. If the user is aiming at a
// submenu link — even the far top-left one, reached by a shallow diagonal
// across other pills — the projection falls within the panel rect, so we
// hold the current category instead of letting each grazed pill hijack it.
const isAimingIntoOpenPanel = () => {
const openNav = header.querySelector(".desktop-nav.active");
if (!openNav) return false;
const vx = livePos.x - agedPos.x;
const vy = livePos.y - agedPos.y;
if (vy <= 2) return false; // horizontal / upward → a deliberate switch
const rect = openNav.getBoundingClientRect();
if (livePos.y >= rect.top) return true; // already descending into it
// Where the current heading crosses the panel's top edge.
const xAtTop = livePos.x + (vx * (rect.top - livePos.y)) / vy;
return xAtTop >= rect.left - 24 && xAtTop <= rect.right + 24;
};
children.forEach((child) => {
const { li, nav, menuLink, hasSubmenu } = this.buildDropdownItem(
child,
@@ -626,6 +664,14 @@ return baseclass.extend({
hideTimer = null;
}
// First open waits for hover intent. Once open, scanning the pill row
// switches instantly (Apple's flyout-change feel), but a cursor diving
// diagonally toward a submenu link must NOT hijack every pill it grazes
// — so while diving we fall back to a short intent dwell that a quick
// pass-through cancels via mouseleave before it ever fires.
const isOpen = container?.classList.contains("active");
const delay = !isOpen ? 100 : isAimingIntoOpenPanel() ? 260 : 0;
showTimer = setTimeout(() => {
const wasActive = nav.classList.contains("active");
@@ -638,10 +684,11 @@ return baseclass.extend({
if (container) {
applyCanvasHeight();
container.classList.add("active");
container.classList.remove("closing");
overlay.classList.add("active");
}
nav.classList.add("active");
}, 100);
}, delay);
});
li.addEventListener("mouseleave", () => {
@@ -724,42 +771,47 @@ return baseclass.extend({
document.querySelector(".desktop-menu-overlay")?.classList.remove("active");
if (!container) return;
if (
!container.classList.contains("active") &&
!container.classList.contains("closing")
)
return;
const wasActive = container.classList.contains("active");
container.classList.add("closing");
container.classList.remove("active");
// --mega-menu-height is set on open in initMegaMenu and otherwise never
// cleared. Drop it back to the h-0 fallback once the container is fully
// hidden, so a closed menu doesn't leave invisible scrollable space
// below the header on pages shorter than the canvas.
const resetHeight = () => {
// Retract the canvas: dropping --mega-menu-height lets the container's
// height fall to its 0 fallback, and because the container transitions
// height (see _layout.css) it animates the collapse — the drawer close —
// instead of the height vanishing in one frame after the fade. overflow
// -hidden clips the content as it goes; the end state is already h-0 so no
// post-transition reset is needed.
container.style.removeProperty("--mega-menu-height");
let closeFallback = null;
const finishClosing = (event) => {
if (event?.target && event.target !== container) return;
if (event?.propertyName && event.propertyName !== "height") return;
if (closeFallback !== null) {
clearTimeout(closeFallback);
closeFallback = null;
}
container.removeEventListener("transitionend", finishClosing);
if (!container.classList.contains("active")) {
container.style.removeProperty("--mega-menu-height");
container.classList.remove("closing");
}
};
// If the menu was never open, removing "active" is a no-op: clip-path
// never changes, so transitionend/transitioncancel would never fire and
// these listeners would pile up on every header mouseleave.
if (
!wasActive ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
resetHeight();
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
finishClosing();
return;
}
const onClipPathSettled = (event) => {
if (event.target !== container || event.propertyName !== "clip-path") {
return;
}
container.removeEventListener("transitionend", onClipPathSettled);
container.removeEventListener("transitioncancel", onClipPathSettled);
resetHeight();
};
container.addEventListener("transitionend", onClipPathSettled);
container.addEventListener("transitioncancel", onClipPathSettled);
container.addEventListener("transitionend", finishClosing);
closeFallback = setTimeout(finishClosing, 350);
},
renderModeMenu(tree) {
@@ -94,6 +94,12 @@ class FakeElement {
this.listeners.set(type, listeners);
}
dispatchEvent(event) {
for (const listener of this.listeners.get(event.type) ?? []) {
listener(event);
}
}
closest(selector) {
let current = this;
@@ -169,6 +175,15 @@ class FakeElement {
if (name === "class") this.classList.replace("");
}
removeEventListener(type, listener) {
const listeners = this.listeners.get(type) ?? [];
this.listeners.set(
type,
listeners.filter((item) => item !== listener),
);
}
setAttribute(name, value) {
this.attributes.set(name, String(value));
if (name === "class") this.classList.replace(String(value));
@@ -752,3 +767,36 @@ test("renders an active group expanded when an open mobile list was initially em
assert.equal(region.getAttribute("aria-hidden"), "false");
assert.equal(region.hasAttribute("inert"), false);
});
test("keeps mega-menu stacking state while the close height transition runs", () => {
const container = new FakeElement("div", {
class: "desktop-menu-container active",
});
const overlay = new FakeElement("div", {
class: "desktop-menu-overlay active",
});
const document = createFakeDocument({
elements: {
".desktop-menu-container": container,
".desktop-menu-overlay": overlay,
},
});
const menu = loadMenuModule({ document });
container.style.setProperty("--mega-menu-height", "320px");
menu.hideDesktopNav();
assert.equal(overlay.classList.contains("active"), false);
assert.equal(container.classList.contains("active"), false);
assert.equal(container.classList.contains("closing"), true);
assert.equal(container.style.properties.has("--mega-menu-height"), false);
container.dispatchEvent({
type: "transitionend",
target: container,
propertyName: "height",
});
assert.equal(container.classList.contains("closing"), false);
});
@@ -166,6 +166,24 @@ test("mega-menu panels scroll within the viewport", () => {
assertIncludesUtilities(panel, ["overflow-y-auto", "overscroll-contain"]);
});
test("mega-menu closing state keeps the panel above the curtain", () => {
const megaMenu = getBlock(layoutStyles, '[data-nav-type="mega-menu"] &');
const container = getBlock(megaMenu, "& .desktop-menu-container");
const headerLift = layoutStyles.match(
/When a category is open[\s\S]*?(\[data-nav-type="mega-menu"\][\s\S]*?)\n\s*\.brand/,
)?.[1];
assert.match(
container,
/&\.active,\s*&\.closing\s*\{[\s\S]*@apply[^;]*\bvisible\b/,
);
assert.match(
container,
/&\.active\s*\{[\s\S]*@apply[^;]*pointer-events-auto/,
);
assert.match(headerLift ?? "", /desktop-menu-container[\s\S]*closing/);
});
test("mega-menu category masks use Tailwind arbitrary utilities", () => {
const title = getBlock(layoutStyles, "& .desktop-nav-title");
const icon = getBlock(title, "&::before");
+10 -5
View File
@@ -39,11 +39,15 @@ test("scrim uses one 60% alpha value in both modes", () => {
assert.equal(resolveMode("dark").scrim, "oklch(0% 0 0 / 0.6)");
});
test("menu backgrounds are translucent Gray glass (softer than 0.8)", () => {
test("menu panel is fully opaque (frost lives on the curtain, not the panel)", () => {
const l = resolveMode("light");
const d = resolveMode("dark");
assert.ok(l.mega_menu_bg.includes("/ 0.66"), `light: ${l.mega_menu_bg}`);
assert.ok(d.mega_menu_bg.includes("/ 0.62"), `dark: ${d.mega_menu_bg}`);
// The panel animates height, so it carries no backdrop-filter; and it is
// fully opaque so the dimmed curtain never bleeds through to grey it off the
// header tone or leak page content. The header lifts to this same colour on
// open, so any alpha here would break the one-continuous-surface look.
assert.ok(!l.mega_menu_bg.includes("/"), `light opaque: ${l.mega_menu_bg}`);
assert.ok(!d.mega_menu_bg.includes("/"), `dark opaque: ${d.mega_menu_bg}`);
});
test("light bg is the Gray canvas; surface stays pure white", () => {
@@ -57,8 +61,9 @@ test("dark brand and progress colors match the mobile navigation design", () =>
assert.equal(dark.brand, "oklch(0.6 0.13 188.745)");
assert.equal(dark.on_brand, "oklch(1 0 0)");
assert.equal(dark.progress_start, "oklch(43.18% 0.0865 166.9)");
assert.equal(dark.progress_end, "oklch(62.1% 0.145 189.6)");
// Derived from brand like light: end aliases brand, start mixes toward sunken.
assert.equal(dark.progress_start, "oklch(44.78% 0.0865 193.2)");
assert.equal(dark.progress_end, dark.brand);
});
test("dark differs from light", () => {
+15 -7
View File
@@ -49,17 +49,25 @@ test("dark layering: page < card, sunken between page and card", () => {
assert.ok(sunk >= bg && sunk < surf, `sunken not between page and card: ${d.surface_sunken}`);
});
test("mega-menu panel and overlays use stronger blur", () => {
test("mega-menu frost lives on the curtain, not the height-animating panel", () => {
const layout = read("../src/media/_layout.css");
const overlay = read("../src/media/components/_overlay.css");
// The mega-menu panel line carries the menu background + blur.
// The panel animates geometry (height) for the drawer open/close, so it must
// carry NO backdrop-filter — blurring a resizing box re-rasterises every
// frame (the old clip-path flicker). The frost moved entirely to the curtain.
const panel = layout.split("\n").find((l) => l.includes("bg-mega-menu-bg"));
assert.ok(panel?.includes("backdrop-blur-lg"), `panel blur: ${panel}`);
assert.ok(overlay.includes("max-md:backdrop-blur-lg"), "mobile overlay blur");
assert.ok(!panel?.includes("backdrop-blur"), `panel must not blur: ${panel}`);
assert.ok(
/&\.active\s*\{\s*@apply[^;]*backdrop-blur-lg/.test(overlay),
"desktop overlay blur",
panel?.includes("transition-[height,visibility]"),
`panel animates height: ${panel}`,
);
// The curtain carries the blur permanently (Apple's globalnav-curtain) and
// fades it with opacity/visibility, never snapping it on/off via .active.
assert.ok(overlay.includes("max-md:backdrop-blur-lg"), "mobile overlay blur");
const curtain = overlay
.split("\n")
.find((l) => l.includes("bg-mega-menu-scrim"));
assert.ok(curtain?.includes("backdrop-blur-lg"), `desktop curtain blur: ${curtain}`);
});
test("maincontent cards use a hairline border, not heavy shadow", () => {
@@ -94,7 +102,7 @@ test("content dropdowns stay above the closed header and below the open mega-men
const headerRule = layout.match(/^header\s*\{\s*@apply\s+([^;]+);/m)?.[1] ?? "";
const activeHeaderRule =
layout.match(
/\[data-nav-type="mega-menu"\] &:has\(\.desktop-menu-container\.active\)\s*\{\s*@apply\s+([^;]+);/,
/\[data-nav-type="mega-menu"\]\s*&:has\([\s\S]*?\.desktop-menu-container[\s\S]*?\.active[\s\S]*?\)\s*\{\s*@apply\s+([^;]+);/,
)?.[1] ?? "";
const dropdownRule = dropdown.match(/&\.dropdown\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
const messageRule = message.match(/\.alert-message\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
+25 -4
View File
@@ -20,7 +20,19 @@ export const DERIVATIONS = {
danger_surface: ["set", "danger", 0.94, 0.05],
danger_surface_hover: ["shade", "danger_surface", -0.04],
scrim: ["const", "oklch(0 0 0 / 0.6)"],
mega_menu_bg: ["alpha", "surface_overlay", 0.66],
// Fully opaque: a clean solid panel (Apple's #fafafc). Any translucency let
// the dimmed curtain bleed through, greying the panel off the header tone
// and leaking faint blurred page content into the empty columns. The header
// lifts to this same colour when the menu opens (see _layout.css) so bar and
// panel read as one continuous surface.
mega_menu_bg: ["alpha", "surface_overlay", 1],
// Mega-menu curtain: a real dimming layer. A near-page-light grey (the
// earlier #e8e8ed attempt) only blurred without darkening, so the mask read
// as absent. Black at a modest alpha actually dims the page; the now-opaque
// panel + its shadow give a clean edge, so this no longer bands the way the
// old translucent panel over a heavy scrim did. Lighter than the 0.6 modal
// scrim — it's a menu backdrop, not a dialog.
mega_menu_scrim: ["const", "oklch(0 0 0 / 0.32)"],
},
dark: {
text_muted: ["mix", "text", "bg", 0.62],
@@ -33,15 +45,24 @@ export const DERIVATIONS = {
brand_subtle: ["mix", "brand", "bg", 0.16],
brand_subtle_hover: ["shade", "brand_subtle", 0.04],
focus_ring: ["alpha", "brand", 0.6],
progress_start: ["const", "oklch(0.4318 0.0865 166.91)"],
progress_end: ["const", "oklch(0.621 0.145 189.632)"],
// Derived from brand like light — not hardcoded — so the bar tracks the
// active brand colour instead of a frozen teal.
progress_start: ["mix", "brand", "surface_sunken", 0.65],
progress_end: ["const", "var:brand"],
info_surface: ["set", "info", 0.32, 0.05],
warning_surface: ["set", "warning", 0.33, 0.06],
success_surface: ["set", "success", 0.3, 0.05],
danger_surface: ["set", "danger", 0.32, 0.08],
danger_surface_hover: ["shade", "danger_surface", 0.04],
scrim: ["const", "oklch(0 0 0 / 0.6)"],
mega_menu_bg: ["alpha", "surface_overlay", 0.62],
// Deeper than surface_overlay (23%): surface_sunken (16.5%) tracks Apple's
// opened-panel #161617 (~18.5%). Fully opaque — a clean solid panel with no
// curtain bleed; the header lifts to this colour on open (see _layout.css)
// so bar and panel are one continuous surface.
mega_menu_bg: ["alpha", "surface_sunken", 1],
// Dark curtain stays a near-black dim (Apple's rgba(0,0,0,.4)); the dark
// panel and dark page already share a tone, so this only needs to deepen.
mega_menu_scrim: ["const", "oklch(0 0 0 / 0.5)"],
},
};
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=0.12.11
PKG_RELEASE:=28
PKG_VERSION:=0.12.12
PKG_RELEASE:=29
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=
@@ -1,2 +1,2 @@
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-tracking:initial}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-sm:calc(var(--radius-base)*.25);--radius-md:calc(var(--radius-base)*.75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base)*1.5);--radius-2xl:calc(var(--radius-base)*2);--radius-3xl:calc(var(--radius-base)*3);--radius-4xl:calc(var(--radius-base)*4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--animate-spin:spin 1s linear infinite;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base)*.5);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:wheLine truncated
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-tracking:initial}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-sm:calc(var(--radius-base)*.25);--radius-md:calc(var(--radius-base)*.75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base)*1.5);--radius-2xl:calc(var(--radius-base)*2);--radius-3xl:calc(var(--radius-base)*3);--radius-4xl:calc(var(--radius-base)*4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--animate-spin:spin 1s linear infinite;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base)*.5);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:wheLine truncated
@@ -1,2 +1,2 @@
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-leading:initial;--tw-tracking:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base)*.25);--radius-md:calc(var(--radius-base)*.75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base)*1.5);--radius-2xl:calc(var(--radius-base)*2);--radius-3xl:calc(var(--radius-base)*3);--radius-4xl:calc(var(--radius-base)*4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base)*.5);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feLine truncated
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-leading:initial;--tw-tracking:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base)*.25);--radius-md:calc(var(--radius-base)*.75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base)*1.5);--radius-2xl:calc(var(--radius-base)*2);--radius-3xl:calc(var(--radius-base)*3);--radius-4xl:calc(var(--radius-base)*4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base)*.5);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feLine truncated
@@ -1 +1 @@
"use strict";"require baseclass";"require ui";return baseclass.extend({__init__(){ui.menu.load().then(tree=>this.render(tree));this.initNavigationControls();this.initUciIndicator()},initUciIndicator(){const original=ui.changes?.setIndicator;if(!original)return;ui.changes.setIndicator=function(n){original.call(this,n);document.querySelector('[data-indicator="uci-changes"]')?.setAttribute("data-count",n||0)}},initNavigationControls(){const overlay=document.querySelector("#mobile-menu-overlay");const navigationToggle=document.querySelector("#navigation-toggle");if(!navigationToggle||!overlay)return;const mobileList=overlay.querySelector("#mobile-nav-list");const desktop=window.matchMedia("(min-width: 768px)");const SIDEBAR_COLLAPSED_KEY="aurora.sidebarCollapsed";const isDesktopSidebar=()=>desktop.matches&&document.body.dataset.navType==="sidebar";const updateToggleState=expanded=>{const desktopSidebar=isDesktopSidebar();navigationToggle.classList.toggle("is-expanded",expanded&&!desktop.matches);navigationToggle.setAttribute("aria-expanded",expanded?"true":"false");navigationToggle.setAttribute("aria-controls",desktopSidebar?"sidebar-panel":"mobile-menu-overlay");const label=expanded&&!desktopSidebar?_("Close"):_("Navigation");navigationToggle.setAttribute("title",label);navigationToggle.setAttribute("aria-label",label)};const closeMobileNavigation=()=>{overlay.classList.remove("mobile-menu-open");document.body.classList.remove("mobile-navigation-open");document.body.style.overflow="";this.resetNavigationGroups(mobileList)};const getNavigationExpanded=()=>{if(isDesktopSidebar()){return!document.body.classList.contains("sidebar-collapsed")}return!desktop.matches&&overlay.classList.contains("mobile-menu-open")};const setNavigationExpanded=expanded=>{if(isDesktopSidebar()){closeMobileNavigation();const collapsed=!expanded;document.body.classList.toggle("sidebar-collapsed",collapsed);localStorage.setItem(SIDEBAR_COLLAPSED_KEY,collapsed);updateToggleState(expanded);return}if(desktop.matches){closeMobileNavigation();updateToggleState(false);return}overlay.classList.toggle("mobile-menu-open",expanded);document.body.classList.toggle("mobile-navigation-open",expanded);document.body.style.overflow=expanded?"hidden":"";if(expanded)this.expandActiveNavigationGroup(mobileList);else this.resetNavigationGroups(mobileList);updateToggleState(expanded)};const toggleNavigation=()=>setNavigationExpanded(!getNavigationExpanded());const syncNavigationState=()=>{closeMobileNavigation();if(isDesktopSidebar()){const collapsed=localStorage.getItem(SIDEBAR_COLLAPSED_KEY)==="true";document.body.classList.toggle("sidebar-collapsed",collapsed);updateToggleState(!collapsed);return}updateToggleState(false)};navigationToggle.addEventListener("click",e=>{e.stopPropagation();toggleNavigation()});overlay.addEventListener("click",e=>{if(e.target===overlay)setNavigationExpanded(false)});document.addEventListener("keydown",e=>{if(e.key==="Escape"&&overlay.classList.contains("mobile-menu-open")){setNavigationExpanded(false)}});desktop.addEventListener("change",syncNavigationState);syncNavigationState();document.addEventListener("click",e=>{const destination=e.target?.closest?.(".navigation-direct, .navigation-sublink, .mobile-nav-logout");if(destination&&overlay.contains(destination)){setNavigationExpanded(false)}})},renderMobileMenu(items){const list=document.querySelector("#mobile-nav-list");const footerAction=document.querySelector("#mobile-nav-footer-action");if(list)list.innerHTML="";if(footerAction)footerAction.innerHTML="";if(!list)return;items.forEach(item=>{if(item.isLogout){if(footerAction){footerAction.appendChild(E("a",{class:"mobile-nav-logout",href:item.href},[item.title]))}return}list.appendChild(this.renderNavigationItem(item,"mobile"))});this.bindNavigationAccordion(list)},render(tree){this.renderModeMenu(tree);if(L.env.dispatchpath.length>=3){let node=tree;let url="";for(let i=0;i<3&&node;i++){const segment=L.env.dispatchpath[i];node=node.children?.[segment];url+=(url?"/":"")+segment}if(node)this.renderTabMenu(node,url)}},renderTabMenu(tree,url,level=0){const container=document.querySelector("#tabmenu");const ul=E("ul",{class:"tabs"});const children=ui.menu.getChildren(tree);let activeNode=null;children.forEach(child=>{const isActive=L.env.dispatchpath[3+level]===child.name;ul.appendChild(E("li",{class:`tabmenu-item-${child.name}${isActive?" active":""}`},[E("a",{href:L.url(url,child.name)},[_(child.title)])]));if(isActive)activeNode=child});if(!ul.children.length)return E([]);container.appendChild(ul);container.style.display="";if(activeNode){this.renderTabMenu(activeNode,`${url}/${activeNode.name}`,level+1)}return ul},renderMainMenu(tree,url,level=0,navigationItems=null){const ul=level?E("ul",{class:"desktop-nav-list"}):document.querySelector("#topmenu");const children=ui.menu.getChildren(tree);if(level>1)return E([]);if(level===0){const navType=document.body?.dataset?.navType||"mega-menu";if(navType==="sidebar"){this.renderSidebLine truncated
"use strict";"require baseclass";"require ui";return baseclass.extend({__init__(){ui.menu.load().then(tree=>this.render(tree));this.initNavigationControls();this.initUciIndicator()},initUciIndicator(){const original=ui.changes?.setIndicator;if(!original)return;ui.changes.setIndicator=function(n){original.call(this,n);document.querySelector('[data-indicator="uci-changes"]')?.setAttribute("data-count",n||0)}},initNavigationControls(){const overlay=document.querySelector("#mobile-menu-overlay");const navigationToggle=document.querySelector("#navigation-toggle");if(!navigationToggle||!overlay)return;const mobileList=overlay.querySelector("#mobile-nav-list");const desktop=window.matchMedia("(min-width: 768px)");const SIDEBAR_COLLAPSED_KEY="aurora.sidebarCollapsed";const isDesktopSidebar=()=>desktop.matches&&document.body.dataset.navType==="sidebar";const updateToggleState=expanded=>{const desktopSidebar=isDesktopSidebar();navigationToggle.classList.toggle("is-expanded",expanded&&!desktop.matches);navigationToggle.setAttribute("aria-expanded",expanded?"true":"false");navigationToggle.setAttribute("aria-controls",desktopSidebar?"sidebar-panel":"mobile-menu-overlay");const label=expanded&&!desktopSidebar?_("Close"):_("Navigation");navigationToggle.setAttribute("title",label);navigationToggle.setAttribute("aria-label",label)};const closeMobileNavigation=()=>{overlay.classList.remove("mobile-menu-open");document.body.classList.remove("mobile-navigation-open");document.body.style.overflow="";this.resetNavigationGroups(mobileList)};const getNavigationExpanded=()=>{if(isDesktopSidebar()){return!document.body.classList.contains("sidebar-collapsed")}return!desktop.matches&&overlay.classList.contains("mobile-menu-open")};const setNavigationExpanded=expanded=>{if(isDesktopSidebar()){closeMobileNavigation();const collapsed=!expanded;document.body.classList.toggle("sidebar-collapsed",collapsed);localStorage.setItem(SIDEBAR_COLLAPSED_KEY,collapsed);updateToggleState(expanded);return}if(desktop.matches){closeMobileNavigation();updateToggleState(false);return}overlay.classList.toggle("mobile-menu-open",expanded);document.body.classList.toggle("mobile-navigation-open",expanded);document.body.style.overflow=expanded?"hidden":"";if(expanded)this.expandActiveNavigationGroup(mobileList);else this.resetNavigationGroups(mobileList);updateToggleState(expanded)};const toggleNavigation=()=>setNavigationExpanded(!getNavigationExpanded());const syncNavigationState=()=>{closeMobileNavigation();if(isDesktopSidebar()){const collapsed=localStorage.getItem(SIDEBAR_COLLAPSED_KEY)==="true";document.body.classList.toggle("sidebar-collapsed",collapsed);updateToggleState(!collapsed);return}updateToggleState(false)};navigationToggle.addEventListener("click",e=>{e.stopPropagation();toggleNavigation()});overlay.addEventListener("click",e=>{if(e.target===overlay)setNavigationExpanded(false)});document.addEventListener("keydown",e=>{if(e.key==="Escape"&&overlay.classList.contains("mobile-menu-open")){setNavigationExpanded(false)}});desktop.addEventListener("change",syncNavigationState);syncNavigationState();document.addEventListener("click",e=>{const destination=e.target?.closest?.(".navigation-direct, .navigation-sublink, .mobile-nav-logout");if(destination&&overlay.contains(destination)){setNavigationExpanded(false)}})},renderMobileMenu(items){const list=document.querySelector("#mobile-nav-list");const footerAction=document.querySelector("#mobile-nav-footer-action");if(list)list.innerHTML="";if(footerAction)footerAction.innerHTML="";if(!list)return;items.forEach(item=>{if(item.isLogout){if(footerAction){footerAction.appendChild(E("a",{class:"mobile-nav-logout",href:item.href},[item.title]))}return}list.appendChild(this.renderNavigationItem(item,"mobile"))});this.bindNavigationAccordion(list)},render(tree){this.renderModeMenu(tree);if(L.env.dispatchpath.length>=3){let node=tree;let url="";for(let i=0;i<3&&node;i++){const segment=L.env.dispatchpath[i];node=node.children?.[segment];url+=(url?"/":"")+segment}if(node)this.renderTabMenu(node,url)}},renderTabMenu(tree,url,level=0){const container=document.querySelector("#tabmenu");const ul=E("ul",{class:"tabs"});const children=ui.menu.getChildren(tree);let activeNode=null;children.forEach(child=>{const isActive=L.env.dispatchpath[3+level]===child.name;ul.appendChild(E("li",{class:`tabmenu-item-${child.name}${isActive?" active":""}`},[E("a",{href:L.url(url,child.name)},[_(child.title)])]));if(isActive)activeNode=child});if(!ul.children.length)return E([]);container.appendChild(ul);container.style.display="";if(activeNode){this.renderTabMenu(activeNode,`${url}/${activeNode.name}`,level+1)}return ul},renderMainMenu(tree,url,level=0,navigationItems=null){const ul=level?E("ul",{class:"desktop-nav-list"}):document.querySelector("#topmenu");const children=ui.menu.getChildren(tree);if(level>1)return E([]);if(level===0){const navType=document.body?.dataset?.navType||"mega-menu";if(navType==="sidebar"){this.renderSidebLine truncated
+1 -1
View File
@@ -9,7 +9,7 @@ LUCI_TITLE:=Design Theme
LUCI_DEPENDS:=
PKG_LICENSE:=Apache-2.0
PKG_VERSION:=7.1
PKG_RELEASE:=6
PKG_RELEASE:=7
LUCI_MINIFY_CSS:=
CONFIG_LUCI_CSSTIDY:=
@@ -89,18 +89,17 @@ return baseclass.extend({
},
getConditionalMenuStamp: function() {
var googleFuMode = !!document.querySelector('.navbar a[href*="/admin/services/openclash"]');
return Promise.all([
this.getFileListStamp('/usr/share/luci/menu.d', '.json'),
this.getFileListStamp('/usr/share/rpcd/acl.d', '.json'),
L.resolveDefault(fs.stat('/etc/config/wireless'), null)
L.resolveDefault(fs.stat('/etc/config/wireless'), null),
L.resolveDefault(fs.stat('/etc/config/google_fu_mode'), null)
]).then(function(stamps) {
return JSON.stringify({
google_fu_mode: googleFuMode,
menu_definitions: stamps[0],
acl_definitions: stamps[1],
wireless_config: stamps[2] ? [ stamps[2].mtime || 0, stamps[2].size || 0 ].join(':') : 'absent'
wireless_config: stamps[2] ? [ stamps[2].mtime || 0, stamps[2].size || 0 ].join(':') : 'absent',
google_fu_mode: stamps[3] ? [ stamps[3].mtime || 0, stamps[3].size || 0 ].join(':') : 'absent'
});
});
},
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
VERSION="$1"
CURDIR="$2"
BIN_PATH="$3"
if [ -d "$CURDIR/.git" ]; then
config="$CURDIR/.git/config"
else
config="$(sed "s|^gitdir:\s*|$CURDIR/|;s|$|/config|" "$CURDIR/.git")"
fi
[ -n "$(sed -En '/^\[remote /{h;:top;n;/^\[/b;s,(https?://gitcode\.(com|net)),\1,;T top;H;x;s|\n\s*|: |;p;}' "$config")" ] && {
echo -e "#!/bin/sh\necho $VERSION" > "$BIN_PATH"
}
exit 0
+30 -25
View File
@@ -1,30 +1,32 @@
# SPDX-License-Identifier: GPL-2.0
#
# Copyright (C) 2024-2026 Anya Lin <hukk1996@gmail.com>
include $(TOPDIR)/rules.mk
PKG_NAME:=mihomo
PKG_VERSION:=1.19.27
PKG_RELEASE:=21
PKG_RELEASE:=22
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
PKG_SOURCE_VERSION:=v$(PKG_VERSION)
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MIRROR_HASH:=skip
PKG_SOURCE_URL:=https://codeload.github.com/metacubex/mihomo/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
PKG_LICENSE:=GPL3.0+
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com>
PKG_MAINTAINER:=Anya Lin <hukk1996@gmail.com>
PKG_LICENSE:=GPL-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
PKG_BUILD_VERSION:=v$(PKG_VERSION)
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
GO_PKG:=github.com/metacubex/mihomo
GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
PKG_BUILD_TIME:=$(shell date -u +%FT%TZ%z)
GO_PKG_LDFLAGS_X:=\
$(GO_PKG)/constant.Version=v$(PKG_VERSION) \
$(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
GO_PKG_TAGS:=with_gvisor
GO_PKG_INSTALL_BIN_PATH:=/usr/libexec
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
@@ -32,24 +34,27 @@ include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/mihomo
SECTION:=net
CATEGORY:=Network
TITLE:=A rule based proxy in Go.
TITLE:=Another Mihomo Kernel.
URL:=https://wiki.metacubex.one
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
DEPENDS:=$(GO_ARCH_DEPENDS)
PROVIDES:=mihomo
ALTERNATIVES:=\
300:/usr/bin/mihomo:/usr/libexec/mihomo
endef
define Package/mihomo/description
Mihomo is a rule based proxy in Go.
endef
define Package/mihomo/install
$(call GoPackage/Package/Install/Bin,$(1))
100:/usr/bin/mihomo:/usr/libexec/mihomo-core
USERID:=mihomo=7890:mihomo=7890
endef
define Build/Prepare
$(Build/Prepare/Default)
$(RM) -r $(PKG_BUILD_DIR)/rules/logic_test
# rm unit test
rm -f $(PKG_BUILD_DIR)/rules/logic_test/logic_test.go
endef
define Package/mihomo/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(CURDIR)/.prepare.sh $(VERSION) $(CURDIR) $(PKG_INSTALL_DIR)/usr/bin/$(PKG_NAME)
$(INSTALL_DIR) $(1)/usr/libexec/
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/$(PKG_NAME) $(1)/usr/libexec/mihomo-core
endef
$(eval $(call GoBinPackage,mihomo))
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=momo
PKG_VERSION:=2026.06.03
PKG_RELEASE:=10
PKG_RELEASE:=11
PKG_LICENSE:=GPL-3.0+
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com>
+4 -4
View File
@@ -26,9 +26,9 @@ FIREWALL_INCLUDE_SH="$SH_DIR/firewall_include.sh"
DEBUG_SH="$SH_DIR/debug.sh"
## firewall
NFT_DIR="$HOME_DIR/firewall"
GEOIP_CN_NFT="$NFT_DIR/geoip_cn.nft"
GEOIP6_CN_NFT="$NFT_DIR/geoip6_cn.nft"
FIREWALL_DIR="$HOME_DIR/firewall"
GEOIP_CN_NFT="$FIREWALL_DIR/geoip_cn.nft"
GEOIP6_CN_NFT="$FIREWALL_DIR/geoip6_cn.nft"
## log
LOG_DIR="/var/log/momo"
@@ -63,7 +63,7 @@ get_paths() {
json_add_string firewall_include_sh "$FIREWALL_INCLUDE_SH"
json_add_string debug_sh "$DEBUG_SH"
json_add_string nft_dir "$NFT_DIR"
json_add_string firewall_dir "$FIREWALL_DIR"
json_add_string geoip_cn_nft "$GEOIP_CN_NFT"
json_add_string geoip6_cn_nft "$GEOIP6_CN_NFT"
+2 -4
View File
@@ -6,12 +6,10 @@
import { readfile } from 'fs';
import { cursor } from 'uci';
import { connect } from 'ubus';
import { get_paths, uci_bool, uci_array, get_cgroups_version, get_users, get_groups, get_cgroups } from '/etc/momo/ucode/include.uc';
import { uci_bool, uci_array, get_cgroups_version, get_users, get_groups, get_cgroups, load_profile } from '/etc/momo/ucode/include.uc';
const fw4 = require('fw4');
const paths = get_paths();
const cgroups_version = get_cgroups_version();
const users = get_users();
@@ -21,7 +19,7 @@
const uci = cursor();
const ubus = connect();
const profile = json(readfile(paths.run_profile_path));
const profile = load_profile();
uci.load('momo');
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=nexttrace
PKG_VERSION:=1.7.0
PKG_RELEASE:=6
PKG_VERSION:=1.7.1
PKG_RELEASE:=7
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/nxtrace/NTrace-core/tar.gz/v$(PKG_VERSION)?
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=opera-proxy
PKG_VERSION:=1.24.0
PKG_RELEASE:=4
PKG_RELEASE:=5
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
PKG_LICENSE:=MIT
+9 -6
View File
@@ -8,23 +8,26 @@ NAME=opera-proxy
PROG=/usr/bin/${NAME}
start_service() {
local country bind_address verbosity socks_mode timeout
local country bind_address verbosity socks_mode timeout fake_sni args
config_load opera-proxy
config_get country main country "EU"
config_get bind_address main bind_address "127.0.0.1:18080"
config_get verbosity main verbosity "20"
config_get timeout main timeout "10s"
config_get fake_sni main fake_sni
config_get args main args
config_get_bool socks_mode main socks_mode 0
procd_open_instance
procd_set_param command "$PROG"
procd_append_param command -country "$country"
procd_append_param command -bind-address "$bind_address"
procd_append_param command -verbosity "$verbosity"
procd_append_param command -timeout "$timeout"
[ -n "$country" ] && procd_append_param command -country "$country"
[ -n "$bind_address" ] && procd_append_param command -bind-address "$bind_address"
[ -n "$verbosity" ] && procd_append_param command -verbosity "$verbosity"
[ -n "$timeout" ] && procd_append_param command -timeout "$timeout"
[ "$socks_mode" = "1" ] && procd_append_param command -socks-mode
[ -n "$fake_sni" ] && procd_append_param command -fake-SNI "$fake_sni"
[ -n "$args" ] && procd_append_param command "$args"
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
procd_set_param stdout 1
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=qBittorrent
PKG_VERSION:=5.2.1
PKG_RELEASE:=2
PKG_VERSION:=5.2.2
PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/qbittorrent/qBittorrent/tar.gz/release-$(PKG_VERSION)?
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=tuic-client
PKG_VERSION:=1.8.6
PKG_RELEASE:=10
PKG_VERSION:=1.8.7
PKG_RELEASE:=11
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/Itsusinn/tuic/tar.gz/v$(PKG_VERSION)?