From 4a4c312a8f499c0bef8e82481f4a24fe8f3a1c71 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Jun 2026 03:09:38 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20Sync=202026-06-21=2003:09:38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ariang/Makefile | 4 +- dae/Makefile | 3 +- dae/files/dae.config | 2 +- luci-app-daede/Makefile | 4 +- .../resources/view/daede/backend.js | 3 +- .../resources/view/daede/config.js | 53 ++- .../resources/view/daede/converter.js | 7 +- .../luci-static/resources/view/daede/dae.js | 130 ++++--- .../luci-static/resources/view/daede/daed.js | 5 +- .../luci-static/resources/view/daede/log.js | 3 +- .../resources/view/daede/styles.js | 57 +-- .../resources/view/daede/updates.js | 2 + .../resources/view/daede/widgets.js | 96 +++-- luci-app-daede/po/templates/daede.pot | 15 +- luci-app-daede/po/zh-cn/daede.po | 29 +- luci-app-daede/po/zh_Hans/daede.po | 29 +- .../etc/uci-defaults/90-luci-app-daede-init | 3 + .../share/luci-app-daede/gen-dae-config.sh | 2 +- luci-app-homeproxy/.gitignore | 1 + luci-app-homeproxy/LICENSE | 340 ++++++++++++++++++ luci-app-homeproxy/Makefile | 5 +- luci-app-homeproxy/README | 5 + .../resources/view/homeproxy/client.js | 81 +++++ .../resources/view/homeproxy/status.js | 20 +- luci-app-homeproxy/po/templates/homeproxy.pot | 57 +++ luci-app-homeproxy/po/zh_Hans/homeproxy.po | 57 +++ luci-app-homeproxy/root/etc/config/homeproxy | 3 + .../etc/homeproxy/scripts/generate_client.uc | 33 +- .../root/usr/share/rpcd/ucode/luci.homeproxy | 9 +- 29 files changed, 867 insertions(+), 191 deletions(-) create mode 100644 luci-app-homeproxy/.gitignore create mode 100644 luci-app-homeproxy/LICENSE create mode 100644 luci-app-homeproxy/README diff --git a/ariang/Makefile b/ariang/Makefile index 0dc63faa..65dcbec5 100644 --- a/ariang/Makefile +++ b/ariang/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ariang -PKG_VERSION:=1.3.13 -PKG_RELEASE:=3 +PKG_VERSION:=1.3.14 +PKG_RELEASE:=4 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).zip PKG_SOURCE_URL:=https://github.com/mayswind/AriaNg/releases/download/$(PKG_VERSION) diff --git a/dae/Makefile b/dae/Makefile index d0e806e5..80828161 100644 --- a/dae/Makefile +++ b/dae/Makefile @@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dae PKG_VERSION:=2026.06.14 -PKG_RELEASE:=9 +PKG_RELEASE:=10 PKG_SOURCE:=dae-src-2026.06.14-fa89ea7197a5.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src @@ -128,6 +128,7 @@ define Package/dae/install $(INSTALL_DIR) $(1)/etc/dae $(INSTALL_CONF) $(PKG_BUILD_DIR)/example.dae $(1)/etc/dae/example.dae + $(SED) 's|^[[:space:]]*#lan_interface: docker0| lan_interface: br-lan|' $(1)/etc/dae/example.dae $(INSTALL_DIR) $(1)/etc/config $(INSTALL_CONF) $(CURDIR)/files/dae.config $(1)/etc/config/dae diff --git a/dae/files/dae.config b/dae/files/dae.config index 7575e2f3..7e61ab49 100644 --- a/dae/files/dae.config +++ b/dae/files/dae.config @@ -2,6 +2,6 @@ config dae 'config' option enabled '0' option config_file '/etc/dae/config.dae' + option lan_interface 'br-lan' option log_maxbackups '1' option log_maxsize '1' - diff --git a/luci-app-daede/Makefile b/luci-app-daede/Makefile index b51a7992..aff5c614 100644 --- a/luci-app-daede/Makefile +++ b/luci-app-daede/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-daede -PKG_VERSION:=1.14.0 -PKG_RELEASE:=19 +PKG_VERSION:=1.14.1 +PKG_RELEASE:=20 PKG_MAINTAINER:=kenzok8 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/backend.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/backend.js index 5d355c19..a417e1e1 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/backend.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/backend.js @@ -102,7 +102,8 @@ function setActiveBackend(name) { return fs.write('/etc/config/daede', ''); }).then(function() { return fs.exec('/sbin/uci', ['set', 'daede.config=daede']); }) .then(function() { return fs.exec('/sbin/uci', ['set', 'daede.config.active_backend=' + name]); }) - .then(function() { return fs.exec('/sbin/uci', ['commit', 'daede']); }); + .then(function() { return fs.exec('/sbin/uci', ['commit', 'daede']); }) + .then(function() { uci.set('daede', 'config', 'active_backend', name); }); } function detectBackend() { diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/config.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/config.js index 7f3cca45..91708388 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/config.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/config.js @@ -10,7 +10,7 @@ 'require view.daede.daed as daedView'; return view.extend({ - load: function() { + _loadContext: function() { return backend.detectBackend().then(function(ctx) { return uci.load(ctx.backend.uci).catch(function() {}).then(function() { return ctx; @@ -18,22 +18,57 @@ return view.extend({ }); }, + load: function() { + return this._loadContext(); + }, + render: function(ctx) { + const self = this; + const themeHref = Array.prototype.map.call(document.styleSheets, function(sheet) { return sheet.href || ''; }).join(' '); + document.documentElement.setAttribute('data-daede-theme', /\/argon\//.test(themeHref) ? 'argon' : 'bootstrap'); + /* themes signal dark differently — BootstrapDark sets data-darkmode, - Argon just loads a dark stylesheet with no flag. Detect dark from the - page background luminance and set data-darkmode so our dark rules - (keyed on it) fire uniformly across every theme's dark mode. */ + Argon just loads a dark stylesheet with no flag. Read the first opaque + background up the tree; a transparent body (CSS not applied yet) must + NOT count as black, else argon light is misdetected as dark. */ try { - const bg = getComputedStyle(document.body).backgroundColor.match(/\d+/g); - if (bg && (0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2]) < 128) - document.documentElement.setAttribute('data-darkmode', 'true'); + const probe = [document.body, document.documentElement]; + for (let i = 0; i < probe.length; i++) { + const m = getComputedStyle(probe[i]).backgroundColor.match(/[\d.]+/g); + if (!m) continue; + const a = m.length >= 4 ? parseFloat(m[3]) : 1; + if (a < 0.1) continue; // transparent — keep looking + if (0.299 * m[0] + 0.587 * m[1] + 0.114 * m[2] < 128) + document.documentElement.setAttribute('data-darkmode', 'true'); + break; // first opaque background decides + } } catch (e) {} + const redrawBackend = function(name, message) { + self._backendHint = message; + return self._loadContext() + .then(function(nextCtx) { return self.render(nextCtx); }) + .then(function(nextRoot) { + const current = document.querySelector('.dd-config-page'); + if (!current) return; + Array.prototype.forEach.call(current.querySelectorAll('.dd-status-card'), function(card) { + if (card._ddCleanup) card._ddCleanup(); + }); + current.replaceWith(nextRoot); + if (self._backendHintTimer) clearTimeout(self._backendHintTimer); + self._backendHintTimer = setTimeout(function() { + self._backendHint = ''; + const hint = document.querySelector('.dd-config-page .dd-backend-help'); + if (hint) hint.textContent = ''; + }, 3500); + }); + }; + const listenAddr = uci.get('daed', 'config', 'listen_addr') || backend.BACKENDS.daed.defaultListen; const children = [ E('style', {}, styles.CSS), widgets.renderStatusCard(ctx, listenAddr), - widgets.renderBackendSwitcher(ctx) + widgets.renderBackendSwitcher(ctx, redrawBackend, self._backendHint) ].filter(function(node) { return !!node; }); if (!ctx.installed[ctx.name]) { @@ -49,7 +84,7 @@ return view.extend({ return Promise.all(children.map(function(child) { return child && child.then ? child : Promise.resolve(child); })).then(function(nodes) { - return E('div', { 'class': 'dd-wrap' }, nodes.filter(function(n) { return !!n; })); + return E('div', { 'class': 'dd-wrap dd-config-page' }, nodes.filter(function(n) { return !!n; })); }); }, diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/converter.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/converter.js index 8c48148b..ca0c9777 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/converter.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/converter.js @@ -250,7 +250,6 @@ return view.extend({ const airportName = E('input', { 'class': 'dd-conv-airport-name', 'placeholder': _('Group name'), 'autocomplete': 'off' }); const importButton = E('button', { 'class': 'cbi-button cbi-button-positive', 'disabled': 'disabled' }, _('Import node group')); const importTitle = E('h4', { 'class': 'dd-card-title' }); - const importDescription = E('p', { 'class': 'dd-settings-descr' }); const airportNameLabel = E('label'); const summary = E('div', { 'class': 'dd-conv-summary' }); const groupSummary = E('div', { 'class': 'dd-conv-group-summary' }); @@ -299,9 +298,6 @@ return view.extend({ const updateResultSummary = function() { const isDaed = state.target === 'daed'; importTitle.textContent = isDaed ? _('3. Import Subscription') : _('3. Import Node Group'); - importDescription.textContent = isDaed - ? _('Import selected nodes as one subscription and add it to the default proxy group.') - : _('Import the selected nodes as one named group, so large airport node lists stay easy to manage.'); airportNameLabel.textContent = isDaed ? _('Subscription name') : _('Group name'); const compatible = state.results.filter(function(item) { return item.ok; }).length; const unsupported = state.results.length - compatible; @@ -775,7 +771,7 @@ return view.extend({ return E('div', { 'class': 'dd-wrap dd-converter' }, [ E('style', {}, styles.CSS), E('div', { 'class': 'dd-card dd-conv-card' }, [ - E('p', { 'class': 'dd-settings-descr' }, _('Convert Clash YAML into share links, preview the result, then import selected nodes into dae or daed. Inputs and credentials are not saved.')), + E('p', { 'class': 'dd-settings-descr' }, _('Convert Clash YAML to share links and import the nodes you pick. Nothing is saved.')), E('h4', { 'class': 'dd-card-title' }, _('1. Input Clash YAML')), E('div', { 'class': 'dd-conv-url-row' }, [ urlInput, uaSelect, parseUrl ]), E('div', { 'class': 'dd-conv-or' }, _('or')), @@ -793,7 +789,6 @@ return view.extend({ ]), E('div', { 'class': 'dd-card dd-conv-card' }, [ importTitle, - importDescription, E('div', { 'class': 'dd-conv-import dd-conv-airport' }, [ airportNameLabel, airportName, diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/dae.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/dae.js index 0808f830..970b6f00 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/dae.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/dae.js @@ -167,6 +167,44 @@ function accordionizeSections(mapNode, openTitles) { } } +function organizeDaeSections(mapNode, openTitles) { + accordionizeSections(mapNode, openTitles); + + const advTitles = [ _('Groups'), _('Routing'), _('DNS'), _('Logging') ]; + const advWraps = []; + mapNode.querySelectorAll('.dd-adv').forEach(function(w) { + const t = w.querySelector('.dd-adv-bar span'); + if (t && advTitles.indexOf(t.textContent.trim()) >= 0) advWraps.push(w); + }); + if (!advWraps.length) return; + + const outerOpen = (openTitles || []).indexOf(_('Advanced settings')) >= 0; + const outerBody = E('div', { 'class': 'dd-adv-body' }); + const outer = E('div', { 'class': 'dd-adv' + (outerOpen ? '' : ' dd-closed') }, [ + E('div', { 'class': 'dd-adv-bar' }, [ + E('span', {}, _('Advanced settings')), + E('span', { 'class': 'dd-adv-chevron' }, '›') + ]), + outerBody + ]); + outer.firstChild.addEventListener('click', function() { outer.classList.toggle('dd-closed'); }); + advWraps[0].parentNode.insertBefore(outer, advWraps[0]); + advWraps.forEach(function(w) { outerBody.appendChild(w); }); +} + +function captureAccordionState(mapNode) { + if (!mapNode) return []; + return Array.prototype.map.call( + mapNode.querySelectorAll('.dd-adv:not(.dd-closed) > .dd-adv-bar span:first-child'), + function(node) { return node.textContent.trim(); } + ); +} + +function restoreAccordionState(mapNode, openTitles) { + if (!mapNode || mapNode.querySelector('.dd-adv')) return; + organizeDaeSections(mapNode, openTitles || []); +} + /* Friendly form UI for the dae backend. Form is the source of truth: on save we commit the `dae` UCI package, then gen-dae-config.sh renders config.dae, validates it and hot-reloads. */ @@ -175,8 +213,7 @@ function renderDaeForms(ctx) { m = new form.Map('dae', null, null); /* Subscriptions */ - s = m.section(form.GridSection, 'subscription', _('Subscriptions'), - _('Airport / subscription links. dae resolves them into the node pool.')); + s = m.section(form.GridSection, 'subscription', _('Subscriptions'), null); s.addremove = true; s.anonymous = true; s.sortable = false; @@ -302,6 +339,7 @@ function renderDaeForms(ctx) { o.default = '1'; const status = E('span', { 'class': 'dd-editor-status' }, ''); + let renderSaveActions = function() {}; let statusTimer = null; function flash(text, kind, hold) { status.textContent = text; @@ -316,6 +354,12 @@ function renderDaeForms(ctx) { - opts.start: after generate, enable + start dae (stopped → "Save and Start") - otherwise just save + generate; generate hot-reloads itself when dae runs */ function doSave(opts, btns) { + const settingsCard = status.closest('.dd-settings-card'); + const openTitles = captureAccordionState(settingsCard && settingsCard.querySelector('.cbi-map')); + const restoreLiveAccordions = function() { + const liveCard = status.closest('.dd-settings-card'); + restoreAccordionState(liveCard && liveCard.querySelector('.cbi-map'), openTitles); + }; btns.forEach(function(b) { b.disabled = true; }); flash(_('Saving…')); /* ensure the singleton sections exist before parsing the form — @@ -326,6 +370,7 @@ function renderDaeForms(ctx) { if (!uci.get('dae', 'config')) uci.add('dae', 'dae', 'config'); return m.save(null, true) .then(function() { + restoreLiveAccordions(); /* assign unique tags to any rows the user left blank */ autofillTags('subscription', 'subscription'); autofillTags('node', 'node'); @@ -350,7 +395,6 @@ function renderDaeForms(ctx) { if (!opts.start) { /* generate already hot-reloaded if dae was running */ flash(opts.okMsg || _('Saved'), 'ok'); - setTimeout(function() { window.location.reload(); }, 900); return; } flash(_('Starting…')); @@ -363,7 +407,7 @@ function renderDaeForms(ctx) { flash(_('Start failed: %s').format(r2.stderr || r2.stdout || ('exit ' + r2.code)), 'err', 9000); else { flash(_('Saved · started'), 'ok'); - setTimeout(function() { window.location.reload(); }, 900); + renderSaveActions(true); } }); }) @@ -375,41 +419,18 @@ function renderDaeForms(ctx) { } flash(_('Save failed: %s').format(e.message || e), 'err', 9000); }) - .finally(function() { btns.forEach(function(b) { b.disabled = false; }); }); + .finally(function() { + restoreLiveAccordions(); + btns.forEach(function(b) { b.disabled = false; }); + }); } return m.render().then(function(mapNode) { /* beginner default: only Subscriptions starts open; nodes/groups/routing/dns/logging collapse */ - accordionizeSections(mapNode, [ _('Subscriptions') ]); + organizeDaeSections(mapNode, [ _('Subscriptions') ]); - /* fold groups / routing / dns / logging under one "Advanced settings" - collapsible so the everyday view is just subscriptions + nodes */ - const advTitles = [ _('Groups'), _('Routing'), _('DNS'), _('Logging') ]; - const advWraps = []; - mapNode.querySelectorAll('.dd-adv').forEach(function(w) { - const t = w.querySelector('.dd-adv-bar span'); - if (t && advTitles.indexOf(t.textContent.trim()) >= 0) advWraps.push(w); - }); - if (advWraps.length) { - const outerBody = E('div', { 'class': 'dd-adv-body' }); - const outer = E('div', { 'class': 'dd-adv dd-closed' }, [ - E('div', { 'class': 'dd-adv-bar' }, [ - E('span', {}, _('Advanced settings')), - E('span', { 'class': 'dd-adv-chevron' }, '›') - ]), - outerBody - ]); - outer.firstChild.addEventListener('click', function() { outer.classList.toggle('dd-closed'); }); - advWraps[0].parentNode.insertBefore(outer, advWraps[0]); - advWraps.forEach(function(w) { outerBody.appendChild(w); }); - } - - const cardChildren = [ - E('h4', { 'class': 'dd-card-title' }, _('dae Configuration')), - E('div', { 'class': 'dd-settings-descr' }, - _('Add a subscription or node, then save — it takes effect automatically.')) - ]; + const cardChildren = [ E('h4', { 'class': 'dd-card-title' }, _('dae Configuration')) ]; cardChildren.push(mapNode); /* state-aware primary action: running → one "Save and Apply"; stopped → @@ -458,23 +479,28 @@ function renderDaeForms(ctx) { } } - const actions = []; - let btns; - if (running) { - const apply = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save and Apply')); - btns = [ apply ]; - apply.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ okMsg: _('Saved · applied') }, btns); }); - actions.push(apply); - } else { - const saveOnly = E('button', { 'class': 'cbi-button' }, _('Save config')); - const saveStart = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save and Start')); - btns = [ saveOnly, saveStart ]; - saveOnly.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ okMsg: _('Saved') }, btns); }); - saveStart.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ start: true }, btns); }); - actions.push(saveOnly, saveStart); - } - actions.push(status); - cardChildren.push(E('div', { 'class': 'dd-editor-actions' }, actions)); + const actionsWrap = E('div', { 'class': 'dd-editor-actions' }); + renderSaveActions = function(isRunning) { + while (actionsWrap.firstChild) actionsWrap.removeChild(actionsWrap.firstChild); + let btns; + if (isRunning) { + const apply = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save and Apply')); + btns = [ apply ]; + apply.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ okMsg: _('Saved · applied') }, btns); }); + actionsWrap.appendChild(apply); + } else { + const saveOnly = E('button', { 'class': 'cbi-button' }, _('Save config')); + const saveStart = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save and Start')); + btns = [ saveOnly, saveStart ]; + saveOnly.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ okMsg: _('Saved') }, btns); }); + saveStart.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ start: true }, btns); }); + actionsWrap.appendChild(saveOnly); + actionsWrap.appendChild(saveStart); + } + actionsWrap.appendChild(status); + }; + renderSaveActions(running); + cardChildren.push(actionsWrap); return E('div', { 'class': 'dd-card dd-settings-card' }, cardChildren); }); }); @@ -744,8 +770,6 @@ function renderDaeEditor() { return E('div', { 'class': 'dd-card' }, [ E('h4', { 'class': 'dd-card-title' }, _('Advanced / Manual Mode')), - E('div', { 'class': 'dd-settings-descr' }, - _('Edit config.dae directly. Saving the form above regenerates the file and overwrites changes made here.')), (function() { var adv = E('div', { 'class': 'dd-adv dd-closed', style: 'margin-top:8px' }, [ E('div', { 'class': 'dd-adv-bar' }, [ @@ -756,7 +780,7 @@ function renderDaeEditor() { E('div', { 'class': 'dd-editor-hint', style: 'margin:0 0 10px' }, [ E('b', {}, _('Text-only mode.')), ' ', - _('Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI.') + _('Edit the dae DSL directly. Initialize loads a template. Use daed for a GUI.') ]), phWarn, editWrap, diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/daed.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/daed.js index 4c7be810..07e86c35 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/daed.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/daed.js @@ -14,8 +14,7 @@ function renderDaedSettings() { s.addremove = false; s.anonymous = true; - o = s.option(form.Value, 'listen_addr', _('Listen Address'), - _('Host:port that the daed WebUI and GraphQL API listen on.')); + o = s.option(form.Value, 'listen_addr', _('Listen Address')); o.datatype = 'ipaddrport(1)'; o.default = '0.0.0.0:2023'; o.rmempty = false; @@ -32,7 +31,7 @@ function renderDaedSettings() { return widgets.wrapSettingsCard( _('daede Settings'), - _('A modern dashboard for dae. Subscriptions, nodes, routing and DNS are managed in the daed WebUI.'), + null, m.render(), _('Log Advanced Settings'), ['log_maxsize', 'log_maxbackups'] diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js index b0a6683d..6e4aa0f0 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js @@ -15,7 +15,8 @@ const CSS = [ '.dd-log-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;letter-spacing:.3px;text-transform:uppercase}', '.dd-log-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:0 0 8px;margin-bottom:8px;border-bottom:1px dashed rgba(128,128,128,.2)}', '.dd-log-toolbar label{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;cursor:pointer;margin:0;opacity:.85}', - '.dd-log-toolbar input[type="checkbox"]{margin:0}', + /* Argon shifts label>checkbox down by top:.4rem; reset so flex centers it with the text */ + '.dd-log-toolbar input[type="checkbox"]{position:static;top:auto;right:auto;margin:0}', '.dd-log-toolbar input[type="text"]{font-size:11.5px;padding:4px 8px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit;min-width:160px}', '.dd-log-toolbar .dd-log-btn{font-size:11.5px;line-height:1.4;min-height:0;height:auto;padding:4px 12px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit;cursor:pointer}', '.dd-log-toolbar .dd-log-btn:hover{background:rgba(128,128,128,.1)}', diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js index fe66c78b..e4a2f799 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js @@ -6,7 +6,9 @@ const CSS = [ '.dd-wrap{padding:4px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}', '.dd-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:9px 14px;margin-bottom:7px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}', - '.dd-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;letter-spacing:.3px;text-transform:uppercase}', + /* padding:0 neutralizes Argon's h4{padding:.75rem 1.25rem}, which otherwise + indents the title 20px past the card body and looks misaligned */ + '.dd-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;padding:0;letter-spacing:.3px;text-transform:uppercase}', '.dd-status-row{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:0}', '.dd-status-row .dd-grow{flex:1 1 auto}', '.dd-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 10px;border-radius:999px;font-size:10.5px;font-weight:700;letter-spacing:.3px;border:1px solid transparent;line-height:1.3}', @@ -33,7 +35,7 @@ const CSS = [ '.dd-switch-wrap{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}', '.dd-backend-card{padding:10px 14px}', '.dd-backend-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}', - '.dd-backend-label{min-width:100px;font-size:12px;font-weight:600;opacity:.72}', + '.dd-backend-label{flex:0 0 calc(200px - 10px);font-size:12px;font-weight:600;opacity:.72}', '.dd-backend-segment{display:inline-flex;align-items:center;gap:2px;padding:2px;border-radius:7px;background:rgba(128,128,128,.10)}', '.dd-backend-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-width:78px;height:24px;padding:0 10px;border:0;border-radius:5px;background:transparent;color:inherit;font-size:11px;font-weight:500;opacity:.65;cursor:pointer;transition:background .18s ease,color .18s ease,opacity .18s ease}', '.dd-backend-btn:hover{background:rgba(128,128,128,.10)}', @@ -41,8 +43,16 @@ const CSS = [ '.dd-backend-btn:disabled{opacity:.55;cursor:not-allowed}', '.dd-backend-state{font-size:10.5px;font-weight:500;opacity:.70;margin-left:2px}', '.dd-backend-btn.is-active .dd-backend-state{opacity:.85}', - '.dd-backend-help{margin:6px 0 0 110px;font-size:11.5px;line-height:1.45;opacity:.66}', - '@media (max-width:640px){.dd-backend-label{min-width:0;width:100%}.dd-backend-segment{width:100%}.dd-backend-btn{flex:1;min-width:0}.dd-backend-help{margin-left:0}}', + '.dd-backend-help{margin:6px 0 0 200px;font-size:11.5px;line-height:1.45;opacity:.66}', + '.dd-backend-help:empty{display:none}', + 'html[data-daede-theme="argon"] .dd-backend-label{flex-basis:calc(240px - 10px)}', + 'html[data-daede-theme="argon"] .dd-backend-help{margin-left:240px}', + 'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-card{background:rgba(255,255,255,.78);border-color:rgba(50,50,93,.12);box-shadow:0 2px 8px rgba(50,50,93,.06)}', + 'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-card-title{opacity:.78}', + 'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-title{opacity:1}', + 'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-field input,html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-field select,html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-field textarea{background:#fff!important;border-color:rgba(50,50,93,.24)!important}', + 'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-adv-bar{background:rgba(50,50,93,.04);border-color:rgba(50,50,93,.14);opacity:.78}', + '@media (max-width:640px){.dd-backend-label{flex-basis:100%!important;width:100%}.dd-backend-segment{width:100%}.dd-backend-btn{flex:1;min-width:0}.dd-backend-help{margin-left:0!important}}', /* daed/dae settings card —— LuCI form.Map 字体/边框对齐 dd 卡片体系 */ '.dd-settings-card{padding:10px 14px}', '.dd-settings-card>h2,.dd-settings-card .cbi-map>h2,.dd-settings-card .cbi-section>h3{display:none}', @@ -50,6 +60,10 @@ const CSS = [ '.dd-settings-descr{font-size:11.5px;opacity:.62;margin:0 0 8px;line-height:1.45}', '.dd-settings-card .cbi-section{margin:0;padding:0;background:transparent;border:0;box-shadow:none}', '.dd-settings-card .cbi-value{padding:6px 0;border:0;min-height:0}', + /* Argon's .td.cbi-value-field padding + 40px .cbi-checkbox make table rows + too tall — tighten both for our compact subscription/node tables */ + '.dd-settings-card .cbi-section-table-row>td{padding:6px 10px !important}', + '.dd-settings-card .cbi-section-table-row .cbi-checkbox{height:20px !important;min-height:0 !important}', '.dd-settings-card .cbi-value-title{font-size:12.5px !important;font-weight:500;opacity:.85;padding:6px 12px 6px 0;min-width:140px}', '.dd-settings-card .cbi-value-field input,.dd-settings-card .cbi-value-field select,.dd-settings-card .cbi-value-field textarea{font-size:12.5px !important;padding:5px 8px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit}', '.dd-settings-card .cbi-value-field input:focus,.dd-settings-card .cbi-value-field select:focus,.dd-settings-card .cbi-value-field textarea:focus{border-color:rgba(56,134,161,.7);outline:0;box-shadow:0 0 0 2px rgba(56,134,161,.15)}', @@ -92,10 +106,14 @@ const CSS = [ '.dd-edit-wrap{position:relative}', '.dd-edit-wrap .dd-editor,.dd-edit-wrap .dd-hl{margin:0;padding:10px 12px;border-width:1px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:12px;line-height:1.5;letter-spacing:0;tab-size:4;white-space:pre-wrap;word-break:break-word;box-sizing:border-box}', '.dd-edit-wrap .dd-hl{position:absolute;inset:0;margin:0;overflow:hidden;border:1px solid transparent;border-radius:6px 6px 0 0;pointer-events:none;background:#f6f8fa;color:#3b4252;z-index:1}', - '.dd-edit-wrap .dd-hl code{font:inherit;white-space:inherit;word-break:inherit;display:block}', + /* reset Argon's code{background:var(--lighter)} so the overlay inherits the + pre background uniformly — otherwise a dark pre shows as side bars */ + '.dd-edit-wrap .dd-hl code{font:inherit;white-space:inherit;word-break:inherit;display:block;background:transparent !important}', '.dd-edit-wrap .dd-editor-hl{position:relative;z-index:2;color:transparent !important;background:transparent !important;caret-color:#2f7288;border:1px solid rgba(128,128,128,.28)}', '.dd-edit-wrap .dd-editor-hl::placeholder{color:rgba(128,128,128,.55)}', - '.dd-edit-wrap .dd-editor-hl::selection{background:rgba(56,134,161,.25)}', + /* keep selected text transparent too — else the browser force-colors it and + it ghosts over the highlight pre underneath */ + '.dd-edit-wrap .dd-editor-hl::selection{background:rgba(56,134,161,.25);color:transparent}', '.dh-c{color:#8a919a;font-style:italic}', '.dh-s{color:#2a8a4a}', '.dh-k{color:#9a3fb5;font-weight:600}', @@ -117,7 +135,8 @@ const CSS = [ '.dd-editor-footer .dd-fb-ok{color:#3da66a;font-weight:600}', 'body.dark .dd-editor-footer,html[data-theme="dark"] .dd-editor-footer,html[data-bs-theme="dark"] .dd-editor-footer,html[data-darkmode="true"] .dd-editor-footer{border-color:rgba(255,255,255,.1);background:rgba(255,255,255,.04)}', 'body.dark .dd-editor-footer .dd-fb-warn,html[data-theme="dark"] .dd-editor-footer .dd-fb-warn,html[data-bs-theme="dark"] .dd-editor-footer .dd-fb-warn,html[data-darkmode="true"] .dd-editor-footer .dd-fb-warn{color:#e0b34a}', - '.dd-insert-select{font-size:11.5px;padding:4px 8px;border-radius:5px;border:1px solid rgba(128,128,128,.35);background:transparent;color:inherit;cursor:pointer}', + /* match the action buttons' size (h25 / 11px / 5px radius) for a consistent row */ + '.dd-insert-select{font-size:11px;line-height:1.4;height:25px;padding:0 10px;border-radius:5px;border:1px solid rgba(128,128,128,.35);background:transparent;color:inherit;cursor:pointer;box-sizing:border-box}', '.dd-insert-select:hover{border-color:rgba(56,134,161,.55)}', '.dd-editor-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:10px}', '.dd-editor-status{margin-left:auto;font-size:11.5px;opacity:0;transition:opacity .25s ease;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}', @@ -152,24 +171,9 @@ const CSS = [ 'body.dark .dd-card,html[data-theme="dark"] .dd-card,html[data-bs-theme="dark"] .dd-card,html[data-darkmode="true"] .dd-card{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}', 'body.dark .dd-adv-bar,html[data-theme="dark"] .dd-adv-bar,html[data-bs-theme="dark"] .dd-adv-bar,html[data-darkmode="true"] .dd-adv-bar{background:rgba(255,255,255,.04);border-color:rgba(255,255,255,.10)}', 'body.dark .dd-settings-card .cbi-value-field input,body.dark .dd-settings-card .cbi-value-field select,body.dark .dd-settings-card .cbi-value-field textarea,html[data-theme="dark"] .dd-settings-card .cbi-value-field input,html[data-theme="dark"] .dd-settings-card .cbi-value-field select,html[data-theme="dark"] .dd-settings-card .cbi-value-field textarea,html[data-bs-theme="dark"] .dd-settings-card .cbi-value-field input,html[data-bs-theme="dark"] .dd-settings-card .cbi-value-field select,html[data-bs-theme="dark"] .dd-settings-card .cbi-value-field textarea,html[data-darkmode="true"] .dd-settings-card .cbi-value-field input,html[data-darkmode="true"] .dd-settings-card .cbi-value-field select,html[data-darkmode="true"] .dd-settings-card .cbi-value-field textarea{border-color:rgba(255,255,255,.18)}', - // Argon dark sets no DOM attr (esp. follow-system) — mirror dark rules via @media - '@media (prefers-color-scheme: dark){', - '.dd-edit-wrap .dd-hl{color:#c8cdd6;background:#1e2228}', - '.dd-edit-wrap .dd-editor-hl{caret-color:#7fd0e8}', - '.dh-c{color:#6b7280}', - '.dh-s{color:#7ec699}', - '.dh-k{color:#c792ea}', - '.dh-f{color:#82c4dd}', - '.dh-o{color:#e0a35a}', - '.dh-g{color:#ec6fa8}', - '.dd-editor-footer{border-color:rgba(255,255,255,.1);background:rgba(255,255,255,.04)}', - '.dd-editor-footer .dd-fb-warn{color:#e0b34a}', - '.dd-ph-warn-title{color:#e0b34a}', - '.dd-ph-list .dd-ph-ln{color:#f0c763;background:rgba(217,158,0,.22)}', - '.dd-card{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}', - '.dd-adv-bar{background:rgba(255,255,255,.04);border-color:rgba(255,255,255,.10)}', - '.dd-settings-card .cbi-value-field input,.dd-settings-card .cbi-value-field select,.dd-settings-card .cbi-value-field textarea{border-color:rgba(255,255,255,.18)}', - '}', + /* No prefers-color-scheme dark block: OS dark mode must NOT force a dark + editor when the user picked a LIGHT theme. Dark styling is driven solely + by data-darkmode (config.js reads the real page background). */ /* status card line-2 meta (backend · pid), below the badge+toggle line */ '.dd-status-meta{display:flex;flex-wrap:wrap;gap:10px;margin-top:6px}', /* compact icon-only row actions — the 编辑/删除 text buttons are too wide @@ -196,6 +200,9 @@ const CSS = [ '.dd-settings-card .cbi-section-table-row>td:nth-child(2){flex:1 1 auto !important}', '.dd-settings-card .cbi-section-table-row>td:nth-child(3){flex:0 0 auto !important;text-align:center}', '.dd-settings-card .cbi-section-table-row>td.cbi-section-actions{flex:0 0 auto !important;display:flex;gap:4px}', + /* clear the floated title so a Flag description gets the full row width + instead of wrapping around it (e.g. "局域网 / 私有地址不走代理。") */ + '.dd-settings-card .cbi-value-description,.dd-settings-card .cbi-value-helptext{clear:both;width:100%}', '}', '.dd-converter{width:100%!important;max-width:1180px;margin:0 auto}', '.dd-conv-card{padding:18px 20px;margin-bottom:12px}', diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/updates.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/updates.js index b15bbe85..d73cc859 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/updates.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/updates.js @@ -55,6 +55,8 @@ const CSS = [ '.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%}', + /* fixed width so the Source and Auto-update selects line up consistently */ + '.dd-geo-row select{width:180px;flex:0 0 auto}', '.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}', diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/widgets.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/widgets.js index 08aac3d2..f793b933 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/widgets.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/widgets.js @@ -8,6 +8,14 @@ function notifyAction() {} +function execChecked(command, args) { + return fs.exec(command, args || []).then(function(res) { + if (res && res.code !== 0) + throw new Error((res.stderr || res.stdout || ('exit ' + res.code)).trim()); + return res; + }); +} + function execInit(be, action) { return fs.exec(be.initd, [action]).then(function(res) { notifyAction(action, res); @@ -22,30 +30,43 @@ function rejectIfOtherRunning(be, running) { return Promise.reject(new Error(_('%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment.').format(other, other, be.name))); } -function toggleService(be, turnOn, running) { +function toggleService(be, turnOn) { const enabled = turnOn ? '1' : '0'; const action = turnOn ? 'start' : 'stop'; // Only guard on start: two backends share the eBPF/cgroup attachment, so // starting one while the other runs is unsafe. Stopping is always allowed. - const guard = turnOn ? rejectIfOtherRunning(be, running) : Promise.resolve(); + const guard = turnOn + ? backend.detectRunning().then(function(running) { return rejectIfOtherRunning(be, running); }) + : Promise.resolve(); return guard - .then(function() { return fs.exec('/sbin/uci', ['set', be.uci + '.config.enabled=' + enabled]); }) - .then(function() { return fs.exec('/sbin/uci', ['commit', be.uci]); }) + .then(function() { return execChecked('/sbin/uci', ['set', be.uci + '.config.enabled=' + enabled]); }) + .then(function() { return execChecked('/sbin/uci', ['commit', be.uci]); }) .then(function() { if (turnOn) - return fs.exec(be.initd, ['enable']); - return fs.exec(be.initd, ['disable']); + return execChecked(be.initd, ['enable']); + return execChecked(be.initd, ['disable']); }) .then(function() { if (turnOn && be.useNetns) return fs.exec('/sbin/ip', ['netns', 'del', 'daens']).catch(function() {}); }) - .then(function() { return fs.exec(be.initd, [action]); }); + .then(function() { return execChecked(be.initd, [action]); }); } -function renderBackendSwitcher(ctx) { +function waitForService(be, turnOn, attempts) { + return backend.serviceStatus(be.name).then(function(state) { + if (!!state.running === turnOn) + return state; + if (attempts <= 0) + throw new Error(turnOn ? _('Service did not start in time.') : _('Service did not stop in time.')); + return new Promise(function(resolve) { setTimeout(resolve, 250); }) + .then(function() { return waitForService(be, turnOn, attempts - 1); }); + }); +} + +function renderBackendSwitcher(ctx, onSwitched, initialHint) { if (!ctx.installed.dae && !ctx.installed.daed) return null; @@ -57,7 +78,7 @@ function renderBackendSwitcher(ctx) { ]) ]); const segment = wrap.querySelector('.dd-backend-segment'); - const hint = E('div', { 'class': 'dd-backend-help' }, _('Switching backend stops the current service first. Click start when you want the new backend to run.')); + const hint = E('div', { 'class': 'dd-backend-help' }, initialHint || ''); let busy = false; const showHint = function(msg) { @@ -69,7 +90,8 @@ function renderBackendSwitcher(ctx) { ['dae', 'daed'].forEach(function(name) { if (running && running[name]) - stops.push(fs.exec(backend.BACKENDS[name].initd, ['stop']).catch(function() {})); + stops.push(execChecked(backend.BACKENDS[name].initd, ['stop']) + .then(function() { return waitForService(backend.BACKENDS[name], false, 40); })); }); if (stops.length) @@ -107,8 +129,9 @@ function renderBackendSwitcher(ctx) { .then(stopIfRunning) .then(function() { return backend.setActiveBackend(name); }) .then(function() { - showHint(_('Switched to %s. Click start when you want it to run.').format(name)); - setTimeout(function() { window.location.reload(); }, 650); + const message = _('Switched to %s. Click start when you want it to run.').format(name); + showHint(message); + return onSwitched ? onSwitched(name, message) : null; }) .catch(function(e) { busy = false; @@ -125,13 +148,16 @@ function renderBackendSwitcher(ctx) { function renderStatusCard(ctx, listenAddr) { const be = ctx.backend; + let busy = false; + let lastError = ''; + let refreshGeneration = 0; const body = E('div', { 'id': 'dd-status-body' }, E('em', {}, _('Collecting data…'))); - const card = E('div', { 'class': 'dd-card' }, [ + const card = E('div', { 'class': 'dd-card dd-status-card' }, [ E('h4', { 'class': 'dd-card-title' }, _('Service Status')), body ]); - const render = function(state, running) { + const render = function(state) { while (body.firstChild) body.removeChild(body.firstChild); const badge = state.running @@ -145,14 +171,17 @@ function renderStatusCard(ctx, listenAddr) { if (state.running && state.pid) meta.push(E('span', { 'class': 'dd-meta' }, [ E('span', { 'class': 'dd-meta-label' }, 'PID'), state.pid ])); - const swErr = E('span', { 'class': 'dd-meta dd-err', 'style': 'display:none' }, ''); + const swErr = E('span', { 'class': 'dd-meta dd-err', 'style': lastError ? '' : 'display:none' }, lastError); const sw = E('button', { 'class': 'dd-switch' + (state.running ? ' is-on' : ''), 'type': 'button', 'aria-label': _('Toggle service') }, [ E('span', { 'class': 'dd-switch-knob' }) ]); sw.addEventListener('click', function(ev) { ev.preventDefault(); - if (sw.disabled) return; + if (busy) return; + refreshGeneration++; + busy = true; sw.disabled = true; + lastError = ''; swErr.style.display = 'none'; const turnOn = !state.running; /* instant optimistic feedback — the start/stop chain (esp. dae's eBPF @@ -161,18 +190,17 @@ function renderStatusCard(ctx, listenAddr) { sw.classList.toggle('is-on', turnOn); const lbl = sw.parentNode && sw.parentNode.querySelector('.dd-switch-label'); if (lbl) lbl.textContent = '…'; - toggleService(be, turnOn, running) - /* refresh as soon as the command returns, not on the next poll - tick — this is what made feedback feel laggy */ - .then(function() { return refresh(); }) - .catch(function(e) { - /* revert the optimistic flip and show the reason inline */ - sw.classList.toggle('is-on', !turnOn); - if (lbl) lbl.textContent = state.running ? 'ON' : 'OFF'; - swErr.textContent = _('Toggle failed: %s').format(e.message || e); - swErr.style.display = ''; + toggleService(be, turnOn) + .then(function() { return waitForService(be, turnOn, 40); }) + .then(function() { + busy = false; + return refresh(true); }) - .finally(function() { sw.disabled = false; }); + .catch(function(e) { + busy = false; + lastError = _('Toggle failed: %s').format(e.message || e); + return refresh(true); + }); }); /* line 1: badge + toggle (always aligned, never wraps); line 2: meta */ @@ -256,17 +284,19 @@ function renderStatusCard(ctx, listenAddr) { body.appendChild(E('div', { 'class': 'dd-actions' }, actions)); }; - const refresh = function() { - return Promise.all([ - backend.serviceStatus(be.name), - backend.detectRunning() - ]).then(function(r) { - render(r[0], r[1]); + const refresh = function(force) { + if (busy && !force) + return Promise.resolve(); + const generation = refreshGeneration; + return backend.serviceStatus(be.name).then(function(state) { + if (generation !== refreshGeneration || (busy && !force)) return; + render(state); }); }; poll.add(refresh); refresh(); + card._ddCleanup = function() { poll.remove(refresh); }; return card; } diff --git a/luci-app-daede/po/templates/daede.pot b/luci-app-daede/po/templates/daede.pot index 087c653f..16e04b94 100644 --- a/luci-app-daede/po/templates/daede.pot +++ b/luci-app-daede/po/templates/daede.pot @@ -49,9 +49,6 @@ msgstr "" msgid "%s is not installed. Install it first, then switch backend." msgstr "" -msgid "Switching backend stops the current service first. Click start when you want the new backend to run." -msgstr "" - msgid "Stopping current backend…" msgstr "" @@ -82,21 +79,21 @@ msgstr "" msgid "Toggle failed: %s" msgstr "" +msgid "Service did not start in time." +msgstr "" + +msgid "Service did not stop in time." +msgstr "" + msgid "%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment." msgstr "" msgid "daede Settings" msgstr "" -msgid "A modern dashboard for dae. Subscriptions, nodes, routing and DNS are managed in the daed WebUI." -msgstr "" - msgid "Listen Address" msgstr "" -msgid "Host:port that the daed WebUI and GraphQL API listen on." -msgstr "" - msgid "Max Log Size (MB)" msgstr "" diff --git a/luci-app-daede/po/zh-cn/daede.po b/luci-app-daede/po/zh-cn/daede.po index 7ac80335..e6ef851b 100644 --- a/luci-app-daede/po/zh-cn/daede.po +++ b/luci-app-daede/po/zh-cn/daede.po @@ -58,9 +58,6 @@ msgstr "%s 未安装" msgid "%s is not installed. Install it first, then switch backend." msgstr "%s 未安装。请先安装再切换后端。" -msgid "Switching backend stops the current service first. Click start when you want the new backend to run." -msgstr "切换后端会先停止当前服务。点击开关启动新后端即可。" - msgid "Stopping current backend…" msgstr "停止当前后端中…" @@ -92,6 +89,12 @@ msgstr "操作 \"%s\" 成功。" msgid "Toggle failed: %s" msgstr "开关切换失败:%s" +msgid "Service did not start in time." +msgstr "服务未能及时启动。" + +msgid "Service did not stop in time." +msgstr "服务未能及时停止。" + msgid "%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment." msgstr "%s 已在运行。%s 与 %s 共用 eBPF/cgroup 挂载点,请先停止前者。" @@ -99,15 +102,9 @@ msgstr "%s 已在运行。%s 与 %s 共用 eBPF/cgroup 挂载点,请先停止 msgid "daede Settings" msgstr "daede 设置" -msgid "A modern dashboard for dae. Subscriptions, nodes, routing and DNS are managed in the daed WebUI." -msgstr "dae 的现代化控制台。订阅、节点、路由和 DNS 请在 daed WebUI 中管理。" - msgid "Listen Address" msgstr "监听地址" -msgid "Host:port that the daed WebUI and GraphQL API listen on." -msgstr "daed WebUI 与 GraphQL API 监听的 Host:Port。" - msgid "Max Log Size (MB)" msgstr "单文件大小上限(MB)" @@ -146,8 +143,8 @@ msgstr "dae 没有内置 WebUI,下面的 DSL 配置需要手工编辑(订阅 msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here." msgstr "dae 配置文件为空。点「从示例初始化」开始,或直接粘贴你的配置。" -msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI." -msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。" +msgid "Edit the dae DSL directly. Initialize loads a template. Use daed for a GUI." +msgstr "直接编辑 dae DSL,「从示例初始化」载入模板。图形界面用 daed。" msgid "Save and Hot Reload" msgstr "保存并热重载" @@ -559,9 +556,6 @@ msgstr "兜底 DNS 上游" msgid "Resolves everything else." msgstr "解析其余域名。" -msgid "Add a subscription or node, then save — it takes effect automatically." -msgstr "填好订阅或节点,点保存就自动生效。" - msgid "Apply failed: %s" msgstr "应用失败:%s" @@ -592,9 +586,6 @@ msgstr "导入失败" msgid "Advanced / Manual Mode" msgstr "高级 / 手动模式" -msgid "Edit config.dae directly. Saving the form above regenerates the file and overwrites changes made here." -msgstr "直接编辑 config.dae。上方表单保存会重新生成文件并覆盖这里的改动。" - msgid "Update Subscriptions" msgstr "更新订阅" @@ -701,8 +692,8 @@ msgstr "启动失败:%s" msgid "Subscription Converter" msgstr "订阅转换" -msgid "Convert Clash YAML into share links, preview the result, then import selected nodes into dae or daed. Inputs and credentials are not saved." -msgstr "将 Clash YAML 转换为分享链接,预览后把所选节点导入 dae 或 daed。输入内容和凭据不会保存。" +msgid "Convert Clash YAML to share links and import the nodes you pick. Nothing is saved." +msgstr "把 Clash YAML 转成分享链接,挑选节点导入。输入不会保存。" msgid "1. Input Clash YAML" msgstr "1. 输入 Clash YAML" diff --git a/luci-app-daede/po/zh_Hans/daede.po b/luci-app-daede/po/zh_Hans/daede.po index 7ac80335..e6ef851b 100644 --- a/luci-app-daede/po/zh_Hans/daede.po +++ b/luci-app-daede/po/zh_Hans/daede.po @@ -58,9 +58,6 @@ msgstr "%s 未安装" msgid "%s is not installed. Install it first, then switch backend." msgstr "%s 未安装。请先安装再切换后端。" -msgid "Switching backend stops the current service first. Click start when you want the new backend to run." -msgstr "切换后端会先停止当前服务。点击开关启动新后端即可。" - msgid "Stopping current backend…" msgstr "停止当前后端中…" @@ -92,6 +89,12 @@ msgstr "操作 \"%s\" 成功。" msgid "Toggle failed: %s" msgstr "开关切换失败:%s" +msgid "Service did not start in time." +msgstr "服务未能及时启动。" + +msgid "Service did not stop in time." +msgstr "服务未能及时停止。" + msgid "%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment." msgstr "%s 已在运行。%s 与 %s 共用 eBPF/cgroup 挂载点,请先停止前者。" @@ -99,15 +102,9 @@ msgstr "%s 已在运行。%s 与 %s 共用 eBPF/cgroup 挂载点,请先停止 msgid "daede Settings" msgstr "daede 设置" -msgid "A modern dashboard for dae. Subscriptions, nodes, routing and DNS are managed in the daed WebUI." -msgstr "dae 的现代化控制台。订阅、节点、路由和 DNS 请在 daed WebUI 中管理。" - msgid "Listen Address" msgstr "监听地址" -msgid "Host:port that the daed WebUI and GraphQL API listen on." -msgstr "daed WebUI 与 GraphQL API 监听的 Host:Port。" - msgid "Max Log Size (MB)" msgstr "单文件大小上限(MB)" @@ -146,8 +143,8 @@ msgstr "dae 没有内置 WebUI,下面的 DSL 配置需要手工编辑(订阅 msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here." msgstr "dae 配置文件为空。点「从示例初始化」开始,或直接粘贴你的配置。" -msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI." -msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。" +msgid "Edit the dae DSL directly. Initialize loads a template. Use daed for a GUI." +msgstr "直接编辑 dae DSL,「从示例初始化」载入模板。图形界面用 daed。" msgid "Save and Hot Reload" msgstr "保存并热重载" @@ -559,9 +556,6 @@ msgstr "兜底 DNS 上游" msgid "Resolves everything else." msgstr "解析其余域名。" -msgid "Add a subscription or node, then save — it takes effect automatically." -msgstr "填好订阅或节点,点保存就自动生效。" - msgid "Apply failed: %s" msgstr "应用失败:%s" @@ -592,9 +586,6 @@ msgstr "导入失败" msgid "Advanced / Manual Mode" msgstr "高级 / 手动模式" -msgid "Edit config.dae directly. Saving the form above regenerates the file and overwrites changes made here." -msgstr "直接编辑 config.dae。上方表单保存会重新生成文件并覆盖这里的改动。" - msgid "Update Subscriptions" msgstr "更新订阅" @@ -701,8 +692,8 @@ msgstr "启动失败:%s" msgid "Subscription Converter" msgstr "订阅转换" -msgid "Convert Clash YAML into share links, preview the result, then import selected nodes into dae or daed. Inputs and credentials are not saved." -msgstr "将 Clash YAML 转换为分享链接,预览后把所选节点导入 dae 或 daed。输入内容和凭据不会保存。" +msgid "Convert Clash YAML to share links and import the nodes you pick. Nothing is saved." +msgstr "把 Clash YAML 转成分享链接,挑选节点导入。输入不会保存。" msgid "1. Input Clash YAML" msgstr "1. 输入 Clash YAML" diff --git a/luci-app-daede/root/etc/uci-defaults/90-luci-app-daede-init b/luci-app-daede/root/etc/uci-defaults/90-luci-app-daede-init index 2c2650c6..44e0694e 100644 --- a/luci-app-daede/root/etc/uci-defaults/90-luci-app-daede-init +++ b/luci-app-daede/root/etc/uci-defaults/90-luci-app-daede-init @@ -78,6 +78,9 @@ fi # Idempotent: only add what's missing. Lives in the `dae` UCI package, # consumed by gen-dae-config.sh. if [ -f /etc/config/dae ]; then + if ! uci -q get dae.config.lan_interface >/dev/null 2>&1; then + uci -q set dae.config.lan_interface='br-lan' + fi if ! uci -q show dae | grep -q "=group$"; then g="$(uci add dae group)" uci -q set "dae.$g.name=proxy" diff --git a/luci-app-daede/root/usr/share/luci-app-daede/gen-dae-config.sh b/luci-app-daede/root/usr/share/luci-app-daede/gen-dae-config.sh index 2e2fdc0b..a6e8920f 100755 --- a/luci-app-daede/root/usr/share/luci-app-daede/gen-dae-config.sh +++ b/luci-app-daede/root/usr/share/luci-app-daede/gen-dae-config.sh @@ -194,7 +194,7 @@ generate() { config_get dial_mode config dial_mode "domain" config_get log_level config log_level "info" config_get wan_interface config wan_interface "auto" - config_get lan_interface config lan_interface "" + config_get lan_interface config lan_interface "br-lan" SUB_BUF=""; NODE_BUF=""; GROUP_BUF=""; FIRST_GROUP=""; GROUP_NAMES_SEEN="" SUB_TAGS="" diff --git a/luci-app-homeproxy/.gitignore b/luci-app-homeproxy/.gitignore new file mode 100644 index 00000000..e43b0f98 --- /dev/null +++ b/luci-app-homeproxy/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/luci-app-homeproxy/LICENSE b/luci-app-homeproxy/LICENSE new file mode 100644 index 00000000..d60c31a9 --- /dev/null +++ b/luci-app-homeproxy/LICENSE @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/luci-app-homeproxy/Makefile b/luci-app-homeproxy/Makefile index 2f34e39f..14166586 100644 --- a/luci-app-homeproxy/Makefile +++ b/luci-app-homeproxy/Makefile @@ -1,16 +1,19 @@ # SPDX-License-Identifier: GPL-2.0-only # -# Copyright (C) 2022-2024 ImmortalWrt.org +# Copyright (C) 2022-2023 ImmortalWrt.org include $(TOPDIR)/rules.mk LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64 +LUCI_PKGARCH:=all LUCI_DEPENDS:= \ +sing-box \ +firewall4 \ +kmod-nft-tproxy \ +ucode-mod-digest +PKG_NAME:=luci-app-homeproxy + define Package/luci-app-homeproxy/conffiles /etc/config/homeproxy /etc/homeproxy/certs/ diff --git a/luci-app-homeproxy/README b/luci-app-homeproxy/README new file mode 100644 index 00000000..aea47fe6 --- /dev/null +++ b/luci-app-homeproxy/README @@ -0,0 +1,5 @@ +TODO: +- Subscription page slow response with a large number of nodes +- Refactor nft rules +- Move ACL settings to a dedicated page +- Any other improvements diff --git a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/client.js b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/client.js index f012fddf..823efae6 100644 --- a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/client.js +++ b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/client.js @@ -1543,6 +1543,87 @@ return view.extend({ /* Direct domain list end */ /* ACL settings end */ + /* Dashboard settings start */ + s.tab('dashboard', _('Dashboard Settings')); + + o = s.taboption('dashboard', form.Flag, 'dashboard_enabled', _('Enable Dashboard'), + _('Enable sing-box API and dashboard.
Warning: The API will be accessible to all devices on the LAN.')); + o.default = o.disabled; + o.rmempty = false; + + o = s.taboption('dashboard', form.Value, 'dashboard_secret', _('Dashboard Secret'), + _('API authentication secret. Required for security since the API is LAN-accessible.')); + o.password = true; + o.depends('dashboard_enabled', '1'); + o.rmempty = false; + o.validate = function(section_id, value) { + let enabled = this.section.formvalue(section_id, 'dashboard_enabled'); + if (enabled === '1') { + if (!value || value.length < 6) + return _('Dashboard secret must be at least 6 characters.'); + } + return true; + }; + + o = s.taboption('dashboard', form.Value, 'dashboard_port', _('Dashboard Port'), + _('The listen port for sing-box API and dashboard.')); + o.datatype = 'port'; + o.placeholder = '9090'; + o.depends('dashboard_enabled', '1'); + + o = s.taboption('dashboard', form.Value, 'dashboard_url', _('Dashboard URL'), + _('URL to download dashboard files. Leave empty to use Zashboard by default.')); + o.placeholder = 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn.zip'; + o.depends('dashboard_enabled', '1'); + + o = s.taboption('dashboard', form.ListValue, 'dashboard_detour', _('Download outbound'), + _('Outbound used to download dashboard files.')); + o.load = function(section_id) { + delete this.keylist; + delete this.vallist; + + this.value('direct-out', _('Direct')); + let routing_mode = uci.get('homeproxy', 'config', 'routing_mode'); + if (routing_mode !== 'custom') { + let main_node = uci.get('homeproxy', 'config', 'main_node'); + if (main_node && main_node !== 'nil') + this.value('main-out', _('Main node (proxy)')); + } else { + uci.sections(data[0], 'routing_node', (res) => { + if (res.enabled === '1') + this.value(res['.name'], res.label); + }); + } + + return this.super('load', section_id); + }; + o.default = 'direct-out'; + o.depends('dashboard_enabled', '1'); + + o = s.taboption('dashboard', form.DummyValue, '_dashboard_open', _('Open Dashboard')); + o.depends('dashboard_enabled', '1'); + o.rawhtml = true; + o.cfgvalue = function(section_id) { + let port = uci.get('homeproxy', 'config', 'dashboard_port') || '9090'; + let host = window.location.hostname; + let url = 'http://' + host + ':' + port + '/ui/'; + return E('div', {}, [ + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': function() { + window.open(url, '_blank'); + } + }, [ _('Open Dashboard') ]), + E('div', { 'style': 'margin-top:8px; color:#888; font-size:90%' }, [ + E('div', {}, _('Dashboard URL: %s').format(url)), + E('div', { 'style': 'margin-top:4px' }, + _('If the dashboard asks for connection settings, fill in: Host = %s, Port = %s, Secret = your configured secret above.').format(host, port) + ) + ]) + ]); + }; + /* Dashboard settings end */ + return m.render(); } }); diff --git a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/status.js b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/status.js index f8a61700..9c1efbd6 100644 --- a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/status.js +++ b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/status.js @@ -47,7 +47,7 @@ function getConnStat(o, site) { let ele = o.default.firstElementChild.nextElementSibling; if (ret.result) { ele.style.setProperty('color', 'green'); - ele.innerHTML = _('passed'); + ele.innerHTML = ret.elapsed ? _('passed (%dms)').format(ret.elapsed) : _('passed'); } else { ele.style.setProperty('color', 'red'); ele.innerHTML = _('failed'); @@ -238,6 +238,24 @@ return view.extend({ o = s.option(form.DummyValue, '_check_google', _('Google')); o.cfgvalue = L.bind(getConnStat, this, o, 'google'); + if (uci.get('homeproxy', 'config', 'dashboard_enabled') === '1') { + s = m.section(form.NamedSection, 'config', 'homeproxy', _('Dashboard')); + s.anonymous = true; + + o = s.option(form.DummyValue, '_dashboard_btn', _('Zashboard')); + o.rawhtml = true; + o.cfgvalue = function(section_id) { + let port = uci.get('homeproxy', 'config', 'dashboard_port') || '9090'; + return E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': function() { + let url = window.location.protocol + '//' + window.location.hostname + ':' + port + '/ui/'; + window.open(url, '_blank'); + } + }, [ _('Open Dashboard') ]); + }; + } + s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management')); s.anonymous = true; diff --git a/luci-app-homeproxy/po/templates/homeproxy.pot b/luci-app-homeproxy/po/templates/homeproxy.pot index 0fa71655..caaa2b1f 100644 --- a/luci-app-homeproxy/po/templates/homeproxy.pot +++ b/luci-app-homeproxy/po/templates/homeproxy.pot @@ -2958,6 +2958,9 @@ msgstr "" msgid "passed" msgstr "" +msgid "passed (%dms)" +msgstr "" + #: htdocs/luci-static/resources/view/homeproxy/server.js:791 msgid "private key" msgstr "" @@ -3049,3 +3052,57 @@ msgstr "" #: htdocs/luci-static/resources/homeproxy.js:328 msgid "valid uuid" msgstr "" + +msgid "Dashboard Settings" +msgstr "" + +msgid "Enable Dashboard" +msgstr "" + +msgid "Enable sing-box API and dashboard.
Warning: The API will be accessible to all devices on the LAN." +msgstr "" + +msgid "Open Dashboard" +msgstr "" + +msgid "Dashboard Secret" +msgstr "" + +msgid "API authentication secret. Required for security since the API is LAN-accessible." +msgstr "" + +msgid "Dashboard secret must be at least 6 characters." +msgstr "" + +msgid "Dashboard Port" +msgstr "" + +msgid "The listen port for sing-box API and dashboard." +msgstr "" + +msgid "Dashboard URL" +msgstr "" + +msgid "URL to download dashboard files. Leave empty to use Zashboard by default." +msgstr "" + +msgid "Download outbound" +msgstr "" + +msgid "Outbound used to download dashboard files." +msgstr "" + +msgid "Main node (proxy)" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Zashboard" +msgstr "" + +msgid "Dashboard URL: %s" +msgstr "" + +msgid "If the dashboard asks for connection settings, fill in: Host = %s, Port = %s, Secret = your configured secret above." +msgstr "" diff --git a/luci-app-homeproxy/po/zh_Hans/homeproxy.po b/luci-app-homeproxy/po/zh_Hans/homeproxy.po index 68d3e066..a7d7c30b 100644 --- a/luci-app-homeproxy/po/zh_Hans/homeproxy.po +++ b/luci-app-homeproxy/po/zh_Hans/homeproxy.po @@ -3027,6 +3027,9 @@ msgstr "packet addr (v2ray-core v5+)" msgid "passed" msgstr "通过" +msgid "passed (%dms)" +msgstr "通过(%dms)" + #: htdocs/luci-static/resources/view/homeproxy/server.js:791 msgid "private key" msgstr "私钥" @@ -3119,3 +3122,57 @@ msgstr "有效端口值" #: htdocs/luci-static/resources/homeproxy.js:328 msgid "valid uuid" msgstr "有效 uuid" + +msgid "Dashboard Settings" +msgstr "面板设置" + +msgid "Enable Dashboard" +msgstr "启用面板" + +msgid "Enable sing-box API and dashboard.
Warning: The API will be accessible to all devices on the LAN." +msgstr "启用 sing-box API 和面板。
警告:API 将对局域网内所有设备开放访问。" + +msgid "Open Dashboard" +msgstr "打开面板" + +msgid "Dashboard Secret" +msgstr "面板密钥" + +msgid "API authentication secret. Required for security since the API is LAN-accessible." +msgstr "API 认证密钥。由于 API 可被局域网访问,必须设置此密钥以确保安全。" + +msgid "Dashboard secret must be at least 6 characters." +msgstr "面板密钥长度不能少于 6 位。" + +msgid "Dashboard Port" +msgstr "面板端口" + +msgid "The listen port for sing-box API and dashboard." +msgstr "sing-box API 和面板的监听端口。" + +msgid "Dashboard URL" +msgstr "面板下载地址" + +msgid "URL to download dashboard files. Leave empty to use Zashboard by default." +msgstr "面板文件的下载地址。留空则默认使用 Zashboard。" + +msgid "Download outbound" +msgstr "下载出站" + +msgid "Outbound used to download dashboard files." +msgstr "用于下载面板文件的出站节点。" + +msgid "Main node (proxy)" +msgstr "主节点(代理)" + +msgid "Dashboard" +msgstr "面板" + +msgid "Zashboard" +msgstr "Zashboard" + +msgid "Dashboard URL: %s" +msgstr "面板地址:%s" + +msgid "If the dashboard asks for connection settings, fill in: Host = %s, Port = %s, Secret = your configured secret above." +msgstr "若面板要求填写连接设置,请填入:Host(主机)= %s,Port(端口)= %s,Secret(密钥)= 上方配置的面板密钥。" diff --git a/luci-app-homeproxy/root/etc/config/homeproxy b/luci-app-homeproxy/root/etc/config/homeproxy index 1a53b56e..5e4272cf 100644 --- a/luci-app-homeproxy/root/etc/config/homeproxy +++ b/luci-app-homeproxy/root/etc/config/homeproxy @@ -33,6 +33,9 @@ config homeproxy 'config' option ipv6_support '1' option github_token '' option log_level 'warn' + option dashboard_enabled '0' + option dashboard_port '9090' + option dashboard_secret '' config homeproxy 'control' option lan_proxy_mode 'disabled' diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc b/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc index 0f464c5a..25a2a1e7 100755 --- a/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc +++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc @@ -52,6 +52,12 @@ const dns_port = uci.get(uciconfig, uciinfra, 'dns_port') || '5333'; const ntp_server = uci.get(uciconfig, uciinfra, 'ntp_server') || 'time.apple.com'; +const dashboard_enabled = uci.get(uciconfig, ucimain, 'dashboard_enabled') || '0'; +const dashboard_port = uci.get(uciconfig, ucimain, 'dashboard_port') || '9090'; +const dashboard_secret = uci.get(uciconfig, ucimain, 'dashboard_secret'); +const dashboard_url = uci.get(uciconfig, ucimain, 'dashboard_url'); +const dashboard_detour = uci.get(uciconfig, ucimain, 'dashboard_detour'); + const ipv6_support = uci.get(uciconfig, ucimain, 'ipv6_support') || '0'; let main_node, main_udp_node, dedicated_udp_node, default_outbound, default_outbound_dns, @@ -327,8 +333,8 @@ function generate_outbound(node) { max_early_data: strToInt(node.websocket_early_data), early_data_header_name: node.websocket_early_data_header, service_name: node.grpc_servicename, - idle_timeout: (node.http_idle_timeout), - ping_timeout: (node.http_ping_timeout), + idle_timeout: strToTime(node.http_idle_timeout), + ping_timeout: strToTime(node.http_ping_timeout), permit_without_stream: strToBool(node.grpc_permit_without_stream) } : null, udp_over_tcp: (node.udp_over_tcp === '1') ? { @@ -565,6 +571,7 @@ if (!isEmpty(main_node)) { user: cfg.user, rule_set: get_ruleset(cfg.rule_set), rule_set_ip_cidr_match_source: strToBool(cfg.rule_set_ip_cidr_match_source), + rule_set_ip_cidr_accept_empty: strToBool(cfg.rule_set_ip_cidr_accept_empty), invert: strToBool(cfg.invert), outbound: get_outbound(cfg.outbound), action: cfg.action, @@ -969,6 +976,28 @@ if (routing_mode in ['bypass_mainland_china', 'custom']) { } }; } + +if (dashboard_enabled === '1') { + if (!config.experimental) + config.experimental = {}; + + let download_detour = 'direct-out'; + if (!isEmpty(dashboard_detour)) { + if (dashboard_detour in ['direct-out', 'main-out']) + download_detour = dashboard_detour; + else + download_detour = get_outbound(dashboard_detour); + } + + config.experimental.clash_api = { + external_controller: '0.0.0.0:' + dashboard_port, + secret: !isEmpty(dashboard_secret) ? dashboard_secret : null, + external_ui: RUN_DIR + '/ui', + external_ui_download_url: !isEmpty(dashboard_url) ? dashboard_url : + 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn.zip', + external_ui_download_detour: download_detour, + }; +} /* Experimental end */ system('mkdir -p ' + RUN_DIR); diff --git a/luci-app-homeproxy/root/usr/share/rpcd/ucode/luci.homeproxy b/luci-app-homeproxy/root/usr/share/rpcd/ucode/luci.homeproxy index 68fad464..7c6d0685 100644 --- a/luci-app-homeproxy/root/usr/share/rpcd/ucode/luci.homeproxy +++ b/luci-app-homeproxy/root/usr/share/rpcd/ucode/luci.homeproxy @@ -137,7 +137,12 @@ const methods = { break; } - return { result: (system(`/usr/bin/wget --spider -qT3 ${url} 2>"/dev/null"`, 3100) === 0) }; + let start = clock(true); + let ret = system(`/usr/bin/wget --spider -qT3 ${url} 2>"/dev/null"`, 3100); + let end = clock(true); + let elapsed = (end[0] - start[0]) * 1000 + int((end[1] - start[1]) / 1000000); + + return { result: (ret === 0), elapsed: elapsed }; } }, @@ -163,7 +168,7 @@ const methods = { const type = req.args?.type; let result = {}; - const fd = popen('/usr/bin/sing-box generate ' + type + ` ${req.args?.params || ''}`); + const fd = popen(`/usr/bin/sing-box generate ${type} ${shellquote(req.args?.params || '')}`); if (fd) { let ech_cfg_set = false; let ech_key_set = false;