🦄 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 bf7d8e462c
commit 08963f8537
79 files changed
+1353 -485

No files matched your search

+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede PKG_NAME:=luci-app-daede
PKG_VERSION:=1.12.0 PKG_VERSION:=1.13.0
PKG_RELEASE:=11 PKG_RELEASE:=12
PKG_MAINTAINER:=kenzok8 PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
@@ -13,6 +13,22 @@ const DATA_PATHS = {
geosite: '/usr/share/v2ray/geosite.dat' 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 = { const HEALTH_PATHS = {
btf: '/sys/kernel/btf/vmlinux', btf: '/sys/kernel/btf/vmlinux',
netns: '/run/netns/daens' netns: '/run/netns/daens'
@@ -36,6 +52,11 @@ const CSS = [
'.dd-up-btn:hover{background:rgba(128,128,128,.12)}', '.dd-up-btn:hover{background:rgba(128,128,128,.12)}',
'.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}', '.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}',
'.dd-up-btn-primary{border-color:#4aa065;color:#4aa065}', '.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{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}', '.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}', '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({ return view.extend({
load: function() { load: function() {
// background apk update so new versions show on next poll // background apk update so new versions show on next poll
fs.exec('/usr/share/luci-app-daede/refresh-index.sh', []).catch(function() {}); fs.exec('/usr/share/luci-app-daede/refresh-index.sh', []).catch(function() {});
return Promise.all([ return Promise.all([
backend.detectBackend(), backend.detectBackend(),
uci.load('daed').catch(function() {}) uci.load('daed').catch(function() {}),
uci.load('daede').catch(function() {})
]).then(function(r) { ]).then(function(r) {
return r[0]; return r[0];
}); });
@@ -172,6 +200,49 @@ return view.extend({
return runJob('update-pkg.sh', pkg, btn, '/tmp/luci-app-daede.pkg-' + pkg + '.log'); 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 refresh = function() {
const probes = [ const probes = [
probeFile(DATA_PATHS.geoip), probeFile(DATA_PATHS.geoip),
@@ -309,16 +380,113 @@ return view.extend({
poll.add(refresh); poll.add(refresh);
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' }, [ return E('div', { 'class': 'dd-up-wrap' }, [
E('style', {}, CSS), E('style', {}, CSS),
E('div', { 'class': 'dd-card' }, [ E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Data Updates')), E('h4', { 'class': 'dd-card-title' }, _('Data Updates')),
dataBody dataBody,
geoSettings
]), ]),
E('div', { 'class': 'dd-card' }, [ E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Package Updates')), E('h4', { 'class': 'dd-card-title' }, _('Package Updates')),
pkgBody 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() { (function() {
const adv = E('div', { 'class': 'dd-adv dd-closed' }, [ const adv = E('div', { 'class': 'dd-adv dd-closed' }, [
E('div', { 'class': 'dd-adv-bar' }, [ 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." msgid "Node group \"%s\" is available on %s. Select nodes above to update it."
msgstr "" 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." msgid "Node group \"%s\" is available on %s. Select nodes above to update it."
msgstr "节点组“%s”已导入 %s。重新选择上方节点即可更新此组。" 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." msgid "Node group \"%s\" is available on %s. Select nodes above to update it."
msgstr "节点组“%s”已导入 %s。重新选择上方节点即可更新此组。" 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. # in /tmp prevents concurrent runs.
TYPE="$1" 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 case "$TYPE" in
geoip) 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" DEST="/usr/share/v2ray/geoip.dat"
;; ;;
geosite) 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" DEST="/usr/share/v2ray/geosite.dat"
;; ;;
*) *)
@@ -19,6 +24,7 @@ case "$TYPE" in
exit 64 exit 64
;; ;;
esac esac
[ -n "$URL" ] || URL="$DEF_URL"
LOCK="/tmp/luci-app-daede.${TYPE}.lock" LOCK="/tmp/luci-app-daede.${TYPE}.lock"
LOG="/tmp/luci-app-daede.${TYPE}.log" LOG="/tmp/luci-app-daede.${TYPE}.log"
@@ -19,6 +19,7 @@
"/tmp/luci-app-daede.pkg-dae.log": [ "read" ], "/tmp/luci-app-daede.pkg-dae.log": [ "read" ],
"/tmp/luci-app-daede.pkg-daed.log": [ "read" ], "/tmp/luci-app-daede.pkg-daed.log": [ "read" ],
"/tmp/luci-app-daede.pkg-luci-app-daede.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 dae": [ "exec" ],
"/bin/pidof daed": [ "exec" ], "/bin/pidof daed": [ "exec" ],
"/usr/bin/dae validate -c /tmp/dae-validate.dae": [ "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 daed": [ "exec" ],
"/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "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/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": { "ubus": {
"service": [ "list" ] "service": [ "list" ]
@@ -39,6 +41,8 @@
"file": { "file": {
"/etc/dae/config.dae": [ "write" ], "/etc/dae/config.dae": [ "write" ],
"/tmp/dae-validate.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" ], "/etc/config/daede": [ "write" ],
"/var/log/dae/dae.log": [ "write" ], "/var/log/dae/dae.log": [ "write" ],
"/var/log/daed/daed.log": [ "write" ], "/var/log/daed/daed.log": [ "write" ],
@@ -63,6 +67,8 @@
"/sbin/ip netns del daens": [ "exec" ], "/sbin/ip netns del daens": [ "exec" ],
"/usr/share/luci-app-daede/update-geo.sh geoip": [ "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/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 dae": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh daed": [ "exec" ], "/usr/share/luci-app-daede/update-pkg.sh daed": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh luci-app-daede": [ "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) // @ 下面支持填写针对server-url的TLS配置(sni, skip-cert-verify, fingerprint, certificate, private-key, alpn)
/* TLS fields */ /* 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 = s.taboption('field_general', form.Flag, 'tls', _('TLS'));
o.default = o.disabled; o.default = o.disabled;
o.validate = function(section_id, value) { o.validate = function(section_id, value) {
const type = this.section.getOption('type').formvalue(section_id); const type = this.section.getOption('type').formvalue(section_id);
let tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input'); 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_alpn = this.section.getUIElement(section_id, 'tls_alpn');
let tls_reality = this.section.getUIElement(section_id, 'tls_reality').node.querySelector('input'); let tls_reality = this.section.getUIElement(section_id, 'tls_reality').node.querySelector('input');
@@ -935,6 +930,12 @@ function renderListeners(s, uciconfig, isClient) {
} }
// Force disabled // 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)) { if (['trusttunnel'].includes(type)) {
tls_alpn.node.querySelector('input').disabled = true; tls_alpn.node.querySelector('input').disabled = true;
tls_alpn.setValue(''); tls_alpn.setValue('');
@@ -950,7 +951,13 @@ function renderListeners(s, uciconfig, isClient) {
return true; 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.modalonly = true;
o = s.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'), o = s.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'),
@@ -211,7 +211,7 @@ return view.extend({
render(data) { render(data) {
let m, s, o, ss, so; 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'); 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'), so = ss.taboption('field_tls', form.Value, 'tls_sni', _('TLS SNI'),
_('Used to verify the hostname on the returned certificates.')); _('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', 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.modalonly = true;
so = ss.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'), so = ss.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'),
+4
View File
@@ -805,6 +805,10 @@ msgstr ""
msgid "Edit node" msgid "Edit node"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/fchomo/node.js:214
msgid "Edit outbound"
msgstr ""
#: htdocs/luci-static/resources/view/fchomo/ruleset.js:138 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:138
msgid "Edit ruleset" msgid "Edit ruleset"
msgstr "" msgstr ""
+9 -5
View File
@@ -798,7 +798,7 @@ msgstr "强制覆盖 ECS"
#: htdocs/luci-static/resources/view/fchomo/client.js:1605 #: htdocs/luci-static/resources/view/fchomo/client.js:1605
msgid "EDNS Client Subnet" msgid "EDNS Client Subnet"
msgstr "" msgstr "EDNS 客户端子网"
#: htdocs/luci-static/resources/view/fchomo/global.js:433 #: htdocs/luci-static/resources/view/fchomo/global.js:433
msgid "ETag support" msgid "ETag support"
@@ -820,6 +820,10 @@ msgstr "编辑入站"
msgid "Edit node" msgid "Edit node"
msgstr "编辑节点" msgstr "编辑节点"
#: htdocs/luci-static/resources/view/fchomo/node.js:214
msgid "Edit outbound"
msgstr "编辑出站"
#: htdocs/luci-static/resources/view/fchomo/ruleset.js:138 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:138
msgid "Edit ruleset" msgid "Edit ruleset"
msgstr "编辑规则集" msgstr "编辑规则集"
@@ -1104,7 +1108,7 @@ msgstr "兜底 DNS 服务器 (用于已被投毒污染的域名)"
#: htdocs/luci-static/resources/fchomo/listeners.js:137 #: htdocs/luci-static/resources/fchomo/listeners.js:137
msgid "Firewall" msgid "Firewall"
msgstr "" msgstr "防火墙"
#: htdocs/luci-static/resources/fchomo/listeners.js:535 #: htdocs/luci-static/resources/fchomo/listeners.js:535
#: htdocs/luci-static/resources/view/fchomo/node.js:660 #: htdocs/luci-static/resources/view/fchomo/node.js:660
@@ -1335,7 +1339,7 @@ msgstr "握手超时"
#: htdocs/luci-static/resources/fchomo/listeners.js:244 #: htdocs/luci-static/resources/fchomo/listeners.js:244
msgid "Header to read real client IP from (e.g. X-Forwarded-For)" 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 #: htdocs/luci-static/resources/view/fchomo/node.js:751
msgid "Health check" msgid "Health check"
@@ -1525,7 +1529,7 @@ msgstr "单位为毫秒。留空则使用 <code>%s</code>。"
#: htdocs/luci-static/resources/view/fchomo/node.js:1350 #: htdocs/luci-static/resources/view/fchomo/node.js:1350
msgid "In milliseconds." msgid "In milliseconds."
msgstr "" msgstr "单位为毫秒。"
#: htdocs/luci-static/resources/fchomo/listeners.js:418 #: htdocs/luci-static/resources/fchomo/listeners.js:418
#: htdocs/luci-static/resources/fchomo/listeners.js:487 #: 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:1579
#: htdocs/luci-static/resources/fchomo.js:1585 #: htdocs/luci-static/resources/fchomo.js:1585
msgid "valid format: 2x, 2p, 4v" msgid "valid format: 2x, 2p, 4v"
msgstr "" msgstr "有效格式: 2x, 2p, 4v"
#: htdocs/luci-static/resources/fchomo.js:1566 #: htdocs/luci-static/resources/fchomo.js:1566
msgid "valid key length with %d characters" 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 #: htdocs/luci-static/resources/view/fchomo/client.js:1605
msgid "EDNS Client Subnet" msgid "EDNS Client Subnet"
msgstr "" msgstr "EDNS 客戶端子網"
#: htdocs/luci-static/resources/view/fchomo/global.js:433 #: htdocs/luci-static/resources/view/fchomo/global.js:433
msgid "ETag support" msgid "ETag support"
@@ -820,6 +820,10 @@ msgstr "編輯入站"
msgid "Edit node" msgid "Edit node"
msgstr "編輯節點" msgstr "編輯節點"
#: htdocs/luci-static/resources/view/fchomo/node.js:214
msgid "Edit outbound"
msgstr "編輯出站"
#: htdocs/luci-static/resources/view/fchomo/ruleset.js:138 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:138
msgid "Edit ruleset" msgid "Edit ruleset"
msgstr "編輯規則集" msgstr "編輯規則集"
@@ -1104,7 +1108,7 @@ msgstr "兜底 DNS 伺服器 (用於已被投毒汙染的網域)"
#: htdocs/luci-static/resources/fchomo/listeners.js:137 #: htdocs/luci-static/resources/fchomo/listeners.js:137
msgid "Firewall" msgid "Firewall"
msgstr "" msgstr "防火牆"
#: htdocs/luci-static/resources/fchomo/listeners.js:535 #: htdocs/luci-static/resources/fchomo/listeners.js:535
#: htdocs/luci-static/resources/view/fchomo/node.js:660 #: htdocs/luci-static/resources/view/fchomo/node.js:660
@@ -1335,7 +1339,7 @@ msgstr "握手超時"
#: htdocs/luci-static/resources/fchomo/listeners.js:244 #: htdocs/luci-static/resources/fchomo/listeners.js:244
msgid "Header to read real client IP from (e.g. X-Forwarded-For)" 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 #: htdocs/luci-static/resources/view/fchomo/node.js:751
msgid "Health check" msgid "Health check"
@@ -1525,7 +1529,7 @@ msgstr "單位為毫秒。留空則使用 <code>%s</code>。"
#: htdocs/luci-static/resources/view/fchomo/node.js:1350 #: htdocs/luci-static/resources/view/fchomo/node.js:1350
msgid "In milliseconds." msgid "In milliseconds."
msgstr "" msgstr "單位為毫秒。"
#: htdocs/luci-static/resources/fchomo/listeners.js:418 #: htdocs/luci-static/resources/fchomo/listeners.js:418
#: htdocs/luci-static/resources/fchomo/listeners.js:487 #: 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:1579
#: htdocs/luci-static/resources/fchomo.js:1585 #: htdocs/luci-static/resources/fchomo.js:1585
msgid "valid format: 2x, 2p, 4v" msgid "valid format: 2x, 2p, 4v"
msgstr "" msgstr "有效格式: 2x, 2p, 4v"
#: htdocs/luci-static/resources/fchomo.js:1566 #: htdocs/luci-static/resources/fchomo.js:1566
msgid "valid key length with %d characters" msgid "valid key length with %d characters"
+1 -1
View File
@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_VERSION:=1.2.1 PKG_VERSION:=1.2.1
PKG_RELEASE:=6 PKG_RELEASE:=7
LUCI_TITLE:=LuCI Support for momo LUCI_TITLE:=LuCI Support for momo
LUCI_DEPENDS:=+luci-base +momo LUCI_DEPENDS:=+luci-base +momo
@@ -19,6 +19,12 @@ const callRCInit = rpc.declare({
expect: { '': {} } expect: { '': {} }
}); });
const callFileWrite = rpc.declare({
object: 'file',
method: 'write',
params: ['path', 'data', 'append', 'mode']
});
const callMomoGetPaths = rpc.declare({ const callMomoGetPaths = rpc.declare({
object: 'luci.momo', object: 'luci.momo',
method: 'get_paths', method: 'get_paths',
@@ -81,6 +87,31 @@ return baseclass.extend({
return callRCInit('momo', 'restart'); 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 () { version: function () {
return callMomoVersion(); return callMomoVersion();
}, },
@@ -138,12 +169,12 @@ return baseclass.extend({
clearAppLog: async function () { clearAppLog: async function () {
const paths = await this.getPaths(); const paths = await this.getPaths();
return fs.write(paths.app_log_path); return this.writefile(paths.app_log_path);
}, },
clearCoreLog: async function () { clearCoreLog: async function () {
const paths = await this.getPaths(); const paths = await this.getPaths();
return fs.write(paths.core_log_path); return this.writefile(paths.core_log_path);
}, },
debug: function () { debug: function () {
@@ -51,11 +51,11 @@ return view.extend({
o.wrap = false; o.wrap = false;
o.write = function (section_id, formvalue) { o.write = function (section_id, formvalue) {
const path = m.lookupOption('_file', section_id)[0].formvalue(section_id); 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) { o.remove = function (section_id) {
const path = m.lookupOption('_file', section_id)[0].formvalue(section_id); const path = m.lookupOption('_file', section_id)[0].formvalue(section_id);
return fs.write(path); return momo.writefile(path);
}; };
return m.render(); return m.render();
@@ -2,8 +2,8 @@
'use strict'; 'use strict';
import { access, popen, readfile } from 'fs'; import { popen } from 'fs';
import { get_paths, merge_exists, get_users, get_groups, get_cgroups, load_profile, save_profile } from '/etc/momo/ucode/include.uc'; import { get_paths, merge_exists, get_users, get_groups, get_cgroups, load_profile } from '/etc/momo/ucode/include.uc';
const methods = { const methods = {
get_paths: { get_paths: {
+1 -1
View File
@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_VERSION:=1.26.1 PKG_VERSION:=1.26.1
PKG_RELEASE:=5 PKG_RELEASE:=6
LUCI_TITLE:=LuCI Support for nikki LUCI_TITLE:=LuCI Support for nikki
LUCI_DEPENDS:=+luci-base +nikki LUCI_DEPENDS:=+luci-base +nikki
@@ -19,6 +19,12 @@ const callRCInit = rpc.declare({
expect: { '': {} } expect: { '': {} }
}); });
const callFileWrite = rpc.declare({
object: 'file',
method: 'write',
params: ['path', 'data', 'append', 'mode']
});
const callNikkiVersion = rpc.declare({ const callNikkiVersion = rpc.declare({
object: 'luci.nikki', object: 'luci.nikki',
method: 'version', method: 'version',
@@ -98,6 +104,31 @@ return baseclass.extend({
return callRCInit('nikki', 'restart'); 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 () { version: function () {
return callNikkiVersion(); return callNikkiVersion();
}, },
@@ -183,11 +214,11 @@ return baseclass.extend({
}, },
clearAppLog: function () { clearAppLog: function () {
return fs.write(this.appLogPath); return this.writefile(this.appLogPath, '');
}, },
clearCoreLog: function () { clearCoreLog: function () {
return fs.write(this.coreLogPath); return this.writefile(this.coreLogPath, '');
}, },
debug: function () { debug: function () {
@@ -62,11 +62,11 @@ return view.extend({
o.wrap = false; o.wrap = false;
o.write = function (section_id, formvalue) { o.write = function (section_id, formvalue) {
const path = m.lookupOption('_file', section_id)[0].formvalue(section_id); 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) { o.remove = function (section_id) {
const path = m.lookupOption('_file', section_id)[0].formvalue(section_id); const path = m.lookupOption('_file', section_id)[0].formvalue(section_id);
return fs.write(path); return nikki.writefile(path);
}; };
return m.render(); return m.render();
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.6.3 PKG_VERSION:=26.6.16
PKG_RELEASE:=54 PKG_RELEASE:=55
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -320,15 +320,6 @@ o.rewrite_option = "method"
for a, t in ipairs(ss_method_list) do o:value(t) end for a, t in ipairs(ss_method_list) do o:value(t) end
o:depends({ [_n("protocol")] = "shadowsocks" }) 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 = s:option(ListValue, _n("flow"), translate("flow"))
o.default = "" o.default = ""
o:value("", translate("Disable")) o:value("", translate("Disable"))
@@ -420,7 +411,6 @@ o:value("http/1.1")
o:value("h2,http/1.1") o:value("h2,http/1.1")
o:value("h3,h2,http/1.1") o:value("h3,h2,http/1.1")
o:depends({ [_n("tls")] = true, [_n("reality")] = false }) o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
-- o = s:option(Value, _n("minversion"), translate("minversion")) -- o = s:option(Value, _n("minversion"), translate("minversion"))
-- o.default = "1.3" -- 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 = s:option(Flag, _n("tcpMptcp"), "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration."))
o.default = 0 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 = 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("", translate("Auto"))
o:value("tcp", "TCP") 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 for a, t in ipairs(x_ss_method_list) do o:value(t) end
o:depends({ [_n("protocol")] = "shadowsocks" }) 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 = s:option(ListValue, _n("ss_network"), translate("Transport"))
o.default = "tcp,udp" o.default = "tcp,udp"
o:value("tcp", "TCP") o:value("tcp", "TCP")
@@ -232,7 +229,6 @@ o:value("http/1.1")
o:value("h2,http/1.1") o:value("h2,http/1.1")
o:value("h3,h2,http/1.1") o:value("h3,h2,http/1.1")
o:depends({ [_n("tls")] = true, [_n("reality")] = false }) 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 = s:option(Flag, _n("use_mldsa65Seed"), translate("ML-DSA-65"))
o.default = "0" o.default = "0"
+10 -14
View File
@@ -1611,27 +1611,23 @@ end
function parse_realm_uri(uri) function parse_realm_uri(uri)
if type(uri) ~= "string" then return nil end if type(uri) ~= "string" then return nil end
-- realm://token@server/realm_id?query -- realm[+http]://token@server/realm_id?query
local token, server_url, realm_id, query = trim(uri):match("^realm://([^@]+)@([^/]+)/([^?]*)%??(.*)$") local scheme, token, server_url, realm_id, query = trim(uri):match("^(realm%+http|realm)://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not token or not server_url or not realm_id then return nil end if not scheme or not token or not server_url or not realm_id then return nil end
realm_id = realm_id:gsub("/+$", "") realm_id = realm_id:gsub("/+$", "")
local realm = { local realm = {
scheme = scheme,
token = token, token = token,
server_url = server_url, server_url = server_url,
realm_id = realm_id realm_id = realm_id
} }
-- 解析 query 中的 stun= -- Parse stun= in the query
if query and query ~= "" then local stun_servers
local stun_servers = {} for value in (query or ""):gmatch("[?&]?[Ss][Tt][Uu][Nn]=([^&]+)") do
for key, value in query:gmatch("([^&=?]+)=([^&]+)") do if not stun_servers then stun_servers = {} end
if key == "stun" and value ~= "" then stun_servers[#stun_servers + 1] = value
stun_servers[#stun_servers + 1] = value
end
end
if #stun_servers > 0 then
realm.stun_servers = stun_servers
end
end end
realm.stun_servers = stun_servers
return realm return realm
end end
@@ -64,7 +64,8 @@ function gen_config(var)
server_host = api.get_ipv6_full(server_host) server_host = api.get_ipv6_full(server_host)
end 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 = { local config = {
server = (function() server = (function()
@@ -617,8 +617,9 @@ function gen_outbound(flag, node, tag, proxy_table)
result.server_port = nil result.server_port = nil
local realm = api.parse_realm_uri(node.hysteria2_realm_url) local realm = api.parse_realm_uri(node.hysteria2_realm_url)
if realm then 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.stun_servers = realm.stun_servers or node.hysteria2_realm_stun
realm.scheme = nil
return realm return realm
end end
return nil return nil
@@ -959,8 +960,9 @@ function gen_config_server(node)
realm = node.hysteria2_realms and (function() realm = node.hysteria2_realms and (function()
local realm = api.parse_realm_uri(node.hysteria2_realm_url) local realm = api.parse_realm_uri(node.hysteria2_realm_url)
if realm then 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.stun_servers = realm.stun_servers or node.hysteria2_realm_stun
realm.scheme = nil
realm.stun_domain_resolver = "direct" realm.stun_domain_resolver = "direct"
return realm return realm
end end
@@ -1797,13 +1799,12 @@ function gen_config(var)
end end
end end
table.insert(route.rules, {
action = "route",
ip_is_private = true,
outbound = "direct"
})
if COMMON.default_outbound_tag then 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 route.final = COMMON.default_outbound_tag
end end
@@ -285,21 +285,22 @@ function gen_outbound(flag, node, tag, proxy_table)
local finalmask = {} local finalmask = {}
local TP = node.transport local TP = node.transport
if TP == "mkcp" then if TP == "mkcp" then
local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat", local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat",
dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"} dtls = "dtls", wireguard = "wireguard", dns = "dns"}
local udp = {} local udp = {}
if node.mkcp_guise and node.mkcp_guise ~= "none" then 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 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 end
udp[#udp+1] = g udp[#udp+1] = g
end 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 if node.mkcp_seed and node.mkcp_seed ~= "" then
c.settings = { password = node.mkcp_seed } s.settings = { value = node.mkcp_seed }
end end
udp[#udp+1] = c udp[#udp+1] = s
finalmask.udp = udp finalmask.udp = udp
elseif TP == "hysteria" then elseif TP == "hysteria" then
local udp = {} 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 realm = api.parse_realm_uri(node.hysteria2_realm_url)
local url, stun local url, stun
if realm then if realm then
if realm.token and realm.server_url and realm.realm_id then url = realm.scheme .. "://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
url = "realm://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
end
stun = realm.stun_servers or node.hysteria2_realm_stun stun = realm.stun_servers or node.hysteria2_realm_stun
end end
local r = { local r = {
@@ -386,43 +385,18 @@ function gen_outbound(flag, node, tag, proxy_table)
end)() end)()
} or nil, } or nil,
settings = { settings = {
vnext = (node.protocol == "vmess" or node.protocol == "vless") and { address = (node.protocol == "wireguard") and node.wireguard_local_address or node.address,
{ port = tonumber(node.port),
address = node.address, id = (node.protocol == "vmess" or node.protocol == "vless") and node.uuid or nil,
port = tonumber(node.port), encryption = (node.protocol == "vless") and ((node.encryption and node.encryption ~= "") and node.encryption or "none") or nil,
users = { 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,
id = node.uuid, security = (node.protocol == "vmess") and node.security or nil,
level = 0, user = (node.protocol == "socks" or node.protocol == "http") and node.username or nil,
security = (node.protocol == "vmess") and node.security or nil, pass = (node.protocol == "socks" or node.protocol == "http") and node.password or nil,
testpre = (node.protocol == "vless") and tonumber(node.preconns) or nil, password = (node.protocol == "shadowsocks" or node.protocol == "trojan") and node.password or nil,
encryption = (node.protocol == "vless") and ((node.encryption and node.encryption ~= "") and node.encryption or "none") or nil, method = (node.protocol == "shadowsocks") and ((node.method == "chacha20-ietf-poly1305" and "chacha20-poly1305") or
flow = (node.protocol == "vless" (node.method == "xchacha20-ietf-poly1305" and "xchacha20-poly1305") or (node.method ~= "" and node.method) or nil) or nil,
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,
secretKey = (node.protocol == "wireguard") and node.wireguard_secret_key or nil, secretKey = (node.protocol == "wireguard") and node.wireguard_secret_key or nil,
peers = (node.protocol == "wireguard") and { peers = (node.protocol == "wireguard") and {
{ {
@@ -434,13 +408,13 @@ function gen_outbound(flag, node, tag, proxy_table)
} or nil, } or nil,
mtu = (node.protocol == "wireguard" and node.wireguard_mtu) and tonumber(node.wireguard_mtu) 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, 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 if node.protocol == "wireguard" then
result.settings.kernelMode = false result.settings.noKernelTun = true
if node.finalmask and node.finalmask ~= "" then if node.finalmask and node.finalmask ~= "" then
local ok, fm = pcall(jsonc.parse, api.base64Decode(node.finalmask)) local ok, fm = pcall(jsonc.parse, api.base64Decode(node.finalmask))
if ok and type(fm) == "table" then if ok and type(fm) == "table" then
@@ -561,7 +535,6 @@ function gen_config_server(node)
settings = { settings = {
method = node.method, method = node.method,
password = node.password, password = node.password,
ivCheck = ("1" == node.iv_check) and true or false,
network = node.ss_network or "TCP,UDP" network = node.ss_network or "TCP,UDP"
} }
elseif node.protocol == "trojan" then elseif node.protocol == "trojan" then
@@ -740,21 +713,22 @@ function gen_config_server(node)
finalmask = (function() finalmask = (function()
local finalmask = {} local finalmask = {}
if node.transport == "mkcp" then if node.transport == "mkcp" then
local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat", local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat",
dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"} dtls = "dtls", wireguard = "wireguard", dns = "dns"}
local udp = {} local udp = {}
if node.mkcp_guise and node.mkcp_guise ~= "none" then 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 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 end
udp[#udp+1] = g udp[#udp+1] = g
end 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 if node.mkcp_seed and node.mkcp_seed ~= "" then
c.settings = { password = node.mkcp_seed } s.settings = { value = node.mkcp_seed }
end end
udp[#udp+1] = c udp[#udp+1] = s
finalmask.udp = udp finalmask.udp = udp
elseif node.transport == "hysteria" then elseif node.transport == "hysteria" then
local udp = {} local udp = {}
@@ -772,9 +746,7 @@ function gen_config_server(node)
local realm = api.parse_realm_uri(node.hysteria2_realm_url) local realm = api.parse_realm_uri(node.hysteria2_realm_url)
local url, stun local url, stun
if realm then if realm then
if realm.token and realm.server_url and realm.realm_id then url = realm.scheme .. "://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
url = "realm://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
end
stun = realm.stun_servers or node.hysteria2_realm_stun stun = realm.stun_servers or node.hysteria2_realm_stun
end end
local r = { local r = {
@@ -1524,7 +1496,7 @@ function gen_config(var)
outboundTag = default_outboundTag, outboundTag = default_outboundTag,
} }
if node.domainStrategy == "IPIfNonMatch" then if node.domainStrategy == "IPIfNonMatch" then
rule.ip = { "0.0.0.0/0", "::/0" } rule.port = "1-65535"
else else
rule.network = "tcp,udp" rule.network = "tcp,udp"
end end
@@ -1719,6 +1691,7 @@ function gen_config(var)
end end
end end
local dns_outbound
if dns_listen_port then if dns_listen_port then
table.insert(inbounds, { table.insert(inbounds, {
listen = "127.0.0.1", listen = "127.0.0.1",
@@ -1775,12 +1748,13 @@ function gen_config(var)
} }
} }
local type_dns = direct_type_dns local type_dns = direct_type_dns
table.insert(outbounds, { dns_outbound = {
tag = "dns-out", tag = "dns-out",
protocol = "dns", protocol = "dns",
proxySettings = type_dns.proxySettings, proxySettings = type_dns.proxySettings,
settings = type_dns.settings settings = type_dns.settings
}) }
table.insert(outbounds, dns_outbound)
table.insert(routing.rules, 1, { table.insert(routing.rules, 1, {
inboundTag = { inboundTag = {
"dns-in" "dns-in"
@@ -1815,6 +1789,7 @@ function gen_config(var)
end end
-- Shunt rule DNS logic -- Shunt rule DNS logic
local dns_out_rules = {}
if dns_domain_rules and #dns_domain_rules > 0 then if dns_domain_rules and #dns_domain_rules > 0 then
for index, value in ipairs(dns_domain_rules) do for index, value in ipairs(dns_domain_rules) do
if value.domain and value.outboundTag then if value.domain and value.outboundTag then
@@ -1834,19 +1809,19 @@ function gen_config(var)
end end
end 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_outboundTag == "blackhole" then
if dns_block then table.insert(dns_out_rules, {
local dns_block_mode = "Hosts" -- use the Hosts mode to block. action = "return",
if dns_block_mode == "Hosts" then rCode = 0,
for d_i, d_k in ipairs(value.domain) do domain = api.clone(value.domain)
dns.hosts[d_k] = "0.0.0.0" })
end dns_server = nil
dns_server = nil else
end table.insert(dns_out_rules, {
else action = "hijack",
dns_server = nil qType = "1,28",
end domain = api.clone(value.domain)
})
end end
if dns_server then if dns_server then
dns_server.finalQuery = true dns_server.finalQuery = true
@@ -1861,6 +1836,11 @@ function gen_config(var)
end end
end 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
end end
-9
View File
@@ -1251,9 +1251,6 @@ msgstr "پاک کردن گزارش‌ها"
msgid "Password" msgid "Password"
msgstr "رمز عبور" msgstr "رمز عبور"
msgid "IV Check"
msgstr "بررسی IV"
msgid "UDP over TCP" msgid "UDP over TCP"
msgstr "UDP بر روی TCP" msgstr "UDP بر روی TCP"
@@ -1266,12 +1263,6 @@ msgstr "پورت محلی"
msgid "Fast Open" msgid "Fast Open"
msgstr "بازگشایی سریع (Fast Open)" msgstr "بازگشایی سریع (Fast Open)"
msgid "Pre-connections"
msgstr "پیش‌اتصالات"
msgid "Number of early established connections to reduce latency."
msgstr "تعداد اتصالات از پیش برقرار شده برای کاهش تاخیر."
msgid "plugin" msgid "plugin"
msgstr "افزونه" msgstr "افزونه"
-9
View File
@@ -1252,9 +1252,6 @@ msgstr "Очистить журнал"
msgid "Password" msgid "Password"
msgstr "Пароль" msgstr "Пароль"
msgid "IV Check"
msgstr "Проверка IV"
msgid "UDP over TCP" msgid "UDP over TCP"
msgstr "UDP через TCP" msgstr "UDP через TCP"
@@ -1267,12 +1264,6 @@ msgstr "Локальный порт"
msgid "Fast Open" msgid "Fast Open"
msgstr "Fast Open" msgstr "Fast Open"
msgid "Pre-connections"
msgstr "Предварительные соединения"
msgid "Number of early established connections to reduce latency."
msgstr "Количество заранее установленных соединений для снижения задержки."
msgid "plugin" msgid "plugin"
msgstr "Плагин" msgstr "Плагин"
-9
View File
@@ -1243,9 +1243,6 @@ msgstr "清空日志"
msgid "Password" msgid "Password"
msgstr "密码" msgstr "密码"
msgid "IV Check"
msgstr "IV 检查"
msgid "UDP over TCP" msgid "UDP over TCP"
msgstr "TCP 封装 UDP" msgstr "TCP 封装 UDP"
@@ -1258,12 +1255,6 @@ msgstr "本地端口"
msgid "Fast Open" msgid "Fast Open"
msgstr "快速打开" msgstr "快速打开"
msgid "Pre-connections"
msgstr "预连接"
msgid "Number of early established connections to reduce latency."
msgstr "预连接的数量,用于降低延迟。"
msgid "plugin" msgid "plugin"
msgstr "插件" msgstr "插件"
-9
View File
@@ -1243,9 +1243,6 @@ msgstr "清空日誌"
msgid "Password" msgid "Password"
msgstr "密碼" msgstr "密碼"
msgid "IV Check"
msgstr "IV 檢查"
msgid "UDP over TCP" msgid "UDP over TCP"
msgstr "TCP 封装 UDP" msgstr "TCP 封装 UDP"
@@ -1258,12 +1255,6 @@ msgstr "本地端口"
msgid "Fast Open" msgid "Fast Open"
msgstr "快速打開" msgstr "快速打開"
msgid "Pre-connections"
msgstr "預連接"
msgid "Number of early established connections to reduce latency."
msgstr "預連接的數量,用於降低延遲。"
msgid "plugin" msgid "plugin"
msgstr "插件" msgstr "插件"
@@ -2,6 +2,15 @@
START=99 START=99
boot_func() {
sleep 90
restart
}
boot() {
boot_func >/dev/null 2>&1 &
}
start() { start() {
lua /usr/lib/lua/luci/passwall2/server_app.lua start lua /usr/lib/lua/luci/passwall2/server_app.lua start
} }
@@ -88,6 +88,23 @@ config shunt_rules 'Russia'
option ip_list 'geoip:ru 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' #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' config nodes 'examplenode'
option remarks 'Example' option remarks 'Example'
option type 'Xray' option type 'Xray'
@@ -104,6 +121,7 @@ config nodes 'rulenode'
option remarks 'rulenode' option remarks 'rulenode'
option type 'Xray' option type 'Xray'
option protocol '_shunt' option protocol '_shunt'
option PrivateIP '_direct'
option default_node 'examplenode' option default_node 'examplenode'
option domainStrategy 'IPOnDemand' option domainStrategy 'IPOnDemand'
option domainMatcher 'hybrid' option domainMatcher 'hybrid'
@@ -1937,7 +1937,6 @@ local function curl(url, file, ua, mode)
ua = (ua == "passwall2") and ("passwall2/" .. api.get_version()) or ua ua = (ua == "passwall2") and ("passwall2/" .. api.get_version()) or ua
curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"' curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"'
end end
curl_args[#curl_args + 1] = get_headers()
local return_code, result local return_code, result
if mode == "direct" then if mode == "direct" then
return_code, result = api.curl_direct(url, file, curl_args) 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) return return_code, tonumber(result)
end 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) local function truncate_nodes(group)
for _, config in pairs(CONFIG) do for _, config in pairs(CONFIG) do
if config.currentNodes and #config.currentNodes > 0 then if config.currentNodes and #config.currentNodes > 0 then
+8 -5
View File
@@ -5,17 +5,19 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI based ipk store LUCI_TITLE:=LuCI based ipk/apk store
LUCI_DESCRIPTION:=luci-app-store is a ipk store developed by LinkEase team LUCI_DESCRIPTION:=luci-app-store is a ipk/apk store developed by LinkEase team
LUCI_DEPENDS:=@(x86_64||aarch64) +curl +opkg +luci-lib-ipkg +tar +libuci-lua +mount-utils +luci-lib-taskd 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_EXTRA_DEPENDS:=luci-lib-taskd (>=1.0.19)
LUCI_PKGARCH:=all 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 MUST be empty for luci.mk
PKG_RELEASE:= PKG_RELEASE:=
ISTORE_UI_VERSION:=0.1.32 ISTORE_UI_VERSION:=0.2.0
ISTORE_UI_RELEASE:=1 ISTORE_UI_RELEASE:=1
PKG_HASH:=skip 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= 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+= SED="$(SED)"
TARGET_CONFIGURE_OPTS+= USE_APK="$(CONFIG_USE_APK)"
define Package/luci-app-store/conffiles define Package/luci-app-store/conffiles
/etc/.app_store.id /etc/.app_store.id
+23 -21
View File
@@ -166,6 +166,11 @@ end
function store_index() function store_index()
local fs = require "nixio.fs" local fs = require "nixio.fs"
local features = { "_lua_force_array_" } 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 if fs.access("/usr/libexec/istore/backup") then
features[#features+1] = "backup" features[#features+1] = "backup"
end end
@@ -251,13 +256,11 @@ function validate_pkgname(val)
end end
local function get_installed_and_cache() 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 cachedir = "/tmp/cache/istore"
local cachefile = cachedir .. "/installed.json" local cachefile = cachedir .. "/installed.json"
local metapkgpre = "app-meta-"
local nixio = require "nixio" local nixio = require "nixio"
local fs = require "nixio.fs" local fs = require "nixio.fs"
local ipkg = require "luci.model.ipkg"
local jsonc = require "luci.jsonc" local jsonc = require "luci.jsonc"
local result = {} local result = {}
local lock, msg = flock("/var/lock/istore-installed.lock", "lock") local lock, msg = flock("/var/lock/istore-installed.lock", "lock")
@@ -274,7 +277,8 @@ local function get_installed_and_cache()
local pkg local pkg
for pkg in itr do for pkg in itr do
if pkg:match("^.*%.json$") then if pkg:match("^.*%.json$") then
local metadata = fs.readfile(metadir .. "/" .. pkg) local metafile = metadir .. "/" .. pkg
local metadata = fs.readfile(metafile)
if metadata ~= nil then if metadata ~= nil then
local meta = jsonc.parse(metadata) local meta = jsonc.parse(metadata)
if meta == nil then if meta == nil then
@@ -291,15 +295,10 @@ local function get_installed_and_cache()
} }
end end
local time = nil local time = nil
local metapkg = metapkgpre .. meta.name local istat = fs.stat(metafile)
local status = ipkg.status(metapkg) if istat ~= nil then
if next(status) ~= nil then time = istat["mtime"]
time = status[metapkg]["Installed-Time"]
else else
local istat = fs.stat("/usr/lib/opkg/info/" .. metapkg .. ".list")
if istat ~= nil then
time = istat["mtime"]
end
cacheable = false cacheable = false
end end
if time ~= nil then if time ~= nil then
@@ -313,7 +312,7 @@ local function get_installed_and_cache()
result = data result = data
if cacheable then if cacheable then
fs.mkdirr(cachedir) 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) local mfile, code, msg = nixio.open(cachefile, oflags)
mfile:writeall(jsonc.stringify(result)) mfile:writeall(jsonc.stringify(result))
mfile:close() mfile:close()
@@ -327,11 +326,10 @@ local function get_installed_and_cache()
end end
function store_action(param) function store_action(param)
local metadir = "/usr/lib/opkg/meta" local metadir = "/tmp/run/istore-meta/meta"
local metapkgpre = "app-meta-" local metapkgpre = "app-meta-"
local code, out, err, ret local code, out, err, ret
local fs = require "nixio.fs" local fs = require "nixio.fs"
local ipkg = require "luci.model.ipkg"
local jsonc = require "luci.jsonc" local jsonc = require "luci.jsonc"
local json_parse = jsonc.parse local json_parse = jsonc.parse
local action = param.action or "" local action = param.action or ""
@@ -342,18 +340,22 @@ function store_action(param)
luci.http.status(400, "Bad Request") luci.http.status(400, "Bad Request")
return return
end end
local metapkg = metapkgpre .. pkg
local meta = {} local meta = {}
local metadata = fs.readfile(metadir .. "/" .. pkg .. ".json") local metafile = metadir .. "/" .. pkg .. ".json"
local metadata = fs.readfile(metafile)
if metadata ~= nil then if metadata ~= nil then
meta = json_parse(metadata) or {} meta = json_parse(metadata) or {}
end end
meta.installed = false meta.installed = false
local status = ipkg.status(metapkg) local time = nil
if next(status) ~= nil then local istat = fs.stat(metafile)
if istat ~= nil then
time = istat["mtime"]
end
if time ~= nil then
meta.installed=true meta.installed=true
meta.time=tonumber(status[metapkg]["Installed-Time"]) meta.time=tonumber(time)
end end
ret = meta ret = meta
@@ -571,7 +573,7 @@ function entrysh()
errors[#errors+1] = {app=meta.name, code=500, msg="json parse failed: " .. o} errors[#errors+1] = {app=meta.name, code=500, msg="json parse failed: " .. o}
else else
results[#results+1] = status 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) local mfile, code, msg = nixio.open(cachefile, oflags)
mfile:writeall(jsonc.stringify(status)) mfile:writeall(jsonc.stringify(status))
mfile:close() mfile:close()
+221 -56
View File
@@ -10,25 +10,46 @@ if [ "${action:0:9}" = "AUTOCONF=" ]; then
fi fi
IS_ROOT=/tmp/is-root IS_ROOT=/tmp/is-root
DL_DIR=${IS_ROOT}/tmp/dl TMP_SELF_COPY=/var/run/cloned-is-opkg
LISTS_DIR_O=/tmp/opkg-lists USE_APK=false
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"
DISABLE_MIRROR=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 # for istore self upgrade
ISTORE_PKG=luci-app-store ISTORE_PKG=luci-app-store
ISTORE_DEP_PKGS="luci-lib-taskd luci-lib-xterm taskd" ISTORE_DEP_PKGS="luci-lib-taskd luci-lib-xterm taskd"
ISTORE_INDEX=https://istore.istoreos.com/repo/all/store/Packages.gz
NEWLINE=$'\n' NEWLINE=$'\n'
is_init() { 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 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 [ -e ${IS_ROOT}/var/lock ] || ln -s /var/lock ${IS_ROOT}/var/lock
} }
apk_wrap() {
APK_CONFIG=${APK_CONF} apk "$@"
}
opkg_wrap() { opkg_wrap() {
OPKG_CONF_DIR=${OPKG_CONF_DIR} opkg -f ${IS_ROOT}/etc/opkg.conf "$@" 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 server
local file
if ! $DISABLE_MIRROR; then if ! $DISABLE_MIRROR; then
local newpath="/usr/libexec/istore/mirror-bin:$PATH"
for server in $FEEDS_SERVER_MIRRORS ; do 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" 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 done
DISABLE_MIRROR=true DISABLE_MIRROR=true
fi fi
echo "Try origin server $FEEDS_SERVER" 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' alias fcurl='curl -L --fail --show-error'
check_space() { check_space() {
@@ -91,14 +148,41 @@ update() {
fi fi
echo "Fetch feed list for ${ARCH}" echo "Fetch feed list for ${ARCH}"
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/meta.conf "${FEEDS_SERVER}/all/meta.conf" && \ if $USE_APK; then
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/all.conf "${FEEDS_SERVER}/all/isfeeds.conf" && \ rm -rf ${IS_ROOT}/var/tmp ${IS_ROOT}/etc/arch ${IS_ROOT}/etc/istore.pem
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/arch.conf "${FEEDS_SERVER}/${ARCH}/isfeeds.conf" || \ mkdir -p ${IS_ROOT}/var/tmp
return 1 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" 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 return 0
} }
@@ -110,9 +194,30 @@ update_if_outdate() {
return 0 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() { 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 newest=`get_online_version "$ISTORE_INDEX" "$ISTORE_PKG"`
local current=`grep -Fm1 'Version: ' /usr/lib/opkg/info/${ISTORE_PKG}.control | sed 's/^Version: //'` local current=`get_local_version "$ISTORE_PKG"`
if [ "v$newest" = "v" -o "v$current" = "v" ]; then if [ "v$newest" = "v" -o "v$current" = "v" ]; then
echo "Check version failed!" >&2 echo "Check version failed!" >&2
exit 255 exit 255
@@ -124,13 +229,13 @@ check_self_upgrade() {
} }
do_self_upgrade_0() { 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() { do_self_upgrade() {
check_mtime || return 1 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 newest=`get_online_version "$ISTORE_INDEX" "$ISTORE_PKG"`
local current=`grep -Fm1 'Version: ' /usr/lib/opkg/info/${ISTORE_PKG}.control | sed 's/^Version: //'` local current=`get_local_version "$ISTORE_PKG"`
if [ "v$newest" = "v" -o "v$current" = "v" ]; then if [ "v$newest" = "v" -o "v$current" = "v" ]; then
echo "Check version failed!" >&2 echo "Check version failed!" >&2
return 1 return 1
@@ -139,7 +244,7 @@ do_self_upgrade() {
echo "Already the latest version!" >&2 echo "Already the latest version!" >&2
return 1 return 1
fi 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 do_self_upgrade_0 && return 0
update_if_outdate || return 1 update_if_outdate || return 1
do_self_upgrade_0 do_self_upgrade_0
@@ -150,7 +255,13 @@ do_self_upgrade() {
} }
check_mtime() { 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() { wrapped_in_update() {
@@ -160,6 +271,16 @@ wrapped_in_update() {
eval "$@" 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() { step_upgrade() {
local pkg local pkg
local pkgs="" local pkgs=""
@@ -172,10 +293,11 @@ step_upgrade() {
fi fi
done done
if [ -n "$pkgs" ]; then if [ -n "$pkgs" ]; then
opkg_wrap_mirrors upgrade $pkgs || return 1 do_upgrade_in_mirrors $pkgs || return 1
fi fi
if [ -n "$metapkg" ]; then if [ -n "$metapkg" ]; then
opkg_wrap_mirrors upgrade $metapkg || return 1 do_upgrade_in_mirrors $metapkg || return 1
refresh_installed $metapkg
fi fi
return 0 return 0
} }
@@ -183,7 +305,7 @@ step_upgrade() {
new_upgrade() { new_upgrade() {
check_mtime || return 1 check_mtime || return 1
local metapkg=`echo "$@" | sed 's/ /\n/g' | grep -F app-meta-` 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 true
else else
update_if_outdate update_if_outdate
@@ -192,7 +314,11 @@ new_upgrade() {
} }
remove() { 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() { autoconf_to_env() {
@@ -224,16 +350,29 @@ try_autoconf() {
PATH="$CLEANPATH" /usr/libexec/istorea/${ISTORE_AUTOCONF}.sh PATH="$CLEANPATH" /usr/libexec/istorea/${ISTORE_AUTOCONF}.sh
} }
try_upgrade_depends() { try_upgrade_depends() {
local pkg="$1" local pkg="$1"
if [[ $pkg == app-meta-* ]]; then 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) local deps
[ -z "$deps" ] || opkg_wrap_mirrors install $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 fi
return 0 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() { dotrun() {
local path="$1" local path="$1"
[ -f "$path" ] || { echo "file not found: $path" >&2; return 1; } [ -f "$path" ] || { echo "file not found: $path" >&2; return 1; }
@@ -248,8 +387,8 @@ dotrun() {
echo "Executing .run file" echo "Executing .run file"
chmod 0755 "$path" && "$path" chmod 0755 "$path" && "$path"
else else
echo "Installing .ipk file" echo "Installing pkg file"
opkg install "$path" $CMD_INSTALL "$path"
fi fi
local RET=$? local RET=$?
rm -f "$path" rm -f "$path"
@@ -286,6 +425,13 @@ run_records() {
echo "]" echo "]"
} }
if $USE_APK; then
CMD_REMOVE="apk del"
else
CMD_REMOVE="opkg --autoremove remove"
fi
unrun() { unrun() {
local id="$1" local id="$1"
[ -s "/usr/share/istore/run-records/$id.txt" ] || { echo "record not found: $id" >&2; return 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 local round max_round=6
for round in `seq 1 $max_round`; do for round in `seq 1 $max_round`; do
if [ $round == $max_round ]; then 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 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 fi
done done
sh -c "$CMD_INSTALLED" > "/tmp/is-root/tmp/post_unrun_$id.txt" sh -c "$CMD_INSTALLED" > "/tmp/is-root/tmp/post_unrun_$id.txt"
@@ -327,13 +473,19 @@ usage() {
echo " do_self_upgrade Upgrade iStore" echo " do_self_upgrade Upgrade iStore"
echo " arch Show libc architecture" echo " arch Show libc architecture"
echo " opkg sys opkg wrap" echo " opkg sys opkg wrap"
echo " dotrun {path} install .run/.ipk" echo " dotrun {path} install .run/.ipk/.apk"
echo " run_records list .run/.ipk install records" echo " run_records list .run/.ipk/.apk install records"
echo " del_record {id} delete .run/.ipk install record" echo " del_record {id} delete .run/.ipk/.apk install record"
echo " unrun {id} Remove installed packages by .run/.ipk" echo " unrun {id} Remove installed packages by .run/.ipk/.apk"
exit 1 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 is_init >/dev/null 2>&1
CLEANPATH="$PATH" CLEANPATH="$PATH"
@@ -352,7 +504,7 @@ case $action in
;; ;;
"install") "install")
check_space 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") "autoconf")
try_autoconf try_autoconf
@@ -364,10 +516,18 @@ case $action in
remove "$@" || remove "$@" remove "$@" || remove "$@"
;; ;;
"info") "info")
opkg_wrap info "$@" if $USE_APK; then
apk_wrap query "$@"
else
opkg_wrap info "$@"
fi
;; ;;
"list-upgradable") "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")
check_self_upgrade check_self_upgrade
@@ -394,6 +554,11 @@ case $action in
"unrun") "unrun")
unrun "$1" 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 usage
;; ;;
+1
View File
@@ -7,6 +7,7 @@ boot() {
local ARCH=`/bin/is-opkg arch` local ARCH=`/bin/is-opkg arch`
generate_store_id $ARCH generate_store_id $ARCH
generate_super_arch $ARCH generate_super_arch $ARCH
/bin/is-opkg link_meta
} }
generate_store_id() { generate_store_id() {
@@ -9,4 +9,7 @@ if [ -z "`uci -q get istore.istore.hide_docker`" ] && ! which docker >/dev/null;
EOF EOF
fi 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 rm -rf /tmp/luci-indexcache /tmp/luci-modulecache
+23 -18
View File
@@ -5,11 +5,12 @@
IS_ROOT=/tmp/is-backup IS_ROOT=/tmp/is-backup
APP_LIST_FILE=/etc/istore/app.list APP_LIST_FILE=/etc/istore/app.list
BACKUP_CONFIG_FILE=/etc/config/istore BACKUP_CONFIG_FILE=/etc/config/istore
USE_APK=false
[[ -s /etc/apk/arch ]] && USE_APK=true
action=${1} action=${1}
shift shift
is_init() { is_init() {
mkdir -p ${IS_ROOT} mkdir -p ${IS_ROOT}
} }
@@ -90,23 +91,23 @@ ipk_build() {
# if arg is NULL, use light backup, otherwise use local backup # if arg is NULL, use light backup, otherwise use local backup
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, if $USE_APK; then
#sysupgrade will backup/restore it auto when flash new firmware ( cd /lib/apk/packages && ls *.list ) | grep '^app-meta-' | sed 's/.list$//' | sort -u > ${IS_ROOT}/istore_installed_package.list
echo "/etc/.app_store.id" > /lib/upgrade/keep.d/luci-app-store else
cat /usr/lib/opkg/info/luci-app-store.list >> /lib/upgrade/keep.d/luci-app-store #write user installed package list to file
echo "/etc/rc.d/S45istore" >> /lib/upgrade/keep.d/luci-app-store opkg_list_installed_packages "userinstalled" 2>/dev/null | sort -u > ${IS_ROOT}/user_installed_package.list
echo "/etc/istore/uci-defaults_bak" >> /lib/upgrade/keep.d/luci-app-store #write installed package list by istore feed to file
echo "${APP_LIST_FILE}" >> /lib/upgrade/keep.d/luci-app-store cat ${IS_ROOT}/user_installed_package.list | \
echo "${BACKUP_CONFIG_FILE}" >> /lib/upgrade/keep.d/luci-app-store grep '^app-meta-' > ${IS_ROOT}/istore_installed_package.list
fi
#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 no input backup path, only back app.list #if no input backup path, only back app.list
mkdir -p /etc/istore mkdir -p /etc/istore
@@ -186,6 +187,10 @@ backup() {
# if arg is NULL, use light backup, otherwise use local backup # if arg is NULL, use light backup, otherwise use local backup
restore() { restore() {
if [ -n "$1" ]; then if [ -n "$1" ]; then
if $USE_APK; then
echo "does not support apk package manager" >&2
exit 1
fi
BACKUP_PATH_FILE=$1 BACKUP_PATH_FILE=$1
else else
echo "install package by ${APP_LIST_FILE}" echo "install package by ${APP_LIST_FILE}"
@@ -233,7 +238,7 @@ restore() {
get_support_backup_features() { get_support_backup_features() {
echo "light_backup" echo "light_backup"
#istore custom img mean support local_backup #istore custom img mean support local_backup
if [ -f /etc/istore_img_flag ];then if ! $USE_APK; then
echo "local_backup" echo "local_backup"
fi 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: install:
[ -n "$(APP_STORE_VERSION)" ] [ -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/" 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" $(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" 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` 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" mkdir -p "$(DESTDIR)$(LUCI_LIBRARYDIR)/i18n"
$(foreach lang,$(LUCI_LANGUAGES),$(foreach po,$(wildcard ${CURDIR}/po/$(lang)/*.po), \ $(foreach lang,$(LUCI_LANGUAGES),$(foreach po,$(wildcard ${CURDIR}/po/$(lang)/*.po), \
po2lmo $(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 include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.17-r1 PKG_VERSION:=1.0.18-r1
PKG_RELEASE:=1 PKG_RELEASE:=2
LUCI_TITLE:=LuCI support for SysTools LUCI_TITLE:=LuCI support for SysTools
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
@@ -4,8 +4,12 @@ FCURL="curl --fail --show-error"
curl -V >/dev/null 2>&1 || { curl -V >/dev/null 2>&1 || {
echo "prereq: install curl" echo "prereq: install curl"
opkg info curl | grep -Fqm1 curl || opkg update if [ -f /etc/apk/arch ]; then
opkg install curl 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'` 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 chmod 755 /tmp/is-opkg
/tmp/is-opkg update /tmp/is-opkg update
# /tmp/is-opkg install taskd # /tmp/is-opkg install taskd
/tmp/is-opkg opkg install --force-reinstall luci-lib-taskd luci-lib-xterm /tmp/is-opkg install --force-reinstall luci-lib-taskd luci-lib-xterm
/tmp/is-opkg opkg install --force-reinstall luci-app-store || exit $? /tmp/is-opkg install --force-reinstall luci-app-store || exit $?
[ -s "/etc/init.d/tasks" ] || /tmp/is-opkg opkg install --force-reinstall taskd [ -s "/etc/init.d/tasks" ] || /tmp/is-opkg install --force-reinstall taskd
[ -s "/usr/lib/lua/luci/cbi.lua" ] || /tmp/is-opkg opkg install luci-compat >/dev/null 2>&1 [ -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 LUCI_NAME:=luci-app-tinyfilemanager
PKG_VERSION:=2.6 PKG_VERSION:=2.6
PKG_RELEASE:=2 PKG_RELEASE:=3
LUCI_TITLE:=LuCI Tiny File Manager 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 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']) return fs.exec('/etc/init.d/tinyfilemanager', ['reload'])
.catch((e) => { ui.addNotification(null, E('p', e.message), 'error') }); .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 = s.option(form.Flag, 'use_auth', _('Enable Authentication'));
o.rmempty = false; o.rmempty = false;
+16 -12
View File
@@ -725,20 +725,24 @@ const methods = {
getCPUInfo: { getCPUInfo: {
call: function() { call: function() {
if (!access('/sbin/cpuinfo')) if (access('/sbin/cpuinfo')) {
return {}; const fd = popen('/sbin/cpuinfo');
if (fd) {
let cpuinfo = fd.read('all');
if (!cpuinfo)
cpuinfo = '?';
fd.close();
const fd = popen('/sbin/cpuinfo'); return { cpuinfo: cpuinfo };
if (fd) { } else {
let cpuinfo = fd.read('all'); return { cpuinfo: error() };
if (!cpuinfo) }
cpuinfo = '?';
fd.close();
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 { 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 { & .header-content {
@apply relative flex h-14 items-center justify-between px-6 py-3 max-md:px-4 max-md:py-2; @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"] & { [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 { & .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 { &.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 /* 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 is a pure cross-fade. Below lg the side tracks drop and only the
links column remains (tablet → just the submenu). The panel scrolls links column remains (tablet → just the submenu). The panel scrolls
internally when its content exceeds the viewport below the header. */ 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 { &.active {
@apply pointer-events-auto translate-y-0 opacity-100; @apply pointer-events-auto translate-y-0 opacity-100;
@@ -136,8 +156,12 @@ header {
} }
} }
[data-nav-type="mega-menu"] &:has(.desktop-menu-container.active) { /* When a category is open the bar lifts to the panel's opened colour so the
@apply z-70; 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 { .brand {
+7 -4
View File
@@ -34,7 +34,8 @@
--danger-surface: oklch(94% 0.05 25); --danger-surface: oklch(94% 0.05 25);
--danger-surface-hover: oklch(90% 0.05 25); --danger-surface-hover: oklch(90% 0.05 25);
--scrim: oklch(0% 0 0 / 0.6); --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-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-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); --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: oklch(20.52% 0.0287 219.4);
--brand-subtle-hover: oklch(24.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); --focus-ring: oklch(60% 0.13 188.7 / 0.6);
--progress-start: oklch(43.18% 0.0865 166.9); --progress-start: oklch(44.78% 0.0865 193.2);
--progress-end: oklch(62.1% 0.145 189.6); --progress-end: oklch(0.6 0.13 188.745);
--info-surface: oklch(32% 0.05 255); --info-surface: oklch(32% 0.05 255);
--warning-surface: oklch(33% 0.06 80); --warning-surface: oklch(33% 0.06 80);
--success-surface: oklch(30% 0.05 158); --success-surface: oklch(30% 0.05 158);
--danger-surface: oklch(32% 0.08 22); --danger-surface: oklch(32% 0.08 22);
--danger-surface-hover: oklch(36% 0.08 22); --danger-surface-hover: oklch(36% 0.08 22);
--scrim: oklch(0% 0 0 / 0.6); --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-sm: 0 4px 12px oklch(0 0 0 / 0.3);
--app-shadow-md: 0 10px 28px oklch(0 0 0 / 0.42); --app-shadow-md: 0 10px 28px oklch(0 0 0 / 0.42);
--app-shadow-lg: 0 20px 48px oklch(0 0 0 / 0.55); --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-danger-surface-hover: var(--danger-surface-hover);
--color-scrim: var(--scrim); --color-scrim: var(--scrim);
--color-mega-menu-bg: var(--mega-menu-bg); --color-mega-menu-bg: var(--mega-menu-bg);
--color-mega-menu-scrim: var(--mega-menu-scrim);
--shadow-sm: var(--app-shadow-sm); --shadow-sm: var(--app-shadow-sm);
--shadow-md: var(--app-shadow-md); --shadow-md: var(--app-shadow-md);
@@ -69,11 +69,18 @@ body.mobile-navigation-open > header {
} }
[data-nav-type="mega-menu"] { [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 { & .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 { &.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); 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) => { children.forEach((child) => {
const { li, nav, menuLink, hasSubmenu } = this.buildDropdownItem( const { li, nav, menuLink, hasSubmenu } = this.buildDropdownItem(
child, child,
@@ -626,6 +664,14 @@ return baseclass.extend({
hideTimer = null; 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(() => { showTimer = setTimeout(() => {
const wasActive = nav.classList.contains("active"); const wasActive = nav.classList.contains("active");
@@ -638,10 +684,11 @@ return baseclass.extend({
if (container) { if (container) {
applyCanvasHeight(); applyCanvasHeight();
container.classList.add("active"); container.classList.add("active");
container.classList.remove("closing");
overlay.classList.add("active"); overlay.classList.add("active");
} }
nav.classList.add("active"); nav.classList.add("active");
}, 100); }, delay);
}); });
li.addEventListener("mouseleave", () => { li.addEventListener("mouseleave", () => {
@@ -724,42 +771,47 @@ return baseclass.extend({
document.querySelector(".desktop-menu-overlay")?.classList.remove("active"); document.querySelector(".desktop-menu-overlay")?.classList.remove("active");
if (!container) return; 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"); container.classList.remove("active");
// --mega-menu-height is set on open in initMegaMenu and otherwise never // Retract the canvas: dropping --mega-menu-height lets the container's
// cleared. Drop it back to the h-0 fallback once the container is fully // height fall to its 0 fallback, and because the container transitions
// hidden, so a closed menu doesn't leave invisible scrollable space // height (see _layout.css) it animates the collapse — the drawer close —
// below the header on pages shorter than the canvas. // instead of the height vanishing in one frame after the fade. overflow
const resetHeight = () => { // -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")) { 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 if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
// never changes, so transitionend/transitioncancel would never fire and finishClosing();
// these listeners would pile up on every header mouseleave.
if (
!wasActive ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
resetHeight();
return; return;
} }
const onClipPathSettled = (event) => { container.addEventListener("transitionend", finishClosing);
if (event.target !== container || event.propertyName !== "clip-path") { closeFallback = setTimeout(finishClosing, 350);
return;
}
container.removeEventListener("transitionend", onClipPathSettled);
container.removeEventListener("transitioncancel", onClipPathSettled);
resetHeight();
};
container.addEventListener("transitionend", onClipPathSettled);
container.addEventListener("transitioncancel", onClipPathSettled);
}, },
renderModeMenu(tree) { renderModeMenu(tree) {
@@ -94,6 +94,12 @@ class FakeElement {
this.listeners.set(type, listeners); this.listeners.set(type, listeners);
} }
dispatchEvent(event) {
for (const listener of this.listeners.get(event.type) ?? []) {
listener(event);
}
}
closest(selector) { closest(selector) {
let current = this; let current = this;
@@ -169,6 +175,15 @@ class FakeElement {
if (name === "class") this.classList.replace(""); 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) { setAttribute(name, value) {
this.attributes.set(name, String(value)); this.attributes.set(name, String(value));
if (name === "class") this.classList.replace(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.getAttribute("aria-hidden"), "false");
assert.equal(region.hasAttribute("inert"), 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"]); 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", () => { test("mega-menu category masks use Tailwind arbitrary utilities", () => {
const title = getBlock(layoutStyles, "& .desktop-nav-title"); const title = getBlock(layoutStyles, "& .desktop-nav-title");
const icon = getBlock(title, "&::before"); 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)"); 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 l = resolveMode("light");
const d = resolveMode("dark"); const d = resolveMode("dark");
assert.ok(l.mega_menu_bg.includes("/ 0.66"), `light: ${l.mega_menu_bg}`); // The panel animates height, so it carries no backdrop-filter; and it is
assert.ok(d.mega_menu_bg.includes("/ 0.62"), `dark: ${d.mega_menu_bg}`); // 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", () => { 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.brand, "oklch(0.6 0.13 188.745)");
assert.equal(dark.on_brand, "oklch(1 0 0)"); assert.equal(dark.on_brand, "oklch(1 0 0)");
assert.equal(dark.progress_start, "oklch(43.18% 0.0865 166.9)"); // Derived from brand like light: end aliases brand, start mixes toward sunken.
assert.equal(dark.progress_end, "oklch(62.1% 0.145 189.6)"); assert.equal(dark.progress_start, "oklch(44.78% 0.0865 193.2)");
assert.equal(dark.progress_end, dark.brand);
}); });
test("dark differs from light", () => { 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}`); 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 layout = read("../src/media/_layout.css");
const overlay = read("../src/media/components/_overlay.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")); 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(!panel?.includes("backdrop-blur"), `panel must not blur: ${panel}`);
assert.ok(overlay.includes("max-md:backdrop-blur-lg"), "mobile overlay blur");
assert.ok( assert.ok(
/&\.active\s*\{\s*@apply[^;]*backdrop-blur-lg/.test(overlay), panel?.includes("transition-[height,visibility]"),
"desktop overlay blur", `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", () => { 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 headerRule = layout.match(/^header\s*\{\s*@apply\s+([^;]+);/m)?.[1] ?? "";
const activeHeaderRule = const activeHeaderRule =
layout.match( 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] ?? ""; )?.[1] ?? "";
const dropdownRule = dropdown.match(/&\.dropdown\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] ?? ""; 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: ["set", "danger", 0.94, 0.05],
danger_surface_hover: ["shade", "danger_surface", -0.04], danger_surface_hover: ["shade", "danger_surface", -0.04],
scrim: ["const", "oklch(0 0 0 / 0.6)"], 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: { dark: {
text_muted: ["mix", "text", "bg", 0.62], text_muted: ["mix", "text", "bg", 0.62],
@@ -33,15 +45,24 @@ export const DERIVATIONS = {
brand_subtle: ["mix", "brand", "bg", 0.16], brand_subtle: ["mix", "brand", "bg", 0.16],
brand_subtle_hover: ["shade", "brand_subtle", 0.04], brand_subtle_hover: ["shade", "brand_subtle", 0.04],
focus_ring: ["alpha", "brand", 0.6], focus_ring: ["alpha", "brand", 0.6],
progress_start: ["const", "oklch(0.4318 0.0865 166.91)"], // Derived from brand like light — not hardcoded — so the bar tracks the
progress_end: ["const", "oklch(0.621 0.145 189.632)"], // 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], info_surface: ["set", "info", 0.32, 0.05],
warning_surface: ["set", "warning", 0.33, 0.06], warning_surface: ["set", "warning", 0.33, 0.06],
success_surface: ["set", "success", 0.3, 0.05], success_surface: ["set", "success", 0.3, 0.05],
danger_surface: ["set", "danger", 0.32, 0.08], danger_surface: ["set", "danger", 0.32, 0.08],
danger_surface_hover: ["shade", "danger_surface", 0.04], danger_surface_hover: ["shade", "danger_surface", 0.04],
scrim: ["const", "oklch(0 0 0 / 0.6)"], 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_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base LUCI_DEPENDS:=+luci-base
PKG_VERSION:=0.12.11 PKG_VERSION:=0.12.12
PKG_RELEASE:=28 PKG_RELEASE:=29
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:= LUCI_MINIFY_CSS:=
@@ -1,2 +1,2 @@
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */ /*! 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 */ /*! 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:= LUCI_DEPENDS:=
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
PKG_VERSION:=7.1 PKG_VERSION:=7.1
PKG_RELEASE:=6 PKG_RELEASE:=7
LUCI_MINIFY_CSS:= LUCI_MINIFY_CSS:=
CONFIG_LUCI_CSSTIDY:= CONFIG_LUCI_CSSTIDY:=
@@ -89,18 +89,17 @@ return baseclass.extend({
}, },
getConditionalMenuStamp: function() { getConditionalMenuStamp: function() {
var googleFuMode = !!document.querySelector('.navbar a[href*="/admin/services/openclash"]');
return Promise.all([ return Promise.all([
this.getFileListStamp('/usr/share/luci/menu.d', '.json'), this.getFileListStamp('/usr/share/luci/menu.d', '.json'),
this.getFileListStamp('/usr/share/rpcd/acl.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) { ]).then(function(stamps) {
return JSON.stringify({ return JSON.stringify({
google_fu_mode: googleFuMode,
menu_definitions: stamps[0], menu_definitions: stamps[0],
acl_definitions: stamps[1], 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 include $(TOPDIR)/rules.mk
PKG_NAME:=mihomo PKG_NAME:=mihomo
PKG_VERSION:=1.19.27 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:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_URL:=https://codeload.github.com/metacubex/mihomo/tar.gz/v$(PKG_VERSION)?
PKG_MIRROR_HASH:=skip PKG_HASH:=skip
PKG_LICENSE:=GPL3.0+ PKG_MAINTAINER:=Anya Lin <hukk1996@gmail.com>
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com> PKG_LICENSE:=GPL-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_BUILD_DEPENDS:=golang/host PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1 PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16 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:=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_TAGS:=with_gvisor
GO_PKG_INSTALL_BIN_PATH:=/usr/libexec
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-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 define Package/mihomo
SECTION:=net SECTION:=net
CATEGORY:=Network CATEGORY:=Network
TITLE:=A rule based proxy in Go. TITLE:=Another Mihomo Kernel.
URL:=https://wiki.metacubex.one URL:=https://wiki.metacubex.one
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle DEPENDS:=$(GO_ARCH_DEPENDS)
PROVIDES:=mihomo
ALTERNATIVES:=\ ALTERNATIVES:=\
300:/usr/bin/mihomo:/usr/libexec/mihomo 100:/usr/bin/mihomo:/usr/libexec/mihomo-core
endef USERID:=mihomo=7890:mihomo=7890
define Package/mihomo/description
Mihomo is a rule based proxy in Go.
endef
define Package/mihomo/install
$(call GoPackage/Package/Install/Bin,$(1))
endef endef
define Build/Prepare define Build/Prepare
$(Build/Prepare/Default) $(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 endef
$(eval $(call GoBinPackage,mihomo)) $(eval $(call GoBinPackage,mihomo))
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=momo PKG_NAME:=momo
PKG_VERSION:=2026.06.03 PKG_VERSION:=2026.06.03
PKG_RELEASE:=10 PKG_RELEASE:=11
PKG_LICENSE:=GPL-3.0+ PKG_LICENSE:=GPL-3.0+
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com> 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" DEBUG_SH="$SH_DIR/debug.sh"
## firewall ## firewall
NFT_DIR="$HOME_DIR/firewall" FIREWALL_DIR="$HOME_DIR/firewall"
GEOIP_CN_NFT="$NFT_DIR/geoip_cn.nft" GEOIP_CN_NFT="$FIREWALL_DIR/geoip_cn.nft"
GEOIP6_CN_NFT="$NFT_DIR/geoip6_cn.nft" GEOIP6_CN_NFT="$FIREWALL_DIR/geoip6_cn.nft"
## log ## log
LOG_DIR="/var/log/momo" LOG_DIR="/var/log/momo"
@@ -63,7 +63,7 @@ get_paths() {
json_add_string firewall_include_sh "$FIREWALL_INCLUDE_SH" json_add_string firewall_include_sh "$FIREWALL_INCLUDE_SH"
json_add_string debug_sh "$DEBUG_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 geoip_cn_nft "$GEOIP_CN_NFT"
json_add_string geoip6_cn_nft "$GEOIP6_CN_NFT" json_add_string geoip6_cn_nft "$GEOIP6_CN_NFT"
+2 -4
View File
@@ -6,12 +6,10 @@
import { readfile } from 'fs'; import { readfile } from 'fs';
import { cursor } from 'uci'; import { cursor } from 'uci';
import { connect } from 'ubus'; 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 fw4 = require('fw4');
const paths = get_paths();
const cgroups_version = get_cgroups_version(); const cgroups_version = get_cgroups_version();
const users = get_users(); const users = get_users();
@@ -21,7 +19,7 @@
const uci = cursor(); const uci = cursor();
const ubus = connect(); const ubus = connect();
const profile = json(readfile(paths.run_profile_path)); const profile = load_profile();
uci.load('momo'); uci.load('momo');
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=nexttrace PKG_NAME:=nexttrace
PKG_VERSION:=1.7.0 PKG_VERSION:=1.7.1
PKG_RELEASE:=6 PKG_RELEASE:=7
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/nxtrace/NTrace-core/tar.gz/v$(PKG_VERSION)? 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_NAME:=opera-proxy
PKG_VERSION:=1.24.0 PKG_VERSION:=1.24.0
PKG_RELEASE:=4 PKG_RELEASE:=5
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru> PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
PKG_LICENSE:=MIT PKG_LICENSE:=MIT
+9 -6
View File
@@ -8,23 +8,26 @@ NAME=opera-proxy
PROG=/usr/bin/${NAME} PROG=/usr/bin/${NAME}
start_service() { 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_load opera-proxy
config_get country main country "EU" config_get country main country "EU"
config_get bind_address main bind_address "127.0.0.1:18080" config_get bind_address main bind_address "127.0.0.1:18080"
config_get verbosity main verbosity "20" config_get verbosity main verbosity "20"
config_get timeout main timeout "10s" 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 config_get_bool socks_mode main socks_mode 0
procd_open_instance procd_open_instance
procd_set_param command "$PROG" procd_set_param command "$PROG"
procd_append_param command -country "$country" [ -n "$country" ] && procd_append_param command -country "$country"
procd_append_param command -bind-address "$bind_address" [ -n "$bind_address" ] && procd_append_param command -bind-address "$bind_address"
procd_append_param command -verbosity "$verbosity" [ -n "$verbosity" ] && procd_append_param command -verbosity "$verbosity"
procd_append_param command -timeout "$timeout" [ -n "$timeout" ] && procd_append_param command -timeout "$timeout"
[ "$socks_mode" = "1" ] && procd_append_param command -socks-mode [ "$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 respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
procd_set_param stdout 1 procd_set_param stdout 1
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=qBittorrent PKG_NAME:=qBittorrent
PKG_VERSION:=5.2.1 PKG_VERSION:=5.2.2
PKG_RELEASE:=2 PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/qbittorrent/qBittorrent/tar.gz/release-$(PKG_VERSION)? 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 include $(TOPDIR)/rules.mk
PKG_NAME:=tuic-client PKG_NAME:=tuic-client
PKG_VERSION:=1.8.6 PKG_VERSION:=1.8.7
PKG_RELEASE:=10 PKG_RELEASE:=11
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/Itsusinn/tuic/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/Itsusinn/tuic/tar.gz/v$(PKG_VERSION)?