dsh-provider-proxy 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -5
- package/lib/client.js +1922 -236
- package/lib/client.js.map +4 -4
- package/lib/index.js +443 -32
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -39,13 +39,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
39
39
|
var require_client = __commonJS({
|
|
40
40
|
"src/client.css"(exports, module2) {
|
|
41
41
|
"use strict";
|
|
42
|
-
var __dshMap = { "root": "
|
|
42
|
+
var __dshMap = { "root": "53783ebc_root", "lead": "87ded16f_lead", "empty": "f139472e_empty", "card": "99d44a8a_card", "cardActive": "6bfd1d28_cardActive", "cardRejected": "ea4afe84_cardRejected", "defaultCard": "6c49773c_defaultCard", "defaultLead": "8e32da71_defaultLead", "cardHead": "d2cf61e5_cardHead", "cardTitles": "c6ef43cd_cardTitles", "name": "6dad9d13_name", "id": "dd160a22_id", "pillOn": "4b27b813_pillOn", "pillLive": "6e7a5d11_pillLive", "pillOff": "7951c4c2_pillOff", "pillRejected": "4756f162_pillRejected", "toggle": "7c4f39f6_toggle", "field": "ebe8e1a3_field", "label": "d6a07995_label", "hint": "5b4fb148_hint", "input": "30a51506_input", "passwordToggle": "fcfbab02_passwordToggle", "testButton": "b3231562_testButton", "diag": "184b2d06_diag", "diagSummary": "49005df6_diagSummary", "diagList": "7f1b6335_diagList", "chipRow": "406f7239_chipRow", "chip": "446f9f30_chip", "chipActive": "152509e7_chipActive", "formGrid": "6fb245a3_formGrid", "formGridFull": "c89ac7ba_formGridFull", "controlRow": "763cfc36_controlRow", "toolbarRow": "97a483ad_toolbarRow", "compactRow": "342803fe_compactRow", "compactRowActive": "b8cadbfb_compactRowActive", "compactRowRejected": "9ccd93bd_compactRowRejected", "copyButton": "dcf70ff1_copyButton", "profileCard": "9889cd73_profileCard", "panel": "64d062a3_panel" };
|
|
43
43
|
var __dshStyles = globalThis.__dshPluginStyles || (globalThis.__dshPluginStyles = /* @__PURE__ */ new Set());
|
|
44
44
|
if (!__dshStyles.has("dsh-provider-proxy/client.css")) {
|
|
45
45
|
__dshStyles.add("dsh-provider-proxy/client.css");
|
|
46
46
|
__dshStyle = document.createElement("style");
|
|
47
47
|
__dshStyle.setAttribute("data-plugin-css", "dsh-provider-proxy/client.css");
|
|
48
|
-
__dshStyle.textContent = '/* dsh-provider-proxy \u2014 provider proxy settings page.\n Tokens only: no literal colours, matching the shared --dsw-* system. */\n\n.b4bf55bf_root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-width: 640px;\n}\n\n.03b72018_lead {\n margin: 0 0 4px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\n}\n\n.9a9a9c27_empty {\n margin: 0;\n padding: 16px;\n border: 1px dashed var(--dsw-alias-border-l1);\n border-radius: 8px;\n font-size: 12px;\n text-align: center;\n color: var(--dsw-alias-state-idle-primary);\n}\n\n/* ---- provider card ---- */\n\n.6d6907d5_card,\n.a287416b_cardActive,\n.aabaa9b3_cardRejected {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.a287416b_cardActive {\n border-color: var(--dsw-alias-state-success-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An enabled route the Host refused: never dress it as active. */\n.aabaa9b3_cardRejected {\n border-color: var(--dsw-alias-state-error-primary, #f85149);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* ---- shared default endpoint ---- */\n\n/* A distinct outer layer, so it reads as "one setting for the page" rather than\n as one more provider card. */\n.ebf26189_defaultCard {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 14px 16px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.5b9e9eb5_defaultLead {\n margin: -2px 0 0;\n font-size: 11px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n}\n\n.a3319282_cardHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.6f64c28d_cardTitles {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.847f7e47_name {\n overflow: hidden;\n font-size: 13px;\n font-weight: 600;\n white-space: nowrap;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.7864474d_id {\n flex: none;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--dsw-alias-bg-layer-2);\n font-family: ui-monospace, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* ---- status pill ---- */\n\n.7ed5399b_pillOn,\n.0ccb6da5_pillLive,\n.3accadc4_pillOff,\n.86897991_pillRejected {\n flex: none;\n padding: 2px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n.7ed5399b_pillOn {\n background: var(--dsw-alias-state-success-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.0ccb6da5_pillLive {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-secondary);\n}\n\n.3accadc4_pillOff {\n color: var(--dsw-alias-state-idle-primary);\n}\n\n.86897991_pillRejected {\n border: 1px solid var(--dsw-alias-state-error-primary, #f85149);\n color: var(--dsw-alias-state-error-primary, #f85149);\n}\n\n/* ---- toggle ---- */\n\n.81025292_toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n padding-top: 2px;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n\n.81025292_toggle input {\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n/* ---- fields ---- */\n\n.5e2577de_field {\n display: grid;\n grid-template-columns: 96px 1fr;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n}\n\n.d9087e9b_label {\n color: var(--dsw-alias-label-secondary);\n}\n\n.aae2a8a2_hint {\n grid-column: 2;\n margin-top: -2px;\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.feb8c902_input {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n padding: 5px 8px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n outline: none;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n/* The base look of every input is INLINE too, so these three state rules only\n ever take effect as author-important declarations \u2014 same reason as the\n self-check button above. `cursor` has no inline base and needs none, and the\n hover border uses `l3` because `l1`\u2192`l2` is a 4%\u219210% black step nobody can\n see. */\n\n.feb8c902_input:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n}\n\n.feb8c902_input:focus {\n border-color: var(--dsw-alias-brand-primary) !important;\n}\n\n.feb8c902_input:disabled {\n color: var(--dsw-alias-state-idle-primary) !important;\n background: var(--dsw-alias-bg-layer-2) !important;\n cursor: not-allowed;\n}\n\n/* The icon sits inside the input, so its hover/focus feedback must be local\n and must outrank the transparent inline base. */\n.5645283d_passwordToggle {\n transition: background 120ms ease, color 120ms ease;\n}\n\n.5645283d_passwordToggle:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n color: var(--dsw-alias-label-primary) !important;\n}\n\n.5645283d_passwordToggle:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* ---- self-check ---- */\n\n/* The hover state is the one thing an inline style cannot express, which is why\n this element carries a class at all. Layout stays inline.\n\n Two traps, both hit before this comment existed:\n - `!important` is load-bearing: the base border and background are INLINE, and\n the cascade sorts a style attribute after every author rule, so a plain\n declaration here would ship, match, and still lose.\n - the TOKENS must be visible ones. `bg-layer-1/2/3` all resolve to\n `neutral-bluish-00` in the light theme, so a layer swap is a no-op there, and\n `border-l1`\u2192`l2` is 4%\u219210% black on a 1px line. The app\'s own hovers use\n `interactive-bg-hover`; using it here is what makes the feedback exist. */\n.de8ca09a_testButton {\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n.de8ca09a_testButton:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\n/* ---- diagnostics ---- */\n\n.4ad17b9d_diag {\n margin-top: 2px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.b2f0759b_diagSummary {\n cursor: pointer;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.f49699bb_diagList {\n margin: 6px 0 0;\n padding-left: 18px;\n font-family: ui-monospace, monospace;\n word-break: break-all;\n}\n';
|
|
48
|
+
__dshStyle.textContent = "/* dsh-provider-proxy \u2014 provider proxy settings page.\r\n Tokens only: no literal colours, matching the shared --dsw-* system. */\r\n\r\n.53783ebc_root {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 14px;\r\n max-width: 760px;\r\n}\r\n\r\n.87ded16f_lead {\r\n margin: 0 0 4px;\r\n font-size: 12px;\r\n line-height: 1.6;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.f139472e_empty {\r\n margin: 0;\r\n padding: 16px;\r\n border: 1px dashed var(--dsw-alias-border-l1);\r\n border-radius: 8px;\r\n font-size: 12px;\r\n text-align: center;\r\n color: var(--dsw-alias-state-idle-primary);\r\n}\r\n\r\n/* ---- provider card ---- */\r\n\r\n.99d44a8a_card,\r\n.6bfd1d28_cardActive,\r\n.ea4afe84_cardRejected {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 12px;\r\n padding: 14px 16px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 10px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n}\r\n\r\n.6bfd1d28_cardActive {\r\n border-color: var(--dsw-alias-state-success-primary);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n/* An enabled route the Host refused: never dress it as active. */\r\n.ea4afe84_cardRejected {\r\n border-color: var(--dsw-alias-state-error-primary, #f85149);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n/* ---- shared default endpoint ---- */\r\n\r\n/* A distinct outer layer, so it reads as \"one setting for the page\" rather than\r\n as one more provider card. */\r\n.6c49773c_defaultCard {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 12px;\r\n padding: 14px 16px;\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n border-radius: 10px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n.8e32da71_defaultLead {\r\n margin: -2px 0 0;\r\n font-size: 11px;\r\n line-height: 1.5;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.d2cf61e5_cardHead {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 12px;\r\n}\r\n\r\n.c6ef43cd_cardTitles {\r\n display: flex;\r\n align-items: baseline;\r\n gap: 8px;\r\n min-width: 0;\r\n}\r\n\r\n.6dad9d13_name {\r\n overflow: hidden;\r\n font-size: 13px;\r\n font-weight: 600;\r\n white-space: nowrap;\r\n text-overflow: ellipsis;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.dd160a22_id {\r\n flex: none;\r\n padding: 1px 6px;\r\n border-radius: 4px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n font-family: ui-monospace, monospace;\r\n font-size: 11px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* ---- status pill ---- */\r\n\r\n.4b27b813_pillOn,\r\n.6e7a5d11_pillLive,\r\n.7951c4c2_pillOff,\r\n.4756f162_pillRejected {\r\n flex: none;\r\n padding: 2px 8px;\r\n border-radius: 999px;\r\n font-size: 11px;\r\n font-weight: 500;\r\n white-space: nowrap;\r\n}\r\n\r\n.4b27b813_pillOn {\r\n background: var(--dsw-alias-state-success-primary);\r\n color: var(--dsw-alias-bg-base);\r\n}\r\n\r\n.6e7a5d11_pillLive {\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.7951c4c2_pillOff {\r\n color: var(--dsw-alias-state-idle-primary);\r\n}\r\n\r\n.4756f162_pillRejected {\r\n border: 1px solid var(--dsw-alias-state-error-primary, #f85149);\r\n color: var(--dsw-alias-state-error-primary, #f85149);\r\n}\r\n\r\n/* ---- toggle ---- */\r\n\r\n.7c4f39f6_toggle {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding-top: 2px;\r\n font-size: 12px;\r\n color: var(--dsw-alias-label-primary);\r\n cursor: pointer;\r\n}\r\n\r\n.7c4f39f6_toggle input {\r\n accent-color: var(--dsw-alias-brand-primary);\r\n cursor: pointer;\r\n}\r\n\r\n/* ---- fields ---- */\r\n\r\n.ebe8e1a3_field {\r\n display: grid;\r\n grid-template-columns: 76px 1fr;\r\n align-items: center;\r\n gap: 8px;\r\n font-size: 12px;\r\n min-width: 0;\r\n}\r\n\r\n.d6a07995_label {\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.5b4fb148_hint {\r\n grid-column: 1 / -1;\r\n margin-top: 2px;\r\n font-size: 11px;\r\n color: var(--dsw-alias-state-warn-primary, #d29922);\r\n}\r\n\r\n.30a51506_input {\r\n box-sizing: border-box;\r\n width: 100%;\r\n min-width: 0;\r\n padding: 5px 8px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 6px;\r\n outline: none;\r\n background: var(--dsw-alias-bg-layer-1);\r\n color: var(--dsw-alias-label-primary);\r\n font: inherit;\r\n font-size: 12px;\r\n transition: border-color 120ms ease, background 120ms ease;\r\n}\r\n\r\n/* The base look of every input is INLINE too, so these three state rules only\r\n ever take effect as author-important declarations \u2014 same reason as the\r\n self-check button above. `cursor` has no inline base and needs none, and the\r\n hover border uses `l3` because `l1`\u2192`l2` is a 4%\u219210% black step nobody can\r\n see. */\r\n\r\n.30a51506_input:hover:not(:disabled) {\r\n border-color: var(--dsw-alias-border-l3) !important;\r\n}\r\n\r\n.30a51506_input:focus {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n}\r\n\r\n.30a51506_input:disabled {\r\n color: var(--dsw-alias-state-idle-primary) !important;\r\n background: var(--dsw-alias-bg-layer-2) !important;\r\n cursor: not-allowed;\r\n}\r\n\r\n/* The icon sits inside the input, so its hover/focus feedback must be local\r\n and must outrank the transparent inline base. */\r\n.fcfbab02_passwordToggle {\r\n transition: background 120ms ease, color 120ms ease;\r\n}\r\n\r\n.fcfbab02_passwordToggle:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-label-primary) !important;\r\n}\r\n\r\n.fcfbab02_passwordToggle:focus-visible {\r\n outline: 2px solid var(--dsw-alias-brand-primary);\r\n outline-offset: 1px;\r\n}\r\n\r\n/* ---- self-check ---- */\r\n\r\n/* The hover state is the one thing an inline style cannot express, which is why\r\n this element carries a class at all. Layout stays inline.\r\n\r\n Two traps, both hit before this comment existed:\r\n - `!important` is load-bearing: the base border and background are INLINE, and\r\n the cascade sorts a style attribute after every author rule, so a plain\r\n declaration here would ship, match, and still lose.\r\n - the TOKENS must be visible ones. `bg-layer-1/2/3` all resolve to\r\n `neutral-bluish-00` in the light theme, so a layer swap is a no-op there, and\r\n `border-l1`\u2192`l2` is 4%\u219210% black on a 1px line. The app's own hovers use\r\n `interactive-bg-hover`; using it here is what makes the feedback exist. */\r\n.b3231562_testButton {\r\n transition: border-color 120ms ease, background 120ms ease;\r\n}\r\n\r\n.b3231562_testButton:hover:not(:disabled) {\r\n border-color: var(--dsw-alias-border-l3) !important;\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n}\r\n\r\n/* ---- diagnostics ---- */\r\n\r\n.184b2d06_diag {\r\n padding: 10px 14px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 10px;\r\n font-size: 11px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.49005df6_diagSummary {\r\n cursor: pointer;\r\n color: var(--dsw-alias-state-warn-primary);\r\n}\r\n\r\n.7f1b6335_diagList {\r\n margin: 6px 0 0;\r\n padding-left: 18px;\r\n font-family: ui-monospace, monospace;\r\n word-break: break-all;\r\n}\r\n\r\n/* ---- v0.8.0 N9 additions: status filter chips ---- */\r\n\r\n.406f7239_chipRow {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.446f9f30_chip {\r\n transition: border-color 120ms ease, background 120ms ease, color 120ms ease;\r\n}\r\n\r\n/* Hover is a HINT, never a selection.\r\n\r\n Two earlier versions of this rule were wrong in the same direction:\r\n it painted the active background (any hovered chip looked selected), and\r\n then it painted an outline (a deselected chip still looked framed). An\r\n unselected chip is bare text; hovering gives it a faint surface, nothing\r\n more. Selection is the only state allowed a border and a solid fill. */\r\n.446f9f30_chip:hover:not(:disabled) {\r\n border-color: transparent !important;\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-label-primary) !important;\r\n}\r\n\r\n/* The selected chip, driven by STATE rather than by class precedence, so it\r\n cannot be outranked by `:hover` on a sibling. */\r\n.152509e7_chipActive,\r\n.446f9f30_chip[aria-pressed='true'] {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n background: var(--dsw-alias-brand-primary) !important;\r\n color: var(--dsw-alias-bg-base) !important;\r\n font-weight: 600;\r\n}\r\n\r\n/* A selected chip keeps its own surface while hovered: the pointer is on it,\r\n which must not read as \"about to become something else\". */\r\n.152509e7_chipActive:hover:not(:disabled),\r\n.446f9f30_chip[aria-pressed='true']:hover:not(:disabled) {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n background: var(--dsw-alias-brand-primary) !important;\r\n color: var(--dsw-alias-bg-base) !important;\r\n}\r\n\r\n/* ---- v0.8.0 N11 additions: form grid ---- */\r\n\r\n.6fb245a3_formGrid {\r\n display: grid;\r\n grid-template-columns: repeat(2, minmax(0, 1fr));\r\n align-items: start;\r\n gap: 10px 14px;\r\n}\r\n\r\n.c89ac7ba_formGridFull {\r\n grid-column: 1 / -1;\r\n}\r\n\r\n@media (max-width: 520px) {\r\n .6fb245a3_formGrid {\r\n grid-template-columns: 1fr !important;\r\n }\r\n .c89ac7ba_formGridFull {\r\n grid-column: auto !important;\r\n }\r\n /* The endpoint-source picker and the enable switch stack too, instead of\r\n shrinking the picker until its option text is unreadable. */\r\n .763cfc36_controlRow {\r\n grid-template-columns: 1fr !important;\r\n }\r\n /* The list toolbar's buttons drop below the search box at this width. */\r\n .97a483ad_toolbarRow > * {\r\n flex: 1 1 100% !important;\r\n }\r\n}\r\n\r\n/* ---- v0.8.0 N10 additions: compact view ---- */\r\n\r\n.342803fe_compactRow,\r\n.b8cadbfb_compactRowActive,\r\n.9ccd93bd_compactRowRejected {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: stretch;\r\n gap: 8px;\r\n padding: 12px 16px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 10px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n}\r\n\r\n.b8cadbfb_compactRowActive {\r\n border-color: var(--dsw-alias-state-success-primary);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n.9ccd93bd_compactRowRejected {\r\n border-color: var(--dsw-alias-state-error-primary, #f85149);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n/* ---- v0.8.0 N13 additions: quick copy ---- */\r\n.dcf70ff1_copyButton {\r\n transition: background 120ms ease, color 120ms ease;\r\n}\r\n\r\n.dcf70ff1_copyButton:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-label-primary) !important;\r\n}\r\n\r\n/* ---- v0.9.0 N14 additions: named proxy presets ----\r\n The manager reuses the diagnostics/details shell, so only the per-preset row\r\n needs its own rules: an inset card whose border lights up while it is being\r\n edited, which is the only way to tell several identical rows apart. */\r\n\r\n.9889cd73_profileCard {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n padding: 12px 14px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 10px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n transition: border-color 120ms ease;\r\n}\r\n\r\n.9889cd73_profileCard:focus-within {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n}\r\n\r\n/* ---- v0.9.0 N19 additions: one layout scale ----\r\n These classes exist only so the narrow-screen rules above can reach the\r\n containers. Their visible properties stay inline, for the same reason every\r\n other box on this page does: the stylesheet has silently failed to bind here\r\n before, and layout must survive that. */\r\n\r\n.763cfc36_controlRow,\r\n.97a483ad_toolbarRow,\r\n.64d062a3_panel {\r\n min-width: 0;\r\n}\r\n";
|
|
49
49
|
document.head.appendChild(__dshStyle);
|
|
50
50
|
}
|
|
51
51
|
var __dshStyle;
|
|
@@ -71,6 +71,9 @@ function splitHostRules(raw) {
|
|
|
71
71
|
function normalizeHostRule(raw) {
|
|
72
72
|
return raw.trim().replace(/^\*?\./, "").toLowerCase();
|
|
73
73
|
}
|
|
74
|
+
function parseHostList(raw) {
|
|
75
|
+
return splitHostRules(raw).map(normalizeHostRule).filter((entry) => entry.length > 0);
|
|
76
|
+
}
|
|
74
77
|
function isBracketedIpv6(token) {
|
|
75
78
|
return /^\[[0-9a-f:.]+\]$/i.test(token);
|
|
76
79
|
}
|
|
@@ -88,6 +91,30 @@ function hostRuleProblem(raw) {
|
|
|
88
91
|
function deadRuleEntries(raw) {
|
|
89
92
|
return splitHostRules(raw).filter((entry) => hostRuleProblem(entry) !== void 0);
|
|
90
93
|
}
|
|
94
|
+
var PROVIDER_HOST_TEMPLATES = {
|
|
95
|
+
openai: { label: "OpenAI", hosts: "api.openai.com, *.openai.com" },
|
|
96
|
+
anthropic: { label: "Anthropic", hosts: "api.anthropic.com" },
|
|
97
|
+
google: { label: "Google", hosts: "generativelanguage.googleapis.com" },
|
|
98
|
+
deepseek: { label: "DeepSeek", hosts: "api.deepseek.com" }
|
|
99
|
+
};
|
|
100
|
+
function providerHostTemplate(providerId) {
|
|
101
|
+
const key = providerId.trim().toLowerCase().split(/[-_/:.]/)[0] ?? "";
|
|
102
|
+
const template = PROVIDER_HOST_TEMPLATES[key];
|
|
103
|
+
return template === void 0 ? void 0 : { id: key, ...template };
|
|
104
|
+
}
|
|
105
|
+
function appendHostRules(raw, hosts) {
|
|
106
|
+
const existing = new Set(parseHostList(raw));
|
|
107
|
+
const additions = [];
|
|
108
|
+
for (const token of splitHostRules(hosts)) {
|
|
109
|
+
const normalized = normalizeHostRule(token);
|
|
110
|
+
if (normalized.length === 0 || existing.has(normalized)) continue;
|
|
111
|
+
existing.add(normalized);
|
|
112
|
+
additions.push(token);
|
|
113
|
+
}
|
|
114
|
+
if (additions.length === 0) return raw.trim();
|
|
115
|
+
const prefix = raw.trim();
|
|
116
|
+
return prefix.length === 0 ? additions.join(", ") : `${prefix}, ${additions.join(", ")}`;
|
|
117
|
+
}
|
|
91
118
|
|
|
92
119
|
// src/proxy-address.ts
|
|
93
120
|
var PROXY_HOST_INVALID = "\u4EE3\u7406\u5730\u5740\u53EA\u80FD\u586B IPv4\u3001IPv6 \u6216\u57DF\u540D";
|
|
@@ -125,6 +152,233 @@ function defaultProxyProblem(endpoint) {
|
|
|
125
152
|
return void 0;
|
|
126
153
|
}
|
|
127
154
|
|
|
155
|
+
// src/presets.ts
|
|
156
|
+
var DEFAULT_PROFILE_LABEL = "\u9ED8\u8BA4\u9884\u8BBE (Default)";
|
|
157
|
+
var DEFAULT_PROFILE_BINDING = "__default__";
|
|
158
|
+
function routeBindingOf(route) {
|
|
159
|
+
const profileId = (route?.profileId ?? "").trim();
|
|
160
|
+
if (profileId.length > 0) return { kind: "profile", profileId };
|
|
161
|
+
if (route?.useDefault === true) return { kind: "default" };
|
|
162
|
+
return { kind: "own" };
|
|
163
|
+
}
|
|
164
|
+
function bindingFields(binding) {
|
|
165
|
+
switch (binding.kind) {
|
|
166
|
+
case "profile":
|
|
167
|
+
return { profileId: binding.profileId, useDefault: false };
|
|
168
|
+
case "default":
|
|
169
|
+
return { profileId: "", useDefault: true };
|
|
170
|
+
case "own":
|
|
171
|
+
return { profileId: "", useDefault: false };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function profileNameProblem(raw) {
|
|
175
|
+
const name = raw.trim();
|
|
176
|
+
if (name.length === 0) return "\u9884\u8BBE\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A";
|
|
177
|
+
if (name.length > 40) return "\u9884\u8BBE\u540D\u79F0\u4E0D\u80FD\u8D85\u8FC7 40 \u4E2A\u5B57\u7B26";
|
|
178
|
+
if (/[\u0000-\u001f\u007f]/.test(name)) return "\u9884\u8BBE\u540D\u79F0\u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26";
|
|
179
|
+
if (name === DEFAULT_PROFILE_LABEL) return "\u8BE5\u540D\u79F0\u7531\u5185\u7F6E\u9ED8\u8BA4\u9884\u8BBE\u4FDD\u7559\uFF0C\u8BF7\u6362\u4E00\u4E2A\u540D\u79F0";
|
|
180
|
+
if (name === DEFAULT_PROFILE_BINDING) return "\u8BE5\u540D\u79F0\u7531\u63D2\u4EF6\u4FDD\u7559\uFF0C\u8BF7\u6362\u4E00\u4E2A\u540D\u79F0";
|
|
181
|
+
return void 0;
|
|
182
|
+
}
|
|
183
|
+
function profileNames(profiles) {
|
|
184
|
+
return Object.keys(profiles ?? {}).sort((a, b) => a.localeCompare(b));
|
|
185
|
+
}
|
|
186
|
+
function routesBoundTo(routes, profileId) {
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const [id, route] of Object.entries(routes ?? {})) {
|
|
189
|
+
if ((route?.profileId ?? "").trim() === profileId) out.push(id);
|
|
190
|
+
}
|
|
191
|
+
return out.sort();
|
|
192
|
+
}
|
|
193
|
+
function endpointSummary(endpoint) {
|
|
194
|
+
const protocol = endpoint?.protocol ?? "http";
|
|
195
|
+
const host = (endpoint?.host ?? "").trim() || "\u2014";
|
|
196
|
+
const port = endpoint?.port ?? 0;
|
|
197
|
+
return `${protocol}://${host}:${port === 0 ? "\u2014" : port}`;
|
|
198
|
+
}
|
|
199
|
+
function endpointAddress(endpoint) {
|
|
200
|
+
const host = (endpoint?.host ?? "").trim();
|
|
201
|
+
const port = endpoint?.port ?? 0;
|
|
202
|
+
if (host.length === 0 || port === 0) return "\u672A\u914D\u7F6E";
|
|
203
|
+
return `${host}:${port}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/proxy-clients.ts
|
|
207
|
+
var PROXY_CLIENT_PRESETS = [
|
|
208
|
+
{ id: "clash", label: "Clash / Mihomo (127.0.0.1:7890)", protocol: "http", host: "127.0.0.1", port: 7890 },
|
|
209
|
+
{ id: "clash-verge", label: "Clash Verge / Meta (127.0.0.1:7897)", protocol: "http", host: "127.0.0.1", port: 7897 },
|
|
210
|
+
{ id: "v2ray-http", label: "V2Ray / Xray HTTP (127.0.0.1:10809)", protocol: "http", host: "127.0.0.1", port: 10809 },
|
|
211
|
+
{ id: "v2ray-socks", label: "V2Ray / Xray SOCKS5 (127.0.0.1:10808)", protocol: "socks5", host: "127.0.0.1", port: 10808 }
|
|
212
|
+
];
|
|
213
|
+
function proxyClientPreset(id) {
|
|
214
|
+
return PROXY_CLIENT_PRESETS.find((preset) => preset.id === id);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/backup.ts
|
|
218
|
+
var PROTOCOLS = ["http", "https", "socks5"];
|
|
219
|
+
function normalizeEndpoint(raw) {
|
|
220
|
+
const value = raw ?? {};
|
|
221
|
+
return {
|
|
222
|
+
protocol: PROTOCOLS.includes(value.protocol) ? value.protocol : "http",
|
|
223
|
+
host: typeof value.host === "string" ? value.host.trim() : "",
|
|
224
|
+
port: typeof value.port === "number" && Number.isFinite(value.port) ? value.port : 0,
|
|
225
|
+
username: typeof value.username === "string" ? value.username.trim() : "",
|
|
226
|
+
password: typeof value.password === "string" ? value.password : ""
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
var FALLBACK_TEST_URL = "https://api.ipify.org?format=json";
|
|
230
|
+
function fillRoute(raw) {
|
|
231
|
+
const value = raw ?? {};
|
|
232
|
+
return {
|
|
233
|
+
enabled: value.enabled === true,
|
|
234
|
+
profileId: typeof value.profileId === "string" ? value.profileId.trim() : "",
|
|
235
|
+
useDefault: value.useDefault === true,
|
|
236
|
+
protocol: PROTOCOLS.includes(value.protocol) ? value.protocol : "http",
|
|
237
|
+
host: typeof value.host === "string" ? value.host.trim() : "",
|
|
238
|
+
port: typeof value.port === "number" && Number.isFinite(value.port) ? value.port : 0,
|
|
239
|
+
username: typeof value.username === "string" ? value.username.trim() : "",
|
|
240
|
+
password: typeof value.password === "string" ? value.password : "",
|
|
241
|
+
onlyHosts: typeof value.onlyHosts === "string" ? value.onlyHosts : "",
|
|
242
|
+
bypassHosts: typeof value.bypassHosts === "string" ? value.bypassHosts : "",
|
|
243
|
+
testTarget: typeof value.testTarget === "string" ? value.testTarget : ""
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function buildBackup(source, options = { includePasswords: false }) {
|
|
247
|
+
const include = options.includePasswords === true;
|
|
248
|
+
const hide = (password) => include ? password : "";
|
|
249
|
+
const providers = {};
|
|
250
|
+
for (const [id, raw] of Object.entries(source.providers ?? {})) {
|
|
251
|
+
const filled = fillRoute(raw);
|
|
252
|
+
providers[id] = { ...filled, password: hide(filled.password) };
|
|
253
|
+
}
|
|
254
|
+
const proxyProfiles = {};
|
|
255
|
+
for (const [name, raw] of Object.entries(source.proxyProfiles ?? {})) {
|
|
256
|
+
const filled = normalizeEndpoint(raw);
|
|
257
|
+
proxyProfiles[name] = { ...filled, password: hide(filled.password) };
|
|
258
|
+
}
|
|
259
|
+
const defaultProxy = normalizeEndpoint(source.defaultProxy);
|
|
260
|
+
return {
|
|
261
|
+
version: 1,
|
|
262
|
+
exportedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
263
|
+
providers,
|
|
264
|
+
proxyProfiles,
|
|
265
|
+
defaultProxy: { ...defaultProxy, password: hide(defaultProxy.password) },
|
|
266
|
+
localDirect: source.localDirect !== false,
|
|
267
|
+
testUrl: typeof source.testUrl === "string" && source.testUrl.trim().length > 0 ? source.testUrl : FALLBACK_TEST_URL
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function backupFileName(now = /* @__PURE__ */ new Date()) {
|
|
271
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
272
|
+
return `dsh-proxy-backup-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}.json`;
|
|
273
|
+
}
|
|
274
|
+
function isPlainObject(value) {
|
|
275
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
276
|
+
}
|
|
277
|
+
function parseBackup(text) {
|
|
278
|
+
let parsed;
|
|
279
|
+
try {
|
|
280
|
+
parsed = JSON.parse(text);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
error: `\u4E0D\u662F\u5408\u6CD5\u7684 JSON\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (!isPlainObject(parsed)) return { ok: false, error: "\u5907\u4EFD\u6587\u4EF6\u7684\u9876\u5C42\u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61" };
|
|
288
|
+
const root = parsed;
|
|
289
|
+
if (root.providers === void 0 && root.defaultProxy === void 0 && root.proxyProfiles === void 0) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
error: "\u5907\u4EFD\u6587\u4EF6\u7F3A\u5C11 providers / proxyProfiles / defaultProxy\uFF0C\u65E0\u6CD5\u8BC6\u522B\u4E3A\u4EE3\u7406\u914D\u7F6E"
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (root.providers !== void 0 && !isPlainObject(root.providers)) {
|
|
296
|
+
return { ok: false, error: "providers \u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61" };
|
|
297
|
+
}
|
|
298
|
+
if (root.proxyProfiles !== void 0 && !isPlainObject(root.proxyProfiles)) {
|
|
299
|
+
return { ok: false, error: "proxyProfiles \u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61" };
|
|
300
|
+
}
|
|
301
|
+
if (root.defaultProxy !== void 0 && !isPlainObject(root.defaultProxy)) {
|
|
302
|
+
return { ok: false, error: "defaultProxy \u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61" };
|
|
303
|
+
}
|
|
304
|
+
if (root.localDirect !== void 0 && typeof root.localDirect !== "boolean") {
|
|
305
|
+
return { ok: false, error: "localDirect \u5FC5\u987B\u662F\u5E03\u5C14\u503C" };
|
|
306
|
+
}
|
|
307
|
+
if (root.testUrl !== void 0 && typeof root.testUrl !== "string") {
|
|
308
|
+
return { ok: false, error: "testUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32" };
|
|
309
|
+
}
|
|
310
|
+
const endpoint = (value, label) => {
|
|
311
|
+
if (value.protocol !== void 0 && !PROTOCOLS.includes(value.protocol)) {
|
|
312
|
+
return `${label}.protocol \u5FC5\u987B\u662F ${PROTOCOLS.join(" / ")}`;
|
|
313
|
+
}
|
|
314
|
+
for (const key of ["host", "username", "password"]) {
|
|
315
|
+
if (value[key] !== void 0 && typeof value[key] !== "string") {
|
|
316
|
+
return `${label}.${key} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (value.port !== void 0 && (typeof value.port !== "number" || !Number.isFinite(value.port))) {
|
|
320
|
+
return `${label}.port \u5FC5\u987B\u662F\u6570\u5B57`;
|
|
321
|
+
}
|
|
322
|
+
return void 0;
|
|
323
|
+
};
|
|
324
|
+
const providers = {};
|
|
325
|
+
for (const [id, value] of Object.entries(root.providers ?? {})) {
|
|
326
|
+
if (!isPlainObject(value)) return { ok: false, error: `providers.${id} \u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61` };
|
|
327
|
+
const why = endpoint(value, `providers.${id}`);
|
|
328
|
+
if (why !== void 0) return { ok: false, error: why };
|
|
329
|
+
for (const key of ["enabled", "useDefault"]) {
|
|
330
|
+
if (value[key] !== void 0 && typeof value[key] !== "boolean") {
|
|
331
|
+
return { ok: false, error: `providers.${id}.${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C` };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const key of ["profileId", "onlyHosts", "bypassHosts", "testTarget"]) {
|
|
335
|
+
if (value[key] !== void 0 && typeof value[key] !== "string") {
|
|
336
|
+
return { ok: false, error: `providers.${id}.${key} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32` };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
providers[id] = fillRoute(value);
|
|
340
|
+
}
|
|
341
|
+
const proxyProfiles = {};
|
|
342
|
+
for (const [name, value] of Object.entries(
|
|
343
|
+
root.proxyProfiles ?? {}
|
|
344
|
+
)) {
|
|
345
|
+
if (!isPlainObject(value)) return { ok: false, error: `proxyProfiles.${name} \u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61` };
|
|
346
|
+
const why = endpoint(value, `proxyProfiles.${name}`);
|
|
347
|
+
if (why !== void 0) return { ok: false, error: why };
|
|
348
|
+
proxyProfiles[name] = normalizeEndpoint(value);
|
|
349
|
+
}
|
|
350
|
+
if (root.defaultProxy !== void 0) {
|
|
351
|
+
const why = endpoint(root.defaultProxy, "defaultProxy");
|
|
352
|
+
if (why !== void 0) return { ok: false, error: why };
|
|
353
|
+
}
|
|
354
|
+
const document2 = {
|
|
355
|
+
version: 1,
|
|
356
|
+
exportedAt: typeof root.exportedAt === "string" ? root.exportedAt : "",
|
|
357
|
+
providers,
|
|
358
|
+
proxyProfiles,
|
|
359
|
+
defaultProxy: normalizeEndpoint(root.defaultProxy ?? {}),
|
|
360
|
+
localDirect: root.localDirect !== false,
|
|
361
|
+
testUrl: typeof root.testUrl === "string" && root.testUrl.trim().length > 0 ? root.testUrl : FALLBACK_TEST_URL
|
|
362
|
+
};
|
|
363
|
+
return { ok: true, document: document2 };
|
|
364
|
+
}
|
|
365
|
+
function backupOps(document2) {
|
|
366
|
+
return [
|
|
367
|
+
{ op: "set", path: ["providers"], value: document2.providers },
|
|
368
|
+
{ op: "set", path: ["proxyProfiles"], value: document2.proxyProfiles },
|
|
369
|
+
{ op: "set", path: ["defaultProxy"], value: document2.defaultProxy },
|
|
370
|
+
{ op: "set", path: ["localDirect"], value: document2.localDirect },
|
|
371
|
+
{ op: "set", path: ["testUrl"], value: document2.testUrl }
|
|
372
|
+
];
|
|
373
|
+
}
|
|
374
|
+
function backupCounts(document2) {
|
|
375
|
+
return {
|
|
376
|
+
providers: Object.keys(document2.providers).length,
|
|
377
|
+
profiles: Object.keys(document2.proxyProfiles).length,
|
|
378
|
+
enabled: Object.values(document2.providers).filter((route) => route.enabled).length
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
128
382
|
// src/client.ts
|
|
129
383
|
var ReactCache;
|
|
130
384
|
function react() {
|
|
@@ -139,7 +393,7 @@ function react() {
|
|
|
139
393
|
}
|
|
140
394
|
return ReactCache;
|
|
141
395
|
}
|
|
142
|
-
var
|
|
396
|
+
var PROTOCOLS2 = ["http", "https", "socks5"];
|
|
143
397
|
var NAMESPACE = "provider-proxy";
|
|
144
398
|
var PLUGINS = ["slots"];
|
|
145
399
|
var inject = PLUGINS;
|
|
@@ -357,7 +611,24 @@ async function fetchCatalog() {
|
|
|
357
611
|
};
|
|
358
612
|
}
|
|
359
613
|
}
|
|
360
|
-
|
|
614
|
+
const environmentProxy = (() => {
|
|
615
|
+
const raw = body?.environment?.proxy;
|
|
616
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
617
|
+
const candidate = raw;
|
|
618
|
+
const protocol = candidate.protocol;
|
|
619
|
+
if (protocol !== "http" && protocol !== "https" && protocol !== "socks5") return null;
|
|
620
|
+
if (typeof candidate.host !== "string" || candidate.host.length === 0) return null;
|
|
621
|
+
const port = candidate.port;
|
|
622
|
+
if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535) return null;
|
|
623
|
+
return {
|
|
624
|
+
source: typeof candidate.source === "string" ? candidate.source : "environment",
|
|
625
|
+
protocol,
|
|
626
|
+
host: candidate.host,
|
|
627
|
+
port,
|
|
628
|
+
hadCredentials: candidate.hadCredentials === true
|
|
629
|
+
};
|
|
630
|
+
})();
|
|
631
|
+
return { rows, publishedRows: rows, hits, problems, lastUse, environmentProxy };
|
|
361
632
|
}
|
|
362
633
|
var CATALOG_REFRESH_MS = 1e4;
|
|
363
634
|
var catalogPoll;
|
|
@@ -378,8 +649,9 @@ var byName = (a, b) => a.name.localeCompare(b.name);
|
|
|
378
649
|
function view(route) {
|
|
379
650
|
return {
|
|
380
651
|
enabled: route?.enabled === true,
|
|
652
|
+
profileId: typeof route?.profileId === "string" ? route.profileId : "",
|
|
381
653
|
useDefault: route?.useDefault === true,
|
|
382
|
-
protocol:
|
|
654
|
+
protocol: PROTOCOLS2.includes(route?.protocol) ? route.protocol : "http",
|
|
383
655
|
host: route?.host ?? "",
|
|
384
656
|
port: route?.port ?? 0,
|
|
385
657
|
username: route?.username ?? "",
|
|
@@ -389,6 +661,15 @@ function view(route) {
|
|
|
389
661
|
testTarget: route?.testTarget ?? ""
|
|
390
662
|
};
|
|
391
663
|
}
|
|
664
|
+
function profileView(profile) {
|
|
665
|
+
return {
|
|
666
|
+
protocol: PROTOCOLS2.includes(profile?.protocol) ? profile.protocol : "http",
|
|
667
|
+
host: typeof profile?.host === "string" ? profile.host : "",
|
|
668
|
+
port: typeof profile?.port === "number" ? profile.port : 0,
|
|
669
|
+
username: typeof profile?.username === "string" ? profile.username : "",
|
|
670
|
+
password: typeof profile?.password === "string" ? profile.password : ""
|
|
671
|
+
};
|
|
672
|
+
}
|
|
392
673
|
function unbracket(hostname) {
|
|
393
674
|
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
394
675
|
}
|
|
@@ -411,7 +692,7 @@ function splitProxyUrl(text) {
|
|
|
411
692
|
}
|
|
412
693
|
const scheme = url.protocol.slice(0, -1).toLowerCase();
|
|
413
694
|
const protocol = scheme === "socks" ? "socks5" : scheme;
|
|
414
|
-
if (!
|
|
695
|
+
if (!PROTOCOLS2.includes(protocol)) return void 0;
|
|
415
696
|
if (url.hostname.length === 0) return void 0;
|
|
416
697
|
const decode = (value) => {
|
|
417
698
|
try {
|
|
@@ -431,6 +712,30 @@ function splitProxyUrl(text) {
|
|
|
431
712
|
}
|
|
432
713
|
var WRITE_DELAY_MS = 300;
|
|
433
714
|
var pendingWrites = /* @__PURE__ */ new Map();
|
|
715
|
+
var VIEW_PREFS_KEY = "dsh-provider-proxy:view";
|
|
716
|
+
function readViewPrefs() {
|
|
717
|
+
try {
|
|
718
|
+
const raw = globalThis.localStorage?.getItem(VIEW_PREFS_KEY);
|
|
719
|
+
if (typeof raw !== "string" || raw.length === 0) return {};
|
|
720
|
+
const parsed = JSON.parse(raw);
|
|
721
|
+
if (parsed === null || typeof parsed !== "object") return {};
|
|
722
|
+
const out = {};
|
|
723
|
+
if (parsed.viewMode === "card" || parsed.viewMode === "compact") out.viewMode = parsed.viewMode;
|
|
724
|
+
if (parsed.statusFilter === "all" || parsed.statusFilter === "proxied" || parsed.statusFilter === "direct" || parsed.statusFilter === "rejected") {
|
|
725
|
+
out.statusFilter = parsed.statusFilter;
|
|
726
|
+
}
|
|
727
|
+
return out;
|
|
728
|
+
} catch {
|
|
729
|
+
return {};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function writeViewPrefs(patch) {
|
|
733
|
+
try {
|
|
734
|
+
const next = { ...readViewPrefs(), ...patch };
|
|
735
|
+
globalThis.localStorage?.setItem(VIEW_PREFS_KEY, JSON.stringify(next));
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
}
|
|
434
739
|
var writeQueue = Promise.resolve();
|
|
435
740
|
function enqueueWrite(run) {
|
|
436
741
|
const next = writeQueue.then(run, run);
|
|
@@ -472,16 +777,43 @@ var S = {
|
|
|
472
777
|
root: {
|
|
473
778
|
display: "flex",
|
|
474
779
|
flexDirection: "column",
|
|
475
|
-
gap: "
|
|
476
|
-
maxWidth: "
|
|
780
|
+
gap: "14px",
|
|
781
|
+
maxWidth: "760px"
|
|
782
|
+
},
|
|
783
|
+
/**
|
|
784
|
+
* One vertical group of page-level blocks.
|
|
785
|
+
*
|
|
786
|
+
* Page settings (default endpoint, presets, backup, advanced) are one visual
|
|
787
|
+
* unit; the provider list is another. Without the grouping every block was a
|
|
788
|
+
* sibling at the same level, so the list controls ended up four blocks below
|
|
789
|
+
* the list they operate on.
|
|
790
|
+
*/
|
|
791
|
+
group: {
|
|
792
|
+
display: "flex",
|
|
793
|
+
flexDirection: "column",
|
|
794
|
+
gap: "10px",
|
|
795
|
+
minWidth: "0"
|
|
796
|
+
},
|
|
797
|
+
/** A small heading that separates the page's groups. */
|
|
798
|
+
groupTitle: {
|
|
799
|
+
margin: "6px 0 -2px",
|
|
800
|
+
fontSize: "11px",
|
|
801
|
+
fontWeight: "600",
|
|
802
|
+
letterSpacing: "0.04em",
|
|
803
|
+
textTransform: "uppercase",
|
|
804
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
477
805
|
},
|
|
478
806
|
card: {
|
|
479
807
|
display: "flex",
|
|
480
808
|
flexDirection: "column",
|
|
481
809
|
gap: "12px",
|
|
482
810
|
padding: "14px 16px",
|
|
483
|
-
|
|
484
|
-
|
|
811
|
+
// Longhands, for the same reason as `chip`: `cardActive`/`cardRejected`
|
|
812
|
+
// override ONLY `borderColor`, and a shorthand here would lose its colour
|
|
813
|
+
// the moment a card went back to the resting state.
|
|
814
|
+
borderWidth: "1px",
|
|
815
|
+
borderStyle: "solid",
|
|
816
|
+
borderColor: "var(--dsw-alias-border-l1)",
|
|
485
817
|
borderRadius: "10px",
|
|
486
818
|
background: "var(--dsw-alias-bg-layer-1)"
|
|
487
819
|
},
|
|
@@ -631,7 +963,7 @@ var S = {
|
|
|
631
963
|
defaultCard: {
|
|
632
964
|
display: "flex",
|
|
633
965
|
flexDirection: "column",
|
|
634
|
-
gap: "
|
|
966
|
+
gap: "12px",
|
|
635
967
|
padding: "14px 16px",
|
|
636
968
|
border: "1px solid var(--dsw-alias-border-l2)",
|
|
637
969
|
borderRadius: "10px",
|
|
@@ -666,10 +998,26 @@ var S = {
|
|
|
666
998
|
},
|
|
667
999
|
field: {
|
|
668
1000
|
display: "grid",
|
|
669
|
-
gridTemplateColumns: "
|
|
1001
|
+
gridTemplateColumns: "76px 1fr",
|
|
670
1002
|
alignItems: "center",
|
|
671
|
-
gap: "
|
|
672
|
-
fontSize: "12px"
|
|
1003
|
+
gap: "8px",
|
|
1004
|
+
fontSize: "12px",
|
|
1005
|
+
minWidth: "0"
|
|
1006
|
+
},
|
|
1007
|
+
/** A row of peer controls, each with its own label above it. */
|
|
1008
|
+
controlRow: {
|
|
1009
|
+
display: "grid",
|
|
1010
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
|
|
1011
|
+
alignItems: "end",
|
|
1012
|
+
gap: "10px 14px"
|
|
1013
|
+
},
|
|
1014
|
+
/** One cell of `controlRow`: a small label over its control. */
|
|
1015
|
+
controlCell: {
|
|
1016
|
+
display: "flex",
|
|
1017
|
+
flexDirection: "column",
|
|
1018
|
+
gap: "4px",
|
|
1019
|
+
fontSize: "12px",
|
|
1020
|
+
minWidth: "0"
|
|
673
1021
|
},
|
|
674
1022
|
label: { color: "var(--dsw-alias-label-secondary)" },
|
|
675
1023
|
input: {
|
|
@@ -686,17 +1034,180 @@ var S = {
|
|
|
686
1034
|
fontSize: "12px"
|
|
687
1035
|
},
|
|
688
1036
|
diag: {
|
|
689
|
-
|
|
690
|
-
padding: "8px 10px",
|
|
1037
|
+
padding: "10px 14px",
|
|
691
1038
|
border: "1px solid var(--dsw-alias-border-l1)",
|
|
692
|
-
borderRadius: "
|
|
1039
|
+
borderRadius: "10px",
|
|
693
1040
|
fontSize: "11px",
|
|
694
1041
|
color: "var(--dsw-alias-label-secondary)"
|
|
695
1042
|
},
|
|
696
1043
|
hint: {
|
|
697
|
-
gridColumn: "
|
|
1044
|
+
gridColumn: "1 / -1",
|
|
698
1045
|
fontSize: "11px",
|
|
699
|
-
color: "var(--dsw-alias-
|
|
1046
|
+
color: "var(--dsw-alias-state-warn-primary, #d29922)"
|
|
1047
|
+
},
|
|
1048
|
+
/** The shared shell of every collapsible panel on the page. */
|
|
1049
|
+
panelBody: {
|
|
1050
|
+
display: "flex",
|
|
1051
|
+
flexDirection: "column",
|
|
1052
|
+
gap: "10px",
|
|
1053
|
+
marginTop: "8px"
|
|
1054
|
+
},
|
|
1055
|
+
/** The bordered block of list controls, directly above the list. */
|
|
1056
|
+
toolbar: {
|
|
1057
|
+
display: "flex",
|
|
1058
|
+
flexDirection: "column",
|
|
1059
|
+
gap: "8px",
|
|
1060
|
+
padding: "10px 12px",
|
|
1061
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
1062
|
+
borderRadius: "10px",
|
|
1063
|
+
background: "var(--dsw-alias-bg-layer-1)"
|
|
1064
|
+
},
|
|
1065
|
+
toolbarRow: {
|
|
1066
|
+
display: "flex",
|
|
1067
|
+
alignItems: "center",
|
|
1068
|
+
flexWrap: "wrap",
|
|
1069
|
+
gap: "8px",
|
|
1070
|
+
minWidth: "0"
|
|
1071
|
+
},
|
|
1072
|
+
/** The backup bar: explanation on the left, its two actions on the right. */
|
|
1073
|
+
backupBar: {
|
|
1074
|
+
display: "flex",
|
|
1075
|
+
alignItems: "center",
|
|
1076
|
+
justifyContent: "space-between",
|
|
1077
|
+
flexWrap: "wrap",
|
|
1078
|
+
gap: "10px",
|
|
1079
|
+
padding: "10px 12px",
|
|
1080
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
1081
|
+
borderRadius: "10px",
|
|
1082
|
+
background: "var(--dsw-alias-bg-layer-1)"
|
|
1083
|
+
},
|
|
1084
|
+
chipRow: {
|
|
1085
|
+
display: "flex",
|
|
1086
|
+
alignItems: "center",
|
|
1087
|
+
gap: "6px",
|
|
1088
|
+
flexWrap: "wrap",
|
|
1089
|
+
marginBottom: "2px"
|
|
1090
|
+
},
|
|
1091
|
+
chip: {
|
|
1092
|
+
display: "inline-flex",
|
|
1093
|
+
alignItems: "center",
|
|
1094
|
+
padding: "3px 10px",
|
|
1095
|
+
borderRadius: "999px",
|
|
1096
|
+
// An UNSELECTED chip is bare text: no outline, no fill.
|
|
1097
|
+
//
|
|
1098
|
+
// It used to carry a faint 1px border, which is the same shape as the
|
|
1099
|
+
// selected chip's outline — so after switching filters the old chip still
|
|
1100
|
+
// read as "selected, just with dimmer text". The border is kept in the
|
|
1101
|
+
// declaration as `transparent` rather than removed, so selecting a chip
|
|
1102
|
+
// cannot shift the row by a pixel.
|
|
1103
|
+
//
|
|
1104
|
+
// WRITTEN AS THREE LONGHANDS, NOT AS THE `border` SHORTHAND. This is not
|
|
1105
|
+
// style: React diffs inline styles key by key, and `chipActive` only
|
|
1106
|
+
// overrides `borderColor`. Deselecting a chip therefore DELETES the
|
|
1107
|
+
// `borderColor` key, React writes `style.borderColor = ''`, and the
|
|
1108
|
+
// shorthand's transparent value is NOT restored — the border falls back to
|
|
1109
|
+
// the CSS initial value `currentColor`, leaving a text-coloured outline on
|
|
1110
|
+
// a chip that is supposed to have none. Keeping all three longhands present
|
|
1111
|
+
// in every state means only the VALUE changes, never the key.
|
|
1112
|
+
borderWidth: "1px",
|
|
1113
|
+
borderStyle: "solid",
|
|
1114
|
+
borderColor: "transparent",
|
|
1115
|
+
background: "transparent",
|
|
1116
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1117
|
+
fontSize: "11px",
|
|
1118
|
+
fontWeight: "500",
|
|
1119
|
+
cursor: "pointer",
|
|
1120
|
+
userSelect: "none"
|
|
1121
|
+
},
|
|
1122
|
+
/** The one selected chip: a solid fill, unmistakable at a glance. */
|
|
1123
|
+
chipActive: {
|
|
1124
|
+
borderColor: "var(--dsw-alias-brand-primary)",
|
|
1125
|
+
background: "var(--dsw-alias-brand-primary)",
|
|
1126
|
+
color: "var(--dsw-alias-bg-base)",
|
|
1127
|
+
fontWeight: "600"
|
|
1128
|
+
},
|
|
1129
|
+
compactRow: {
|
|
1130
|
+
display: "flex",
|
|
1131
|
+
flexDirection: "column",
|
|
1132
|
+
gap: "8px",
|
|
1133
|
+
padding: "12px 16px",
|
|
1134
|
+
borderWidth: "1px",
|
|
1135
|
+
borderStyle: "solid",
|
|
1136
|
+
borderColor: "var(--dsw-alias-border-l1)",
|
|
1137
|
+
borderRadius: "10px",
|
|
1138
|
+
background: "var(--dsw-alias-bg-layer-1)"
|
|
1139
|
+
},
|
|
1140
|
+
/** Row 1 of a compact entry: identity and the proxy it uses. */
|
|
1141
|
+
compactLine: {
|
|
1142
|
+
display: "flex",
|
|
1143
|
+
alignItems: "center",
|
|
1144
|
+
flexWrap: "wrap",
|
|
1145
|
+
gap: "8px",
|
|
1146
|
+
minWidth: "0"
|
|
1147
|
+
},
|
|
1148
|
+
/** Row 2 of a compact entry: every control, right-aligned. */
|
|
1149
|
+
compactActions: {
|
|
1150
|
+
display: "flex",
|
|
1151
|
+
alignItems: "center",
|
|
1152
|
+
flexWrap: "wrap",
|
|
1153
|
+
gap: "8px",
|
|
1154
|
+
minWidth: "0"
|
|
1155
|
+
},
|
|
1156
|
+
/**
|
|
1157
|
+
* Row 3 of a compact entry: the self-check outcome.
|
|
1158
|
+
*
|
|
1159
|
+
* Its own line, so a running or failing check never shifts the picker or the
|
|
1160
|
+
* buttons, and a long failure message gets the full width instead of being
|
|
1161
|
+
* squeezed between two controls.
|
|
1162
|
+
*/
|
|
1163
|
+
compactResultLine: {
|
|
1164
|
+
display: "flex",
|
|
1165
|
+
alignItems: "baseline",
|
|
1166
|
+
flexWrap: "wrap",
|
|
1167
|
+
gap: "6px",
|
|
1168
|
+
paddingTop: "6px",
|
|
1169
|
+
borderTop: "1px solid var(--dsw-alias-border-l1)",
|
|
1170
|
+
fontSize: "11px",
|
|
1171
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1172
|
+
minWidth: "0",
|
|
1173
|
+
wordBreak: "break-all"
|
|
1174
|
+
},
|
|
1175
|
+
compactRowActive: {
|
|
1176
|
+
borderColor: "var(--dsw-alias-state-success-primary)",
|
|
1177
|
+
background: "var(--dsw-alias-bg-layer-2)"
|
|
1178
|
+
},
|
|
1179
|
+
compactRowRejected: {
|
|
1180
|
+
borderColor: "var(--dsw-alias-state-error-primary, #f85149)",
|
|
1181
|
+
background: "var(--dsw-alias-bg-layer-2)"
|
|
1182
|
+
},
|
|
1183
|
+
compactEndpoint: {
|
|
1184
|
+
fontSize: "11px",
|
|
1185
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1186
|
+
overflow: "hidden",
|
|
1187
|
+
textOverflow: "ellipsis",
|
|
1188
|
+
whiteSpace: "nowrap",
|
|
1189
|
+
maxWidth: "180px"
|
|
1190
|
+
},
|
|
1191
|
+
formGrid: {
|
|
1192
|
+
display: "grid",
|
|
1193
|
+
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
1194
|
+
alignItems: "start",
|
|
1195
|
+
gap: "10px 14px"
|
|
1196
|
+
},
|
|
1197
|
+
formGridFull: {
|
|
1198
|
+
gridColumn: "1 / -1"
|
|
1199
|
+
},
|
|
1200
|
+
copyButton: {
|
|
1201
|
+
display: "inline-flex",
|
|
1202
|
+
alignItems: "center",
|
|
1203
|
+
padding: "2px 4px",
|
|
1204
|
+
border: "none",
|
|
1205
|
+
borderRadius: "4px",
|
|
1206
|
+
background: "transparent",
|
|
1207
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1208
|
+
cursor: "pointer",
|
|
1209
|
+
fontSize: "11px",
|
|
1210
|
+
lineHeight: "1"
|
|
700
1211
|
}
|
|
701
1212
|
};
|
|
702
1213
|
function renderPage(ctx) {
|
|
@@ -733,6 +1244,7 @@ function renderPage(ctx) {
|
|
|
733
1244
|
hits: {},
|
|
734
1245
|
problems: {},
|
|
735
1246
|
lastUse: {},
|
|
1247
|
+
environmentProxy: null,
|
|
736
1248
|
error: void 0,
|
|
737
1249
|
pending: true
|
|
738
1250
|
};
|
|
@@ -743,6 +1255,7 @@ function renderPage(ctx) {
|
|
|
743
1255
|
hits: catalog.hits,
|
|
744
1256
|
problems: catalog.problems,
|
|
745
1257
|
lastUse: catalog.lastUse,
|
|
1258
|
+
environmentProxy: catalog.environmentProxy ?? null,
|
|
746
1259
|
error: catalog.error,
|
|
747
1260
|
pending: false
|
|
748
1261
|
});
|
|
@@ -779,6 +1292,24 @@ function renderPage(ctx) {
|
|
|
779
1292
|
const [defaultDraft, setDefaultDraft] = (React.useState ?? ((v) => [v, () => {
|
|
780
1293
|
}]))({});
|
|
781
1294
|
const editDefault = (field2, value) => setDefaultDraft((current) => ({ ...current, [field2]: value }));
|
|
1295
|
+
const [profileDraft, setProfileDraft] = (React.useState ?? ((v) => [v, () => {
|
|
1296
|
+
}]))({});
|
|
1297
|
+
const editProfile = (name, field2, value) => setProfileDraft((current) => ({
|
|
1298
|
+
...current,
|
|
1299
|
+
[name]: { ...current[name], [field2]: value }
|
|
1300
|
+
}));
|
|
1301
|
+
const [newProfileName, setNewProfileName] = (React.useState ?? ((v) => [v, () => {
|
|
1302
|
+
}]))("");
|
|
1303
|
+
const [renamingProfile, setRenamingProfile] = (React.useState ?? ((v) => [v, () => {
|
|
1304
|
+
}]))(
|
|
1305
|
+
void 0
|
|
1306
|
+
);
|
|
1307
|
+
const [renameDraft, setRenameDraft] = (React.useState ?? ((v) => [v, () => {
|
|
1308
|
+
}]))("");
|
|
1309
|
+
const [profileManagerOpen, setProfileManagerOpen] = (React.useState ?? ((v) => [v, () => {
|
|
1310
|
+
}]))(
|
|
1311
|
+
false
|
|
1312
|
+
);
|
|
782
1313
|
const [passwordVisible, setPasswordVisible] = (React.useState ?? ((v) => [v, () => {
|
|
783
1314
|
}]))({});
|
|
784
1315
|
const [defaultPasswordVisible, setDefaultPasswordVisible] = (React.useState ?? ((v) => [v, () => {
|
|
@@ -845,11 +1376,81 @@ function renderPage(ctx) {
|
|
|
845
1376
|
};
|
|
846
1377
|
const [filter, setFilter] = (React.useState ?? ((v) => [v, () => {
|
|
847
1378
|
}]))("");
|
|
1379
|
+
const [statusFilter, setStatusFilter] = (React.useState ?? ((v) => [v, () => {
|
|
1380
|
+
}]))(
|
|
1381
|
+
() => readViewPrefs().statusFilter ?? "all"
|
|
1382
|
+
);
|
|
1383
|
+
const [viewMode, setViewMode] = (React.useState ?? ((v) => [v, () => {
|
|
1384
|
+
}]))(
|
|
1385
|
+
() => readViewPrefs().viewMode ?? "card"
|
|
1386
|
+
);
|
|
1387
|
+
const [expandedCards, setExpandedCards] = (React.useState ?? ((v) => [v, () => {
|
|
1388
|
+
}]))({});
|
|
1389
|
+
const [batchProgress, setBatchProgress] = (React.useState ?? ((v) => [v, () => {
|
|
1390
|
+
}]))(null);
|
|
1391
|
+
const [copiedKey, setCopiedKey] = (React.useState ?? ((v) => [v, () => {
|
|
1392
|
+
}]))(void 0);
|
|
1393
|
+
const copyToClipboard = (key, text) => {
|
|
1394
|
+
try {
|
|
1395
|
+
if (typeof globalThis.navigator?.clipboard?.writeText === "function") {
|
|
1396
|
+
void globalThis.navigator.clipboard.writeText(text).then(() => {
|
|
1397
|
+
setCopiedKey(key);
|
|
1398
|
+
setTimeout(() => setCopiedKey((prev) => prev === key ? void 0 : prev), 1500);
|
|
1399
|
+
}).catch(() => void 0);
|
|
1400
|
+
}
|
|
1401
|
+
} catch {
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
const copyButton = (key, text, ariaLabel) => h(
|
|
1405
|
+
"button",
|
|
1406
|
+
{
|
|
1407
|
+
className: import_client.default.copyButton,
|
|
1408
|
+
style: S.copyButton,
|
|
1409
|
+
type: "button",
|
|
1410
|
+
"aria-label": ariaLabel,
|
|
1411
|
+
title: copiedKey === key ? t.copied : ariaLabel,
|
|
1412
|
+
onClick: (e) => {
|
|
1413
|
+
e.stopPropagation?.();
|
|
1414
|
+
copyToClipboard(key, text);
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
copiedKey === key ? h(
|
|
1418
|
+
"svg",
|
|
1419
|
+
{
|
|
1420
|
+
width: 12,
|
|
1421
|
+
height: 12,
|
|
1422
|
+
viewBox: "0 0 24 24",
|
|
1423
|
+
fill: "none",
|
|
1424
|
+
stroke: "var(--dsw-alias-state-success-primary, #2ea043)",
|
|
1425
|
+
strokeWidth: 2,
|
|
1426
|
+
strokeLinecap: "round",
|
|
1427
|
+
strokeLinejoin: "round",
|
|
1428
|
+
"aria-hidden": "true"
|
|
1429
|
+
},
|
|
1430
|
+
h("path", { d: "M20 6 9 17l-5-5" })
|
|
1431
|
+
) : h(
|
|
1432
|
+
"svg",
|
|
1433
|
+
{
|
|
1434
|
+
width: 12,
|
|
1435
|
+
height: 12,
|
|
1436
|
+
viewBox: "0 0 24 24",
|
|
1437
|
+
fill: "none",
|
|
1438
|
+
stroke: "currentColor",
|
|
1439
|
+
strokeWidth: 2,
|
|
1440
|
+
strokeLinecap: "round",
|
|
1441
|
+
strokeLinejoin: "round",
|
|
1442
|
+
"aria-hidden": "true"
|
|
1443
|
+
},
|
|
1444
|
+
h("rect", { width: 14, height: 14, x: 8, y: 8, rx: 2, ry: 2 }),
|
|
1445
|
+
h("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })
|
|
1446
|
+
),
|
|
1447
|
+
copiedKey === key ? h("span", { style: { marginLeft: "3px", fontSize: "10px", color: "var(--dsw-alias-state-success-primary, #2ea043)" } }, t.copied) : null
|
|
1448
|
+
);
|
|
848
1449
|
const runTest = (id) => {
|
|
849
1450
|
const generation = (testGeneration.get(id) ?? 0) + 1;
|
|
850
1451
|
testGeneration.set(id, generation);
|
|
851
1452
|
setTests((current) => ({ ...current, [id]: { pending: true } }));
|
|
852
|
-
|
|
1453
|
+
return (async () => {
|
|
853
1454
|
try {
|
|
854
1455
|
for (const [key, entry] of [...pendingWrites]) {
|
|
855
1456
|
if (key.startsWith(`${id}\0`) || key.startsWith("\0default\0") || key.startsWith("\0global\0")) {
|
|
@@ -875,6 +1476,28 @@ function renderPage(ctx) {
|
|
|
875
1476
|
}
|
|
876
1477
|
})();
|
|
877
1478
|
};
|
|
1479
|
+
const runBatchTest = () => {
|
|
1480
|
+
const activeIds = rows.filter((row) => view({ ...configured[row.id], ...draft[row.id] }).enabled).map((row) => row.id);
|
|
1481
|
+
if (activeIds.length === 0 || batchProgress !== null) return;
|
|
1482
|
+
setBatchProgress({ done: 0, total: activeIds.length });
|
|
1483
|
+
void (async () => {
|
|
1484
|
+
let index = 0;
|
|
1485
|
+
let completed = 0;
|
|
1486
|
+
const concurrency = Math.min(3, activeIds.length);
|
|
1487
|
+
const worker = async () => {
|
|
1488
|
+
while (index < activeIds.length) {
|
|
1489
|
+
const id = activeIds[index++];
|
|
1490
|
+
await runTest(id);
|
|
1491
|
+
completed++;
|
|
1492
|
+
setBatchProgress({ done: completed, total: activeIds.length });
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
1496
|
+
setTimeout(() => {
|
|
1497
|
+
setBatchProgress(null);
|
|
1498
|
+
}, 2e3);
|
|
1499
|
+
})();
|
|
1500
|
+
};
|
|
878
1501
|
const rows = knownProviders({ ...configured, ...draft }, published);
|
|
879
1502
|
const inheritingIds = () => rows.filter((row) => view({ ...configured[row.id], ...draft[row.id] }).useDefault === true).map((row) => row.id);
|
|
880
1503
|
const save = (key, path, label, value, onRefused) => {
|
|
@@ -1048,32 +1671,277 @@ function renderPage(ctx) {
|
|
|
1048
1671
|
event.preventDefault?.();
|
|
1049
1672
|
apply2(parts);
|
|
1050
1673
|
};
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
control,
|
|
1056
|
-
hint === void 0 ? null : h(
|
|
1057
|
-
"span",
|
|
1674
|
+
const quickImportSelect = (key, apply2) => {
|
|
1675
|
+
const detected = state.environmentProxy ?? null;
|
|
1676
|
+
const select = h(
|
|
1677
|
+
"select",
|
|
1058
1678
|
{
|
|
1059
|
-
className: import_client.default.
|
|
1060
|
-
style: {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1679
|
+
className: import_client.default.input,
|
|
1680
|
+
style: { ...S.input, flex: "none", width: "auto" },
|
|
1681
|
+
key: `${key}:select`,
|
|
1682
|
+
value: "",
|
|
1683
|
+
"aria-label": t.quickImport,
|
|
1684
|
+
onChange: (event) => {
|
|
1685
|
+
const value = String(event.target.value ?? "");
|
|
1686
|
+
if (value.length === 0) return;
|
|
1687
|
+
if (value === "environment") {
|
|
1688
|
+
if (detected === null) return;
|
|
1689
|
+
apply2({
|
|
1690
|
+
protocol: detected.protocol,
|
|
1691
|
+
host: detected.host,
|
|
1692
|
+
port: detected.port
|
|
1693
|
+
});
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
const preset = proxyClientPreset(value);
|
|
1697
|
+
if (preset === void 0) return;
|
|
1698
|
+
apply2({ protocol: preset.protocol, host: preset.host, port: preset.port });
|
|
1699
|
+
}
|
|
1700
|
+
},
|
|
1701
|
+
h("option", { key: "none", value: "" }, t.quickImport),
|
|
1702
|
+
...PROXY_CLIENT_PRESETS.map(
|
|
1703
|
+
(preset) => h("option", { key: preset.id, value: preset.id }, preset.label)
|
|
1704
|
+
),
|
|
1705
|
+
h(
|
|
1706
|
+
"option",
|
|
1707
|
+
{ key: "environment", value: "environment", disabled: detected === null },
|
|
1708
|
+
detected === null ? t.quickImportEnvNone : `${t.quickImportEnv}${detected.source}`
|
|
1709
|
+
)
|
|
1710
|
+
);
|
|
1711
|
+
return h(
|
|
1712
|
+
"div",
|
|
1713
|
+
{ key, style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: "0" } },
|
|
1714
|
+
select,
|
|
1715
|
+
detected !== null && detected.hadCredentials ? h("span", { style: { ...S.hint, gridColumn: "auto" } }, t.quickImportEnvCredentials) : null
|
|
1716
|
+
);
|
|
1717
|
+
};
|
|
1718
|
+
const field = (label, control, hint, hintError, fullWidth) => h(
|
|
1719
|
+
"label",
|
|
1720
|
+
{
|
|
1721
|
+
className: `${import_client.default.field}${fullWidth ? ` ${import_client.default.formGridFull}` : ""}`,
|
|
1722
|
+
style: {
|
|
1723
|
+
...S.field,
|
|
1724
|
+
...fullWidth ? S.formGridFull : {}
|
|
1725
|
+
},
|
|
1726
|
+
key: label
|
|
1727
|
+
},
|
|
1728
|
+
h("span", { className: import_client.default.label, style: S.label }, label),
|
|
1729
|
+
control,
|
|
1730
|
+
hint === void 0 ? null : h(
|
|
1731
|
+
"span",
|
|
1732
|
+
{
|
|
1733
|
+
className: import_client.default.hint,
|
|
1734
|
+
style: {
|
|
1735
|
+
...S.hint,
|
|
1736
|
+
marginTop: "2px",
|
|
1737
|
+
// A warning that a value can never work is an answer, not
|
|
1738
|
+
// decoration — same rule as the failed self-check text.
|
|
1739
|
+
...hintError === true ? { color: "var(--dsw-alias-state-error-primary, #f85149)" } : {}
|
|
1740
|
+
}
|
|
1741
|
+
},
|
|
1068
1742
|
hint
|
|
1069
1743
|
)
|
|
1070
1744
|
);
|
|
1745
|
+
const panel = (options) => h(
|
|
1746
|
+
"details",
|
|
1747
|
+
{
|
|
1748
|
+
className: `${import_client.default.diag} ${import_client.default.panel}`,
|
|
1749
|
+
style: S.diag,
|
|
1750
|
+
key: options.key,
|
|
1751
|
+
...options.open === void 0 ? {} : { open: options.open },
|
|
1752
|
+
...options.onToggle === void 0 ? {} : {
|
|
1753
|
+
onToggle: (event) => {
|
|
1754
|
+
const open = event?.currentTarget?.open;
|
|
1755
|
+
if (typeof open === "boolean") options.onToggle?.(open);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
},
|
|
1759
|
+
h("summary", { className: import_client.default.diagSummary }, options.summary),
|
|
1760
|
+
h(
|
|
1761
|
+
"div",
|
|
1762
|
+
{ style: S.panelBody },
|
|
1763
|
+
options.lead === void 0 ? null : h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, options.lead),
|
|
1764
|
+
...options.children
|
|
1765
|
+
)
|
|
1766
|
+
);
|
|
1071
1767
|
const storedDefault = snapshot.value?.defaultProxy ?? {};
|
|
1072
1768
|
const defaultView = view({ ...storedDefault, ...defaultDraft });
|
|
1073
1769
|
const defaultWhy = defaultProxyProblem(defaultView);
|
|
1074
1770
|
const defaultHostWhy = defaultView.host.trim().length === 0 ? void 0 : proxyHostProblem(defaultView.host);
|
|
1075
1771
|
const defaultComplete = defaultWhy === void 0;
|
|
1076
1772
|
const defaultOrigin = `${defaultView.protocol}://${defaultView.host}:${defaultView.port}`;
|
|
1773
|
+
const cardEndpoint = (route) => {
|
|
1774
|
+
const binding = routeBindingOf(route);
|
|
1775
|
+
if (binding.kind === "profile") {
|
|
1776
|
+
const endpoint = profileValueOf(binding.profileId);
|
|
1777
|
+
if (endpoint === void 0) return { binding, missingProfile: binding.profileId, complete: false };
|
|
1778
|
+
const why = defaultProxyProblem(endpoint);
|
|
1779
|
+
return { binding, endpoint, why, complete: why === void 0 };
|
|
1780
|
+
}
|
|
1781
|
+
if (binding.kind === "default") {
|
|
1782
|
+
return { binding, endpoint: defaultView, why: defaultWhy, complete: defaultComplete };
|
|
1783
|
+
}
|
|
1784
|
+
const complete = route.host.trim().length > 0 && Number.isInteger(route.port) && route.port >= 1 && route.port <= 65535;
|
|
1785
|
+
return { binding, why: defaultProxyProblem(route), complete };
|
|
1786
|
+
};
|
|
1787
|
+
const cardProblemText = (state2) => {
|
|
1788
|
+
if (state2.missingProfile !== void 0) {
|
|
1789
|
+
return `${t.profileMissing}${state2.missingProfile}${t.profileMissingSuffix}`;
|
|
1790
|
+
}
|
|
1791
|
+
if (state2.why === void 0) return void 0;
|
|
1792
|
+
if (state2.binding.kind === "profile") {
|
|
1793
|
+
return `${t.profileUnusable}${state2.binding.profileId}${t.profileUnusableSuffix}${state2.why}`;
|
|
1794
|
+
}
|
|
1795
|
+
if (state2.binding.kind === "default") return `${t.defaultMissing}${state2.why}`;
|
|
1796
|
+
return state2.why;
|
|
1797
|
+
};
|
|
1798
|
+
const storedProfiles = snapshot.value?.proxyProfiles ?? {};
|
|
1799
|
+
const profileEntries = profileNames({ ...storedProfiles, ...profileDraft }).map((name) => ({
|
|
1800
|
+
name,
|
|
1801
|
+
value: profileView({ ...storedProfiles[name], ...profileDraft[name] })
|
|
1802
|
+
}));
|
|
1803
|
+
const profileValueOf = (name) => {
|
|
1804
|
+
if (storedProfiles[name] === void 0 && profileDraft[name] === void 0) return void 0;
|
|
1805
|
+
return profileView({ ...storedProfiles[name], ...profileDraft[name] });
|
|
1806
|
+
};
|
|
1807
|
+
const writeProfiles = (key, next, label, onRefused) => {
|
|
1808
|
+
const full = {};
|
|
1809
|
+
for (const [name, value] of Object.entries(next)) full[name] = profileView(value);
|
|
1810
|
+
saveMany(key, [{ op: "set", path: ["proxyProfiles"], value: full }], label, onRefused);
|
|
1811
|
+
};
|
|
1812
|
+
const addProfile = () => {
|
|
1813
|
+
const name = String(newProfileName ?? "").trim();
|
|
1814
|
+
const problem = profileNameProblem(name);
|
|
1815
|
+
if (problem !== void 0) {
|
|
1816
|
+
setSavedNote(void 0);
|
|
1817
|
+
setSaveError({ label: t.profileNameLabel, message: problem });
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (storedProfiles[name] !== void 0 || profileDraft[name] !== void 0) {
|
|
1821
|
+
setSavedNote(void 0);
|
|
1822
|
+
setSaveError({ label: t.profileNameLabel, message: t.profileNameTaken });
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const next = { ...storedProfiles, ...profileDraft };
|
|
1826
|
+
next[name] = profileView({});
|
|
1827
|
+
setNewProfileName("");
|
|
1828
|
+
setProfileDraft((current) => ({ ...current, [name]: profileView({}) }));
|
|
1829
|
+
setProfileManagerOpen(true);
|
|
1830
|
+
writeProfiles(`\0profile\0add`, next, `${t.profileNameLabel} ${name}`, () => {
|
|
1831
|
+
setProfileDraft((current) => {
|
|
1832
|
+
if (current[name] === void 0) return current;
|
|
1833
|
+
const nextDraft = { ...current };
|
|
1834
|
+
delete nextDraft[name];
|
|
1835
|
+
return nextDraft;
|
|
1836
|
+
});
|
|
1837
|
+
});
|
|
1838
|
+
};
|
|
1839
|
+
const renameProfile = (from) => {
|
|
1840
|
+
const to = String(renameDraft ?? "").trim();
|
|
1841
|
+
setRenamingProfile(void 0);
|
|
1842
|
+
setRenameDraft("");
|
|
1843
|
+
if (to === from) return;
|
|
1844
|
+
const problem = profileNameProblem(to);
|
|
1845
|
+
if (problem !== void 0) {
|
|
1846
|
+
setSavedNote(void 0);
|
|
1847
|
+
setSaveError({ label: t.profileNameLabel, message: problem });
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (storedProfiles[to] !== void 0 || profileDraft[to] !== void 0) {
|
|
1851
|
+
setSavedNote(void 0);
|
|
1852
|
+
setSaveError({ label: t.profileNameLabel, message: t.profileNameTaken });
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
const source = profileValueOf(from) ?? profileView({});
|
|
1856
|
+
const next = { ...storedProfiles, ...profileDraft };
|
|
1857
|
+
delete next[from];
|
|
1858
|
+
next[to] = source;
|
|
1859
|
+
const bound = routesBoundTo({ ...configured, ...draft }, from);
|
|
1860
|
+
setProfileDraft((current) => {
|
|
1861
|
+
const nextDraft = { ...current };
|
|
1862
|
+
delete nextDraft[from];
|
|
1863
|
+
nextDraft[to] = source;
|
|
1864
|
+
return nextDraft;
|
|
1865
|
+
});
|
|
1866
|
+
if (bound.length > 0) {
|
|
1867
|
+
setDraft((current) => {
|
|
1868
|
+
const nextDraft = { ...current };
|
|
1869
|
+
for (const id of bound) nextDraft[id] = { ...nextDraft[id], profileId: to };
|
|
1870
|
+
return nextDraft;
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
invalidateTests(bound);
|
|
1874
|
+
saveMany(
|
|
1875
|
+
"\0profile\0rename",
|
|
1876
|
+
[
|
|
1877
|
+
{ op: "set", path: ["proxyProfiles"], value: next },
|
|
1878
|
+
...bound.map((id) => ({ op: "set", path: ["providers", id, "profileId"], value: to }))
|
|
1879
|
+
],
|
|
1880
|
+
`${t.profileNameLabel} ${from} \u2192 ${to}`,
|
|
1881
|
+
() => void 0
|
|
1882
|
+
);
|
|
1883
|
+
};
|
|
1884
|
+
const deleteProfile = (name) => {
|
|
1885
|
+
const bound = routesBoundTo({ ...configured, ...draft }, name);
|
|
1886
|
+
if (globalThis.window?.confirm?.(
|
|
1887
|
+
bound.length === 0 ? t.profileDeleteConfirm : `${t.profileDeleteInUse}${bound.join("\u3001")}
|
|
1888
|
+
|
|
1889
|
+
${t.profileDeleteConfirm}`
|
|
1890
|
+
) !== true) {
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
const next = { ...storedProfiles, ...profileDraft };
|
|
1894
|
+
delete next[name];
|
|
1895
|
+
setProfileDraft((current) => {
|
|
1896
|
+
const nextDraft = { ...current };
|
|
1897
|
+
delete nextDraft[name];
|
|
1898
|
+
return nextDraft;
|
|
1899
|
+
});
|
|
1900
|
+
invalidateTests(bound);
|
|
1901
|
+
writeProfiles(`\0profile\0delete`, next, `${t.profileNameLabel} ${name}`, () => void 0);
|
|
1902
|
+
};
|
|
1903
|
+
const bindProfile = (name, field2) => (value) => {
|
|
1904
|
+
editProfile(name, field2, value);
|
|
1905
|
+
invalidateTests(routesBoundTo({ ...configured, ...draft }, name));
|
|
1906
|
+
save(
|
|
1907
|
+
`\0profile\0${name}\0${field2}`,
|
|
1908
|
+
["proxyProfiles", name, field2],
|
|
1909
|
+
`${name}.${field2}`,
|
|
1910
|
+
value,
|
|
1911
|
+
() => {
|
|
1912
|
+
setProfileDraft((current) => {
|
|
1913
|
+
const entry = current[name];
|
|
1914
|
+
if (entry === void 0 || entry[field2] === void 0) return current;
|
|
1915
|
+
const nextEntry = { ...entry };
|
|
1916
|
+
delete nextEntry[field2];
|
|
1917
|
+
const nextDraft = { ...current };
|
|
1918
|
+
if (Object.keys(nextEntry).length === 0) delete nextDraft[name];
|
|
1919
|
+
else nextDraft[name] = nextEntry;
|
|
1920
|
+
return nextDraft;
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
);
|
|
1924
|
+
};
|
|
1925
|
+
const bindRouteProfile = (id) => (value) => {
|
|
1926
|
+
const fields = bindingFields(
|
|
1927
|
+
value === DEFAULT_PROFILE_BINDING ? { kind: "default" } : value.length === 0 ? { kind: "own" } : { kind: "profile", profileId: value }
|
|
1928
|
+
);
|
|
1929
|
+
edit(id, "profileId", fields.profileId);
|
|
1930
|
+
edit(id, "useDefault", fields.useDefault);
|
|
1931
|
+
invalidateTests([id]);
|
|
1932
|
+
saveMany(
|
|
1933
|
+
`${id}\0*profile`,
|
|
1934
|
+
[
|
|
1935
|
+
{ op: "set", path: ["providers", id, "profileId"], value: fields.profileId },
|
|
1936
|
+
{ op: "set", path: ["providers", id, "useDefault"], value: fields.useDefault }
|
|
1937
|
+
],
|
|
1938
|
+
`${id}.profileId`,
|
|
1939
|
+
() => {
|
|
1940
|
+
rollbackRouteField(id, "profileId");
|
|
1941
|
+
rollbackRouteField(id, "useDefault");
|
|
1942
|
+
}
|
|
1943
|
+
);
|
|
1944
|
+
};
|
|
1077
1945
|
const defaultBlock = h(
|
|
1078
1946
|
"div",
|
|
1079
1947
|
{ className: import_client.default.defaultCard, style: S.defaultCard, key: "default-proxy" },
|
|
@@ -1095,77 +1963,88 @@ function renderPage(ctx) {
|
|
|
1095
1963
|
)
|
|
1096
1964
|
),
|
|
1097
1965
|
h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, t.defaultLead),
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1966
|
+
h(
|
|
1967
|
+
"div",
|
|
1968
|
+
{ style: S.testRow, key: "default:quick-import" },
|
|
1969
|
+
quickImportSelect("default:quick-import", applyDefaultUrl)
|
|
1970
|
+
),
|
|
1971
|
+
h(
|
|
1972
|
+
"div",
|
|
1973
|
+
{ className: import_client.default.formGrid, style: S.formGrid, key: "default:form-grid" },
|
|
1974
|
+
field(
|
|
1975
|
+
t.protocol,
|
|
1976
|
+
h(
|
|
1977
|
+
"select",
|
|
1978
|
+
{
|
|
1979
|
+
className: import_client.default.input,
|
|
1980
|
+
style: S.input,
|
|
1981
|
+
value: defaultView.protocol,
|
|
1982
|
+
onChange: (event) => bindDefault("protocol")(event.target.value)
|
|
1983
|
+
},
|
|
1984
|
+
...PROTOCOLS2.map(
|
|
1985
|
+
(protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
|
|
1986
|
+
)
|
|
1987
|
+
)
|
|
1988
|
+
),
|
|
1989
|
+
field(
|
|
1990
|
+
t.host,
|
|
1991
|
+
h("input", {
|
|
1103
1992
|
className: import_client.default.input,
|
|
1104
1993
|
style: S.input,
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
)
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
type: "number",
|
|
1133
|
-
min: 1,
|
|
1134
|
-
max: 65535,
|
|
1135
|
-
placeholder: "7890",
|
|
1136
|
-
value: defaultView.port === 0 ? "" : String(defaultView.port),
|
|
1137
|
-
onChange: (event) => {
|
|
1138
|
-
const parsed = Number.parseInt(event.target.value, 10);
|
|
1139
|
-
bindDefault("port")(Number.isNaN(parsed) ? 0 : parsed);
|
|
1140
|
-
}
|
|
1141
|
-
})
|
|
1142
|
-
),
|
|
1143
|
-
field(
|
|
1144
|
-
t.username,
|
|
1145
|
-
h("input", {
|
|
1146
|
-
className: import_client.default.input,
|
|
1147
|
-
style: S.input,
|
|
1148
|
-
autoComplete: "off",
|
|
1149
|
-
value: defaultView.username,
|
|
1150
|
-
onChange: (event) => bindDefault("username")(event.target.value)
|
|
1151
|
-
})
|
|
1152
|
-
),
|
|
1153
|
-
field(
|
|
1154
|
-
t.password,
|
|
1155
|
-
h(
|
|
1156
|
-
"div",
|
|
1157
|
-
{ style: S.passwordControl },
|
|
1994
|
+
placeholder: "127.0.0.1",
|
|
1995
|
+
value: defaultView.host,
|
|
1996
|
+
onChange: (event) => bindDefault("host")(event.target.value),
|
|
1997
|
+
// Same convenience as the cards: a full URL fills the whole block.
|
|
1998
|
+
onPaste: onProxyUrlPaste(applyDefaultUrl)
|
|
1999
|
+
}),
|
|
2000
|
+
defaultHostWhy ?? t.hostHint,
|
|
2001
|
+
defaultHostWhy !== void 0
|
|
2002
|
+
),
|
|
2003
|
+
field(
|
|
2004
|
+
t.port,
|
|
2005
|
+
h("input", {
|
|
2006
|
+
className: import_client.default.input,
|
|
2007
|
+
style: S.input,
|
|
2008
|
+
type: "number",
|
|
2009
|
+
min: 1,
|
|
2010
|
+
max: 65535,
|
|
2011
|
+
placeholder: "7890",
|
|
2012
|
+
value: defaultView.port === 0 ? "" : String(defaultView.port),
|
|
2013
|
+
onChange: (event) => {
|
|
2014
|
+
const parsed = Number.parseInt(event.target.value, 10);
|
|
2015
|
+
bindDefault("port")(Number.isNaN(parsed) ? 0 : parsed);
|
|
2016
|
+
}
|
|
2017
|
+
})
|
|
2018
|
+
),
|
|
2019
|
+
field(
|
|
2020
|
+
t.username,
|
|
1158
2021
|
h("input", {
|
|
1159
2022
|
className: import_client.default.input,
|
|
1160
|
-
style:
|
|
1161
|
-
type: defaultPasswordVisible === true ? "text" : "password",
|
|
2023
|
+
style: S.input,
|
|
1162
2024
|
autoComplete: "off",
|
|
1163
|
-
value: defaultView.
|
|
1164
|
-
onChange: (event) => bindDefault("
|
|
1165
|
-
})
|
|
1166
|
-
passwordToggle(defaultPasswordVisible === true, toggleDefaultPassword)
|
|
2025
|
+
value: defaultView.username,
|
|
2026
|
+
onChange: (event) => bindDefault("username")(event.target.value)
|
|
2027
|
+
})
|
|
1167
2028
|
),
|
|
1168
|
-
|
|
2029
|
+
field(
|
|
2030
|
+
t.password,
|
|
2031
|
+
h(
|
|
2032
|
+
"div",
|
|
2033
|
+
{ style: S.passwordControl },
|
|
2034
|
+
h("input", {
|
|
2035
|
+
className: import_client.default.input,
|
|
2036
|
+
style: { ...S.input, paddingRight: "36px" },
|
|
2037
|
+
type: defaultPasswordVisible === true ? "text" : "password",
|
|
2038
|
+
autoComplete: "off",
|
|
2039
|
+
value: defaultView.password,
|
|
2040
|
+
onChange: (event) => bindDefault("password")(event.target.value)
|
|
2041
|
+
}),
|
|
2042
|
+
passwordToggle(defaultPasswordVisible === true, toggleDefaultPassword)
|
|
2043
|
+
),
|
|
2044
|
+
t.passwordHintProxy,
|
|
2045
|
+
false,
|
|
2046
|
+
true
|
|
2047
|
+
)
|
|
1169
2048
|
),
|
|
1170
2049
|
// Bulk opt-in, in ONE write: a per-card walk would rebuild the table N times.
|
|
1171
2050
|
// Disabled while the default configuration is incomplete, because pointing every card at an
|
|
@@ -1187,86 +2066,533 @@ function renderPage(ctx) {
|
|
|
1187
2066
|
)
|
|
1188
2067
|
);
|
|
1189
2068
|
const storedTestUrl = typeof snapshot.value?.testUrl === "string" ? snapshot.value.testUrl : DEFAULT_TEST_URL;
|
|
2069
|
+
const profileManager = panel({
|
|
2070
|
+
key: "profile-manager",
|
|
2071
|
+
summary: `${t.profileManagerTitle} (${profileEntries.length})`,
|
|
2072
|
+
lead: t.profileManagerLead,
|
|
2073
|
+
open: profileManagerOpen === true,
|
|
2074
|
+
onToggle: (open) => setProfileManagerOpen(open),
|
|
2075
|
+
children: [
|
|
2076
|
+
h(
|
|
2077
|
+
"div",
|
|
2078
|
+
{ style: { ...S.testRow, alignItems: "stretch" }, key: "profile:add" },
|
|
2079
|
+
h("input", {
|
|
2080
|
+
className: import_client.default.input,
|
|
2081
|
+
style: { ...S.input, flex: "1 1 160px", minWidth: "0" },
|
|
2082
|
+
placeholder: t.profileNamePlaceholder,
|
|
2083
|
+
value: newProfileName,
|
|
2084
|
+
"aria-label": t.profileNameLabel,
|
|
2085
|
+
onChange: (event) => setNewProfileName(event.target.value),
|
|
2086
|
+
// Enter is the obvious gesture in a one-field form; without it the user
|
|
2087
|
+
// has to reach for the button after typing, which is the same dead end
|
|
2088
|
+
// the native prompt left them in.
|
|
2089
|
+
onKeyDown: (event) => {
|
|
2090
|
+
if (event?.key !== "Enter") return;
|
|
2091
|
+
event.preventDefault?.();
|
|
2092
|
+
addProfile();
|
|
2093
|
+
}
|
|
2094
|
+
}),
|
|
2095
|
+
h(
|
|
2096
|
+
"button",
|
|
2097
|
+
{
|
|
2098
|
+
className: import_client.default.testButton,
|
|
2099
|
+
style: { ...S.testButton, flex: "none" },
|
|
2100
|
+
type: "button",
|
|
2101
|
+
onClick: addProfile
|
|
2102
|
+
},
|
|
2103
|
+
t.profileAdd
|
|
2104
|
+
)
|
|
2105
|
+
),
|
|
2106
|
+
// The built-in preset is EDITED in the default block above (one source of
|
|
2107
|
+
// truth); here it is only listed, so the picker's options and this list
|
|
2108
|
+
// agree.
|
|
2109
|
+
h(
|
|
2110
|
+
"div",
|
|
2111
|
+
{ key: "profile:builtin", style: S.inheritNote },
|
|
2112
|
+
`${DEFAULT_PROFILE_LABEL} \u2014 ${endpointSummary(defaultView)}`
|
|
2113
|
+
),
|
|
2114
|
+
...profileEntries.map((entry) => {
|
|
2115
|
+
const bound = routesBoundTo({ ...configured, ...draft }, entry.name);
|
|
2116
|
+
const why = defaultProxyProblem(entry.value);
|
|
2117
|
+
return h(
|
|
2118
|
+
"div",
|
|
2119
|
+
{
|
|
2120
|
+
key: `profile:${entry.name}`,
|
|
2121
|
+
className: import_client.default.profileCard,
|
|
2122
|
+
style: {
|
|
2123
|
+
display: "flex",
|
|
2124
|
+
flexDirection: "column",
|
|
2125
|
+
gap: "8px",
|
|
2126
|
+
padding: "12px 14px",
|
|
2127
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
2128
|
+
borderRadius: "10px",
|
|
2129
|
+
background: "var(--dsw-alias-bg-layer-1)"
|
|
2130
|
+
}
|
|
2131
|
+
},
|
|
2132
|
+
h(
|
|
2133
|
+
"div",
|
|
2134
|
+
{ style: S.head },
|
|
2135
|
+
h(
|
|
2136
|
+
"div",
|
|
2137
|
+
{ style: S.titles },
|
|
2138
|
+
h("span", { style: S.name }, entry.name),
|
|
2139
|
+
h(
|
|
2140
|
+
"span",
|
|
2141
|
+
{
|
|
2142
|
+
className: why === void 0 ? import_client.default.pillOn : import_client.default.pillOff,
|
|
2143
|
+
style: { ...S.pill, ...why === void 0 ? S.pillOn : S.pillOff }
|
|
2144
|
+
},
|
|
2145
|
+
why === void 0 ? t.complete : t.notConfigured
|
|
2146
|
+
)
|
|
2147
|
+
),
|
|
2148
|
+
h(
|
|
2149
|
+
"div",
|
|
2150
|
+
{ style: { display: "flex", gap: "6px", flex: "none" } },
|
|
2151
|
+
h(
|
|
2152
|
+
"button",
|
|
2153
|
+
{
|
|
2154
|
+
className: import_client.default.testButton,
|
|
2155
|
+
style: { ...S.testButton, padding: "3px 8px", fontSize: "11px" },
|
|
2156
|
+
type: "button",
|
|
2157
|
+
onClick: () => {
|
|
2158
|
+
setRenamingProfile(entry.name);
|
|
2159
|
+
setRenameDraft(entry.name);
|
|
2160
|
+
}
|
|
2161
|
+
},
|
|
2162
|
+
t.profileRename
|
|
2163
|
+
),
|
|
2164
|
+
h(
|
|
2165
|
+
"button",
|
|
2166
|
+
{
|
|
2167
|
+
className: import_client.default.testButton,
|
|
2168
|
+
style: { ...S.testButton, padding: "3px 8px", fontSize: "11px" },
|
|
2169
|
+
type: "button",
|
|
2170
|
+
onClick: () => deleteProfile(entry.name)
|
|
2171
|
+
},
|
|
2172
|
+
t.profileDelete
|
|
2173
|
+
)
|
|
2174
|
+
)
|
|
2175
|
+
),
|
|
2176
|
+
// The rename field, shown in place of the row title's edit gesture.
|
|
2177
|
+
// Inline rather than a modal, for the same reason the add field is
|
|
2178
|
+
// inline: the Desktop build has no `window.prompt`.
|
|
2179
|
+
renamingProfile !== entry.name ? null : h(
|
|
2180
|
+
"div",
|
|
2181
|
+
{ style: { ...S.testRow, alignItems: "stretch" }, key: `profile:${entry.name}:rename` },
|
|
2182
|
+
h("input", {
|
|
2183
|
+
className: import_client.default.input,
|
|
2184
|
+
style: { ...S.input, flex: "1 1 160px", minWidth: "0" },
|
|
2185
|
+
value: renameDraft,
|
|
2186
|
+
placeholder: t.profileRenamePrompt,
|
|
2187
|
+
"aria-label": t.profileRenamePrompt,
|
|
2188
|
+
autoFocus: true,
|
|
2189
|
+
onChange: (event) => setRenameDraft(event.target.value),
|
|
2190
|
+
onKeyDown: (event) => {
|
|
2191
|
+
if (event?.key === "Escape") {
|
|
2192
|
+
setRenamingProfile(void 0);
|
|
2193
|
+
setRenameDraft("");
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
if (event?.key !== "Enter") return;
|
|
2197
|
+
event.preventDefault?.();
|
|
2198
|
+
renameProfile(entry.name);
|
|
2199
|
+
}
|
|
2200
|
+
}),
|
|
2201
|
+
h(
|
|
2202
|
+
"button",
|
|
2203
|
+
{
|
|
2204
|
+
className: import_client.default.testButton,
|
|
2205
|
+
style: { ...S.testButton, flex: "none" },
|
|
2206
|
+
type: "button",
|
|
2207
|
+
onClick: () => renameProfile(entry.name)
|
|
2208
|
+
},
|
|
2209
|
+
t.profileRenameConfirm
|
|
2210
|
+
),
|
|
2211
|
+
h(
|
|
2212
|
+
"button",
|
|
2213
|
+
{
|
|
2214
|
+
className: import_client.default.testButton,
|
|
2215
|
+
style: { ...S.testButton, flex: "none" },
|
|
2216
|
+
type: "button",
|
|
2217
|
+
onClick: () => {
|
|
2218
|
+
setRenamingProfile(void 0);
|
|
2219
|
+
setRenameDraft("");
|
|
2220
|
+
}
|
|
2221
|
+
},
|
|
2222
|
+
t.cancel
|
|
2223
|
+
)
|
|
2224
|
+
),
|
|
2225
|
+
bound.length === 0 ? null : h("p", { style: S.inheritNote }, `${t.profileUsedBy}${bound.join("\u3001")}`),
|
|
2226
|
+
h(
|
|
2227
|
+
"div",
|
|
2228
|
+
{ style: S.testRow },
|
|
2229
|
+
quickImportSelect(`profile:${entry.name}:quick-import`, (parts) => {
|
|
2230
|
+
setProfileDraft((current) => ({
|
|
2231
|
+
...current,
|
|
2232
|
+
[entry.name]: { ...current[entry.name], ...parts }
|
|
2233
|
+
}));
|
|
2234
|
+
invalidateTests(bound);
|
|
2235
|
+
saveMany(
|
|
2236
|
+
`\0profile\0${entry.name}\0*quick`,
|
|
2237
|
+
Object.keys(parts).map((field2) => ({
|
|
2238
|
+
op: "set",
|
|
2239
|
+
path: ["proxyProfiles", entry.name, field2],
|
|
2240
|
+
value: parts[field2]
|
|
2241
|
+
})),
|
|
2242
|
+
`${entry.name}\uFF08\u5FEB\u6377\u5BFC\u5165\uFF09`,
|
|
2243
|
+
() => {
|
|
2244
|
+
setProfileDraft((current) => {
|
|
2245
|
+
const value = current[entry.name];
|
|
2246
|
+
if (value === void 0) return current;
|
|
2247
|
+
const nextValue = { ...value };
|
|
2248
|
+
for (const field2 of Object.keys(parts)) delete nextValue[field2];
|
|
2249
|
+
const next = { ...current };
|
|
2250
|
+
if (Object.keys(nextValue).length === 0) delete next[entry.name];
|
|
2251
|
+
else next[entry.name] = nextValue;
|
|
2252
|
+
return next;
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
);
|
|
2256
|
+
})
|
|
2257
|
+
),
|
|
2258
|
+
h(
|
|
2259
|
+
"div",
|
|
2260
|
+
{ className: import_client.default.formGrid, style: S.formGrid },
|
|
2261
|
+
field(
|
|
2262
|
+
t.protocol,
|
|
2263
|
+
h(
|
|
2264
|
+
"select",
|
|
2265
|
+
{
|
|
2266
|
+
className: import_client.default.input,
|
|
2267
|
+
style: S.input,
|
|
2268
|
+
value: entry.value.protocol,
|
|
2269
|
+
onChange: (event) => bindProfile(entry.name, "protocol")(event.target.value)
|
|
2270
|
+
},
|
|
2271
|
+
...PROTOCOLS2.map(
|
|
2272
|
+
(protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
|
|
2273
|
+
)
|
|
2274
|
+
),
|
|
2275
|
+
void 0
|
|
2276
|
+
),
|
|
2277
|
+
field(
|
|
2278
|
+
t.host,
|
|
2279
|
+
h("input", {
|
|
2280
|
+
className: import_client.default.input,
|
|
2281
|
+
style: S.input,
|
|
2282
|
+
placeholder: "127.0.0.1",
|
|
2283
|
+
value: entry.value.host,
|
|
2284
|
+
onChange: (event) => bindProfile(entry.name, "host")(event.target.value)
|
|
2285
|
+
}),
|
|
2286
|
+
entry.value.host.trim().length === 0 ? void 0 : proxyHostProblem(entry.value.host),
|
|
2287
|
+
entry.value.host.trim().length > 0 && proxyHostProblem(entry.value.host) !== void 0
|
|
2288
|
+
),
|
|
2289
|
+
field(
|
|
2290
|
+
t.port,
|
|
2291
|
+
h("input", {
|
|
2292
|
+
className: import_client.default.input,
|
|
2293
|
+
style: S.input,
|
|
2294
|
+
type: "number",
|
|
2295
|
+
min: 1,
|
|
2296
|
+
max: 65535,
|
|
2297
|
+
placeholder: "7891",
|
|
2298
|
+
value: entry.value.port === 0 ? "" : String(entry.value.port),
|
|
2299
|
+
onChange: (event) => {
|
|
2300
|
+
const parsed = Number.parseInt(event.target.value, 10);
|
|
2301
|
+
bindProfile(entry.name, "port")(Number.isNaN(parsed) ? 0 : parsed);
|
|
2302
|
+
}
|
|
2303
|
+
}),
|
|
2304
|
+
void 0
|
|
2305
|
+
),
|
|
2306
|
+
field(
|
|
2307
|
+
t.username,
|
|
2308
|
+
h("input", {
|
|
2309
|
+
className: import_client.default.input,
|
|
2310
|
+
style: S.input,
|
|
2311
|
+
autoComplete: "off",
|
|
2312
|
+
value: entry.value.username,
|
|
2313
|
+
onChange: (event) => bindProfile(entry.name, "username")(event.target.value)
|
|
2314
|
+
}),
|
|
2315
|
+
void 0
|
|
2316
|
+
),
|
|
2317
|
+
field(
|
|
2318
|
+
t.password,
|
|
2319
|
+
h(
|
|
2320
|
+
"div",
|
|
2321
|
+
{ style: S.passwordControl },
|
|
2322
|
+
h("input", {
|
|
2323
|
+
className: import_client.default.input,
|
|
2324
|
+
style: { ...S.input, paddingRight: "36px" },
|
|
2325
|
+
type: "password",
|
|
2326
|
+
autoComplete: "off",
|
|
2327
|
+
value: entry.value.password,
|
|
2328
|
+
onChange: (event) => bindProfile(entry.name, "password")(event.target.value)
|
|
2329
|
+
})
|
|
2330
|
+
),
|
|
2331
|
+
t.passwordHintProxy,
|
|
2332
|
+
false,
|
|
2333
|
+
true
|
|
2334
|
+
)
|
|
2335
|
+
)
|
|
2336
|
+
);
|
|
2337
|
+
})
|
|
2338
|
+
]
|
|
2339
|
+
});
|
|
1190
2340
|
const storedLocalDirect = snapshot.value?.localDirect !== false;
|
|
1191
2341
|
const globalTestUrl = typeof globalDraft.testUrl === "string" ? globalDraft.testUrl : storedTestUrl;
|
|
1192
2342
|
const globalLocalDirect = typeof globalDraft.localDirect === "boolean" ? globalDraft.localDirect : storedLocalDirect;
|
|
1193
2343
|
const globalTestUrlHint = testUrlHint(globalTestUrl, t);
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
...
|
|
1200
|
-
|
|
2344
|
+
const exportBackup = () => {
|
|
2345
|
+
const includePasswords = globalThis.window?.confirm?.(t.backupIncludePasswords) === true;
|
|
2346
|
+
const document2 = buildBackup(
|
|
2347
|
+
{
|
|
2348
|
+
providers: { ...configured, ...draft },
|
|
2349
|
+
proxyProfiles: { ...storedProfiles, ...profileDraft },
|
|
2350
|
+
defaultProxy: { ...storedDefault, ...defaultDraft },
|
|
2351
|
+
localDirect: globalLocalDirect,
|
|
2352
|
+
testUrl: globalTestUrl
|
|
1201
2353
|
},
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
2354
|
+
{ includePasswords }
|
|
2355
|
+
);
|
|
2356
|
+
try {
|
|
2357
|
+
const text = JSON.stringify(document2, null, 2);
|
|
2358
|
+
const blob = new globalThis.Blob([text], { type: "application/json" });
|
|
2359
|
+
const url = globalThis.URL.createObjectURL(blob);
|
|
2360
|
+
const anchor = globalThis.document.createElement("a");
|
|
2361
|
+
anchor.href = url;
|
|
2362
|
+
anchor.download = backupFileName();
|
|
2363
|
+
anchor.click();
|
|
2364
|
+
globalThis.setTimeout(() => globalThis.URL.revokeObjectURL(url), 0);
|
|
2365
|
+
setSavedNote(`${t.backupExported}${includePasswords ? t.backupWithPasswords : t.backupWithoutPasswords}`);
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
setSavedNote(void 0);
|
|
2368
|
+
setSaveError({ label: t.backupExport, message: String(error?.message ?? error) });
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
const importBackupFile = async (file) => {
|
|
2372
|
+
let text;
|
|
2373
|
+
try {
|
|
2374
|
+
text = await file.text();
|
|
2375
|
+
} catch (error) {
|
|
2376
|
+
setSaveError({ label: t.backupImport, message: String(error?.message ?? error) });
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
const parsed = parseBackup(text);
|
|
2380
|
+
if (!parsed.ok) {
|
|
2381
|
+
setSavedNote(void 0);
|
|
2382
|
+
setSaveError({ label: t.backupImport, message: parsed.error });
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
const counts = backupCounts(parsed.document);
|
|
2386
|
+
const confirmed = globalThis.window?.confirm?.(
|
|
2387
|
+
`${t.backupConfirm}${counts.providers}${t.backupConfirmProviders}${counts.profiles}${t.backupConfirmProfiles}`
|
|
2388
|
+
);
|
|
2389
|
+
if (confirmed !== true) return;
|
|
2390
|
+
try {
|
|
2391
|
+
await enqueueWrite(() => writeConfig(ctx, backupOps(parsed.document)));
|
|
2392
|
+
setSaveError(void 0);
|
|
2393
|
+
setSavedNote(`${t.backupRestored}${counts.providers}`);
|
|
2394
|
+
setDraft({});
|
|
2395
|
+
setDefaultDraft({});
|
|
2396
|
+
setProfileDraft({});
|
|
2397
|
+
setGlobalDraft({});
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
setSavedNote(void 0);
|
|
2400
|
+
setSaveError({ label: t.backupImport, message: String(error?.message ?? error) });
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
const backupBlock = h(
|
|
2404
|
+
"div",
|
|
2405
|
+
{ style: S.backupBar, key: "backup" },
|
|
1206
2406
|
h(
|
|
1207
|
-
"
|
|
1208
|
-
{
|
|
1209
|
-
h("span", {
|
|
1210
|
-
h("
|
|
1211
|
-
className: import_client.default.input,
|
|
1212
|
-
style: S.input,
|
|
1213
|
-
value: globalTestUrl,
|
|
1214
|
-
onChange: (event) => bindGlobal("testUrl")(event.target.value)
|
|
1215
|
-
}),
|
|
1216
|
-
globalTestUrlHint === void 0 ? h("span", { style: S.hint }, t.testUrlHint) : h(
|
|
1217
|
-
"span",
|
|
1218
|
-
{
|
|
1219
|
-
className: import_client.default.hint,
|
|
1220
|
-
style: {
|
|
1221
|
-
...S.hint,
|
|
1222
|
-
color: "var(--dsw-alias-state-error-primary, #f85149)"
|
|
1223
|
-
}
|
|
1224
|
-
},
|
|
1225
|
-
globalTestUrlHint
|
|
1226
|
-
)
|
|
2407
|
+
"div",
|
|
2408
|
+
{ style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: "0" } },
|
|
2409
|
+
h("span", { style: { ...S.name, fontSize: "12px" } }, t.backupTitle),
|
|
2410
|
+
h("span", { style: S.defaultLead }, t.backupLead)
|
|
1227
2411
|
),
|
|
1228
2412
|
h(
|
|
1229
2413
|
"div",
|
|
1230
|
-
{ style: S.testRow },
|
|
2414
|
+
{ style: { ...S.testRow, flex: "none" } },
|
|
1231
2415
|
h(
|
|
1232
2416
|
"button",
|
|
1233
2417
|
{
|
|
1234
2418
|
className: import_client.default.testButton,
|
|
1235
2419
|
style: S.testButton,
|
|
1236
2420
|
type: "button",
|
|
1237
|
-
onClick:
|
|
2421
|
+
onClick: exportBackup
|
|
1238
2422
|
},
|
|
1239
|
-
t.
|
|
2423
|
+
t.backupExport
|
|
2424
|
+
),
|
|
2425
|
+
h(
|
|
2426
|
+
"label",
|
|
2427
|
+
{
|
|
2428
|
+
className: import_client.default.testButton,
|
|
2429
|
+
style: { ...S.testButton, display: "inline-flex", alignItems: "center" },
|
|
2430
|
+
key: "backup:import"
|
|
2431
|
+
},
|
|
2432
|
+
t.backupImport,
|
|
2433
|
+
h("input", {
|
|
2434
|
+
type: "file",
|
|
2435
|
+
accept: "application/json,.json",
|
|
2436
|
+
style: { display: "none" },
|
|
2437
|
+
onChange: (event) => {
|
|
2438
|
+
const file = event?.target?.files?.[0];
|
|
2439
|
+
if (file !== void 0) void importBackupFile(file);
|
|
2440
|
+
if (event?.target !== void 0) event.target.value = "";
|
|
2441
|
+
}
|
|
2442
|
+
})
|
|
2443
|
+
)
|
|
2444
|
+
)
|
|
2445
|
+
);
|
|
2446
|
+
const globalBlock = panel({
|
|
2447
|
+
key: "global-settings",
|
|
2448
|
+
summary: t.globalTitle,
|
|
2449
|
+
lead: t.globalLead,
|
|
2450
|
+
children: [
|
|
2451
|
+
h(
|
|
2452
|
+
"label",
|
|
2453
|
+
{ className: import_client.default.field, style: S.field },
|
|
2454
|
+
h("span", { className: import_client.default.label, style: S.label }, t.testUrl),
|
|
2455
|
+
h("input", {
|
|
2456
|
+
className: import_client.default.input,
|
|
2457
|
+
style: S.input,
|
|
2458
|
+
value: globalTestUrl,
|
|
2459
|
+
onChange: (event) => bindGlobal("testUrl")(event.target.value)
|
|
2460
|
+
}),
|
|
2461
|
+
globalTestUrlHint === void 0 ? h("span", { className: import_client.default.hint, style: S.hint }, t.testUrlHint) : h(
|
|
2462
|
+
"span",
|
|
2463
|
+
{
|
|
2464
|
+
className: import_client.default.hint,
|
|
2465
|
+
style: {
|
|
2466
|
+
...S.hint,
|
|
2467
|
+
color: "var(--dsw-alias-state-error-primary, #f85149)"
|
|
2468
|
+
}
|
|
2469
|
+
},
|
|
2470
|
+
globalTestUrlHint
|
|
2471
|
+
)
|
|
2472
|
+
),
|
|
2473
|
+
h(
|
|
2474
|
+
"div",
|
|
2475
|
+
{ style: S.testRow },
|
|
2476
|
+
h(
|
|
2477
|
+
"button",
|
|
2478
|
+
{
|
|
2479
|
+
className: import_client.default.testButton,
|
|
2480
|
+
style: S.testButton,
|
|
2481
|
+
type: "button",
|
|
2482
|
+
onClick: () => bindGlobal("testUrl")(DEFAULT_TEST_URL)
|
|
2483
|
+
},
|
|
2484
|
+
t.restoreDefaultTestUrl
|
|
2485
|
+
)
|
|
2486
|
+
),
|
|
2487
|
+
h(
|
|
2488
|
+
"label",
|
|
2489
|
+
{ className: import_client.default.toggle, style: S.toggle },
|
|
2490
|
+
h("input", {
|
|
2491
|
+
type: "checkbox",
|
|
2492
|
+
checked: globalLocalDirect,
|
|
2493
|
+
onChange: (event) => bindGlobal("localDirect")(event.target.checked === true)
|
|
2494
|
+
}),
|
|
2495
|
+
h("span", null, t.localDirect)
|
|
2496
|
+
),
|
|
2497
|
+
h(
|
|
2498
|
+
"p",
|
|
2499
|
+
{ className: import_client.default.hint, style: { ...S.hint, margin: "2px 0 0" } },
|
|
2500
|
+
globalLocalDirect ? t.localDirectOnHint : t.localDirectOffHint
|
|
2501
|
+
)
|
|
2502
|
+
]
|
|
2503
|
+
});
|
|
2504
|
+
const routeStateOf = (row) => {
|
|
2505
|
+
const route = view({ ...configured[row.id], ...draft[row.id] });
|
|
2506
|
+
const recorded = problems[row.id];
|
|
2507
|
+
const state2 = cardEndpoint(route);
|
|
2508
|
+
const problemText = recorded !== void 0 ? recorded : route.enabled ? cardProblemText(state2) : void 0;
|
|
2509
|
+
if (route.enabled && problemText !== void 0) return "rejected";
|
|
2510
|
+
if (route.enabled) return "proxied";
|
|
2511
|
+
return "direct";
|
|
2512
|
+
};
|
|
2513
|
+
const query = filter.trim().toLowerCase();
|
|
2514
|
+
const matchedKeywordRows = query.length === 0 ? rows : rows.filter((row) => `${row.id} ${row.name}`.toLowerCase().includes(query));
|
|
2515
|
+
const countActive = matchedKeywordRows.filter((r) => routeStateOf(r) === "proxied").length;
|
|
2516
|
+
const countDirect = matchedKeywordRows.filter((r) => routeStateOf(r) === "direct").length;
|
|
2517
|
+
const countRejected = matchedKeywordRows.filter((r) => routeStateOf(r) === "rejected").length;
|
|
2518
|
+
const shown = matchedKeywordRows.filter((row) => {
|
|
2519
|
+
if (statusFilter === "all") return true;
|
|
2520
|
+
return routeStateOf(row) === statusFilter;
|
|
2521
|
+
});
|
|
2522
|
+
const listToolbar = rows.length === 0 ? null : h(
|
|
2523
|
+
"div",
|
|
2524
|
+
{ style: S.toolbar, key: "list-toolbar" },
|
|
2525
|
+
h(
|
|
2526
|
+
"div",
|
|
2527
|
+
{ className: import_client.default.chipRow, style: S.chipRow, key: "status-chips" },
|
|
2528
|
+
[
|
|
2529
|
+
{ id: "all", label: `${t.filterStatusAll} (${matchedKeywordRows.length})` },
|
|
2530
|
+
{ id: "proxied", label: `${t.filterStatusProxied} (${countActive})` },
|
|
2531
|
+
{ id: "direct", label: `${t.filterStatusDirect} (${countDirect})` },
|
|
2532
|
+
{ id: "rejected", label: `${t.filterStatusRejected} (${countRejected})` }
|
|
2533
|
+
].map(
|
|
2534
|
+
({ id, label }) => h(
|
|
2535
|
+
"button",
|
|
2536
|
+
{
|
|
2537
|
+
key: id,
|
|
2538
|
+
type: "button",
|
|
2539
|
+
className: statusFilter === id ? import_client.default.chipActive : import_client.default.chip,
|
|
2540
|
+
style: {
|
|
2541
|
+
...S.chip,
|
|
2542
|
+
...statusFilter === id ? S.chipActive : {}
|
|
2543
|
+
},
|
|
2544
|
+
// Selection is a STATE, not a decoration: `aria-pressed`
|
|
2545
|
+
// makes exactly one chip self-describing to assistive tech,
|
|
2546
|
+
// and it is what the regression test reads instead of
|
|
2547
|
+
// trusting a colour.
|
|
2548
|
+
"aria-pressed": statusFilter === id,
|
|
2549
|
+
onClick: () => {
|
|
2550
|
+
setStatusFilter(id);
|
|
2551
|
+
writeViewPrefs({ statusFilter: id });
|
|
2552
|
+
}
|
|
2553
|
+
},
|
|
2554
|
+
label
|
|
2555
|
+
)
|
|
1240
2556
|
)
|
|
1241
2557
|
),
|
|
1242
2558
|
h(
|
|
1243
|
-
"
|
|
1244
|
-
{ className: import_client.default.
|
|
2559
|
+
"div",
|
|
2560
|
+
{ className: import_client.default.toolbarRow, style: S.toolbarRow, key: "list-actions" },
|
|
1245
2561
|
h("input", {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
2562
|
+
className: import_client.default.input,
|
|
2563
|
+
style: { ...S.input, flex: "1 1 180px", minWidth: "0" },
|
|
2564
|
+
placeholder: t.filterPlaceholder,
|
|
2565
|
+
"aria-label": t.filterLabel,
|
|
2566
|
+
value: filter,
|
|
2567
|
+
onChange: (event) => setFilter(event.target.value)
|
|
1249
2568
|
}),
|
|
1250
|
-
h(
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
2569
|
+
h(
|
|
2570
|
+
"button",
|
|
2571
|
+
{
|
|
2572
|
+
className: import_client.default.testButton,
|
|
2573
|
+
style: { ...S.testButton, flex: "none", whiteSpace: "nowrap" },
|
|
2574
|
+
type: "button",
|
|
2575
|
+
disabled: countActive === 0 || batchProgress !== null,
|
|
2576
|
+
onClick: runBatchTest
|
|
2577
|
+
},
|
|
2578
|
+
batchProgress !== null ? `${t.batchTesting} (${batchProgress.done}/${batchProgress.total})` : `${t.batchTest} (${countActive})`
|
|
2579
|
+
),
|
|
2580
|
+
h(
|
|
2581
|
+
"button",
|
|
2582
|
+
{
|
|
2583
|
+
className: import_client.default.testButton,
|
|
2584
|
+
style: { ...S.testButton, flex: "none", whiteSpace: "nowrap" },
|
|
2585
|
+
type: "button",
|
|
2586
|
+
onClick: () => setViewMode((m) => {
|
|
2587
|
+
const next = m === "card" ? "compact" : "card";
|
|
2588
|
+
writeViewPrefs({ viewMode: next });
|
|
2589
|
+
return next;
|
|
2590
|
+
})
|
|
2591
|
+
},
|
|
2592
|
+
viewMode === "card" ? t.viewCompact : t.viewCard
|
|
2593
|
+
)
|
|
1256
2594
|
)
|
|
1257
2595
|
);
|
|
1258
|
-
const query = filter.trim().toLowerCase();
|
|
1259
|
-
const shown = query.length === 0 ? rows : rows.filter((row) => `${row.id} ${row.name}`.toLowerCase().includes(query));
|
|
1260
|
-
const filterRow = rows.length === 0 ? null : field(
|
|
1261
|
-
t.filterLabel,
|
|
1262
|
-
h("input", {
|
|
1263
|
-
className: import_client.default.input,
|
|
1264
|
-
style: S.input,
|
|
1265
|
-
placeholder: t.filterPlaceholder,
|
|
1266
|
-
value: filter,
|
|
1267
|
-
onChange: (event) => setFilter(event.target.value)
|
|
1268
|
-
})
|
|
1269
|
-
);
|
|
1270
2596
|
const catalogueStatus = rows.length === 0 ? state.pending ? { text: t.catalogueLoading, error: false } : state.error !== void 0 ? { text: `${t.catalogueError}${state.error}`, error: true } : void 0 : void 0;
|
|
1271
2597
|
const catalogueNotice = state.error !== void 0 && rows.length > 0 ? h(
|
|
1272
2598
|
"p",
|
|
@@ -1298,14 +2624,16 @@ function renderPage(ctx) {
|
|
|
1298
2624
|
const result = tests[row.id];
|
|
1299
2625
|
const onlyDead = deadRuleEntries(route.onlyHosts);
|
|
1300
2626
|
const bypassDead = deadRuleEntries(route.bypassHosts);
|
|
2627
|
+
const hostTemplate = providerHostTemplate(row.id);
|
|
1301
2628
|
const useDefault = route.useDefault === true;
|
|
1302
|
-
const
|
|
2629
|
+
const boundProfile = route.profileId.trim();
|
|
2630
|
+
const endpointState = cardEndpoint(route);
|
|
2631
|
+
const endpointStored = endpointState.complete;
|
|
1303
2632
|
const showTest = route.enabled || endpointStored;
|
|
1304
2633
|
const problemText = (() => {
|
|
1305
2634
|
const recorded = problems[row.id];
|
|
1306
2635
|
if (recorded !== void 0) return recorded;
|
|
1307
|
-
|
|
1308
|
-
return route.enabled ? defaultProxyProblem(route) : void 0;
|
|
2636
|
+
return route.enabled ? cardProblemText(endpointState) : void 0;
|
|
1309
2637
|
})();
|
|
1310
2638
|
const rejected = route.enabled && problemText !== void 0;
|
|
1311
2639
|
const endpointFields = [
|
|
@@ -1319,7 +2647,7 @@ function renderPage(ctx) {
|
|
|
1319
2647
|
value: route.protocol,
|
|
1320
2648
|
onChange: (event) => bind(row.id, "protocol")(event.target.value)
|
|
1321
2649
|
},
|
|
1322
|
-
...
|
|
2650
|
+
...PROTOCOLS2.map(
|
|
1323
2651
|
(protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
|
|
1324
2652
|
)
|
|
1325
2653
|
),
|
|
@@ -1384,11 +2712,13 @@ function renderPage(ctx) {
|
|
|
1384
2712
|
}),
|
|
1385
2713
|
passwordToggle(passwordVisible[row.id] === true, () => togglePassword(row.id))
|
|
1386
2714
|
),
|
|
1387
|
-
route.protocol === "socks5" ? t.passwordHint : t.passwordHintProxy
|
|
2715
|
+
route.protocol === "socks5" ? t.passwordHint : t.passwordHintProxy,
|
|
2716
|
+
false,
|
|
2717
|
+
true
|
|
1388
2718
|
)
|
|
1389
2719
|
];
|
|
1390
2720
|
const pill = rejected ? { className: import_client.default.pillRejected, style: { ...S.pill, ...S.pillRejected }, text: t.rejected } : route.enabled ? { className: import_client.default.pillOn, style: { ...S.pill, ...S.pillOn }, text: t.on } : { className: import_client.default.pillLive, style: { ...S.pill, ...S.pillLive }, text: t.off };
|
|
1391
|
-
|
|
2721
|
+
const cardSection = h(
|
|
1392
2722
|
"section",
|
|
1393
2723
|
{
|
|
1394
2724
|
className: rejected ? import_client.default.cardRejected : route.enabled ? import_client.default.cardActive : import_client.default.card,
|
|
@@ -1425,36 +2755,94 @@ function renderPage(ctx) {
|
|
|
1425
2755
|
),
|
|
1426
2756
|
h("span", { className: pill.className, style: pill.style }, pill.text)
|
|
1427
2757
|
),
|
|
2758
|
+
// The two route-level controls, on ONE row and in ONE shape: each is
|
|
2759
|
+
// a label over its control. They were a bare `label` (checkbox) next to
|
|
2760
|
+
// a two-column `field` (picker), so the card's own row looked like two
|
|
2761
|
+
// unrelated widgets stacked.
|
|
1428
2762
|
h(
|
|
1429
|
-
"
|
|
1430
|
-
{ className: import_client.default.
|
|
1431
|
-
h(
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
2763
|
+
"div",
|
|
2764
|
+
{ className: import_client.default.controlRow, style: S.controlRow, key: `${row.id}:controls` },
|
|
2765
|
+
h(
|
|
2766
|
+
"label",
|
|
2767
|
+
{ style: S.controlCell },
|
|
2768
|
+
h("span", { className: import_client.default.label, style: S.label }, t.enableLabel),
|
|
2769
|
+
h(
|
|
2770
|
+
"span",
|
|
2771
|
+
{ className: import_client.default.toggle, style: { ...S.toggle, paddingTop: "0" } },
|
|
2772
|
+
h("input", {
|
|
2773
|
+
type: "checkbox",
|
|
2774
|
+
checked: route.enabled,
|
|
2775
|
+
onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
|
|
2776
|
+
}),
|
|
2777
|
+
h("span", { style: { fontSize: "12px" } }, t.useProxy)
|
|
2778
|
+
)
|
|
2779
|
+
),
|
|
2780
|
+
// Which endpoint this card takes.
|
|
2781
|
+
//
|
|
2782
|
+
// Always a picker, never a checkbox: the decision has three possible
|
|
2783
|
+
// shapes (own fields / the built-in default / a named preset), and a
|
|
2784
|
+
// checkbox can only express the first two. Every option carries the
|
|
2785
|
+
// address it will use, because "预设: 香港专线" alone does not tell a
|
|
2786
|
+
// user which port it points at.
|
|
2787
|
+
h(
|
|
2788
|
+
"label",
|
|
2789
|
+
{ style: S.controlCell, key: `${row.id}:profile` },
|
|
2790
|
+
h("span", { className: import_client.default.label, style: S.label }, t.profileBinding),
|
|
2791
|
+
h(
|
|
2792
|
+
"select",
|
|
2793
|
+
{
|
|
2794
|
+
className: import_client.default.input,
|
|
2795
|
+
style: S.input,
|
|
2796
|
+
value: boundProfile.length > 0 ? boundProfile : useDefault ? DEFAULT_PROFILE_BINDING : "",
|
|
2797
|
+
onChange: (event) => {
|
|
2798
|
+
const value = String(event.target.value);
|
|
2799
|
+
bindRouteProfile(row.id)(value);
|
|
2800
|
+
}
|
|
2801
|
+
},
|
|
2802
|
+
h("option", { key: "own", value: "" }, t.profileBindingOwn),
|
|
2803
|
+
h(
|
|
2804
|
+
"option",
|
|
2805
|
+
{ key: "default", value: DEFAULT_PROFILE_BINDING },
|
|
2806
|
+
`${t.profileBindingPrefix}${DEFAULT_PROFILE_LABEL.replace(
|
|
2807
|
+
" (Default)",
|
|
2808
|
+
""
|
|
2809
|
+
)} (${endpointAddress(defaultView)})`
|
|
2810
|
+
),
|
|
2811
|
+
...profileEntries.map(
|
|
2812
|
+
(entry) => h(
|
|
2813
|
+
"option",
|
|
2814
|
+
{ key: entry.name, value: entry.name },
|
|
2815
|
+
`${t.profileBindingPrefix}${entry.name} (${endpointAddress(entry.value)})`
|
|
2816
|
+
)
|
|
2817
|
+
),
|
|
2818
|
+
// A route may still point at a preset that was deleted while this
|
|
2819
|
+
// page was open. Without an option for it the select would render
|
|
2820
|
+
// blank, which reads as "no choice made".
|
|
2821
|
+
boundProfile.length > 0 && !profileEntries.some((entry) => entry.name === boundProfile) ? h(
|
|
2822
|
+
"option",
|
|
2823
|
+
{ key: `missing:${boundProfile}`, value: boundProfile },
|
|
2824
|
+
`${t.profileBindingPrefix}${boundProfile}${t.profileMissingOption}`
|
|
2825
|
+
) : null
|
|
2826
|
+
)
|
|
2827
|
+
)
|
|
1451
2828
|
),
|
|
1452
|
-
problemText !== void 0 ? h("p", { key: `${row.id}:problem`, style: S.inheritWarning }, problemText) :
|
|
2829
|
+
problemText !== void 0 ? h("p", { key: `${row.id}:problem`, style: S.inheritWarning }, problemText) : boundProfile.length > 0 ? h(
|
|
2830
|
+
"p",
|
|
2831
|
+
{ key: `${row.id}:profile-note`, style: S.inheritNote },
|
|
2832
|
+
`${t.inheritingProfile}${boundProfile}\uFF1A${endpointSummary(endpointState.endpoint ?? {})}`
|
|
2833
|
+
) : useDefault ? h(
|
|
1453
2834
|
"p",
|
|
1454
2835
|
{ key: `${row.id}:inherit`, style: S.inheritNote },
|
|
1455
2836
|
`${t.inheritingDefault}${defaultOrigin}`
|
|
1456
2837
|
) : null,
|
|
1457
|
-
...useDefault ? [] :
|
|
2838
|
+
...useDefault || boundProfile.length > 0 ? [] : [
|
|
2839
|
+
h(
|
|
2840
|
+
"div",
|
|
2841
|
+
{ style: S.testRow, key: `${row.id}:quick-import` },
|
|
2842
|
+
quickImportSelect(`${row.id}:quick-import`, (parts) => applyCardUrl(row.id, parts))
|
|
2843
|
+
),
|
|
2844
|
+
h("div", { className: import_client.default.formGrid, style: S.formGrid, key: `${row.id}:form-grid` }, ...endpointFields)
|
|
2845
|
+
],
|
|
1458
2846
|
// The self-check is offered whenever there is something to check: an
|
|
1459
2847
|
// enabled route (the Host's table carries it, and a rejected one still
|
|
1460
2848
|
// reports WHY), or a disabled one whose endpoint is already complete —
|
|
@@ -1495,7 +2883,12 @@ function renderPage(ctx) {
|
|
|
1495
2883
|
{ key: "status" },
|
|
1496
2884
|
`HTTP ${result.status}`
|
|
1497
2885
|
) : null,
|
|
1498
|
-
result.ip === void 0 ? null : h(
|
|
2886
|
+
result.ip === void 0 ? null : h(
|
|
2887
|
+
"span",
|
|
2888
|
+
{ key: "ip", style: { display: "inline-flex", alignItems: "center", gap: "3px" } },
|
|
2889
|
+
`${t.exitIpLabel}${result.ip}`,
|
|
2890
|
+
copyButton(`${row.id}:ip`, result.ip, t.copy)
|
|
2891
|
+
),
|
|
1499
2892
|
result.direct === true ? h("span", { key: "direct" }, t.directLabel) : null,
|
|
1500
2893
|
result.cached === true ? h("span", { key: "cached" }, t.cachedLabel) : null,
|
|
1501
2894
|
result.target === void 0 ? null : h(
|
|
@@ -1506,13 +2899,22 @@ function renderPage(ctx) {
|
|
|
1506
2899
|
flexBasis: "100%",
|
|
1507
2900
|
fontSize: "11px",
|
|
1508
2901
|
color: "var(--dsw-alias-label-secondary)",
|
|
1509
|
-
wordBreak: "break-all"
|
|
2902
|
+
wordBreak: "break-all",
|
|
2903
|
+
display: "inline-flex",
|
|
2904
|
+
alignItems: "center",
|
|
2905
|
+
gap: "3px"
|
|
1510
2906
|
}
|
|
1511
2907
|
},
|
|
1512
|
-
`${t.testTargetLabel}${result.target}${targetSourceSuffix(result, t)}
|
|
2908
|
+
`${t.testTargetLabel}${result.target}${targetSourceSuffix(result, t)}`,
|
|
2909
|
+
copyButton(`${row.id}:target`, result.target, t.copy)
|
|
1513
2910
|
)
|
|
1514
2911
|
] : [
|
|
1515
|
-
h(
|
|
2912
|
+
h(
|
|
2913
|
+
"span",
|
|
2914
|
+
{ key: "err", style: { ...S.testFailure, display: "inline-flex", alignItems: "center", gap: "3px" } },
|
|
2915
|
+
`\u2717 ${result.error}`,
|
|
2916
|
+
copyButton(`${row.id}:err`, String(result.error), t.copy)
|
|
2917
|
+
),
|
|
1516
2918
|
typeof result.status === "number" ? h("span", { key: "status" }, `HTTP ${result.status}`) : null,
|
|
1517
2919
|
result.target === void 0 ? null : h(
|
|
1518
2920
|
"span",
|
|
@@ -1522,17 +2924,21 @@ function renderPage(ctx) {
|
|
|
1522
2924
|
flexBasis: "100%",
|
|
1523
2925
|
fontSize: "11px",
|
|
1524
2926
|
color: "var(--dsw-alias-label-secondary)",
|
|
1525
|
-
wordBreak: "break-all"
|
|
2927
|
+
wordBreak: "break-all",
|
|
2928
|
+
display: "inline-flex",
|
|
2929
|
+
alignItems: "center",
|
|
2930
|
+
gap: "3px"
|
|
1526
2931
|
}
|
|
1527
2932
|
},
|
|
1528
|
-
`${t.testTargetLabel}${result.target}${targetSourceSuffix(result, t)}
|
|
2933
|
+
`${t.testTargetLabel}${result.target}${targetSourceSuffix(result, t)}`,
|
|
2934
|
+
copyButton(`${row.id}:target`, result.target, t.copy)
|
|
1529
2935
|
)
|
|
1530
2936
|
]
|
|
1531
2937
|
) : null,
|
|
1532
2938
|
// Promote this card's endpoint to the shared default. Offered only
|
|
1533
2939
|
// when the card carries its OWN endpoint: an inheriting card would
|
|
1534
2940
|
// just be copying the default onto itself.
|
|
1535
|
-
endpointStored && !useDefault ? h(
|
|
2941
|
+
endpointStored && !useDefault && boundProfile.length === 0 ? h(
|
|
1536
2942
|
"button",
|
|
1537
2943
|
{
|
|
1538
2944
|
className: import_client.default.testButton,
|
|
@@ -1560,6 +2966,26 @@ function renderPage(ctx) {
|
|
|
1560
2966
|
onlyDead.length === 0 ? t.onlyHostsHint : `${t.ruleDeadHint}${onlyDead.join("\u3001")}`,
|
|
1561
2967
|
onlyDead.length > 0
|
|
1562
2968
|
),
|
|
2969
|
+
// The official-upstream pill. Offered only for a provider whose id
|
|
2970
|
+
// maps to a known template: a button that guessed a hostname for an
|
|
2971
|
+
// arbitrary relay would be worse than no button at all.
|
|
2972
|
+
hostTemplate === void 0 ? null : h(
|
|
2973
|
+
"div",
|
|
2974
|
+
{ style: S.chipRow, key: `${row.id}:host-template` },
|
|
2975
|
+
h(
|
|
2976
|
+
"button",
|
|
2977
|
+
{
|
|
2978
|
+
className: import_client.default.chip,
|
|
2979
|
+
style: S.chip,
|
|
2980
|
+
type: "button",
|
|
2981
|
+
title: hostTemplate.hosts,
|
|
2982
|
+
onClick: () => bind(row.id, "onlyHosts")(
|
|
2983
|
+
appendHostRules(route.onlyHosts, hostTemplate.hosts)
|
|
2984
|
+
)
|
|
2985
|
+
},
|
|
2986
|
+
`${t.loadHostTemplate}${hostTemplate.label}: ${hostTemplate.hosts}`
|
|
2987
|
+
)
|
|
2988
|
+
),
|
|
1563
2989
|
field(
|
|
1564
2990
|
t.bypassHosts,
|
|
1565
2991
|
h("input", {
|
|
@@ -1588,6 +3014,158 @@ function renderPage(ctx) {
|
|
|
1588
3014
|
)
|
|
1589
3015
|
)
|
|
1590
3016
|
);
|
|
3017
|
+
const isExpanded = expandedCards[row.id] === true;
|
|
3018
|
+
const endpointText = endpointState.missingProfile !== void 0 ? `${t.profileBindingPrefix}${endpointState.missingProfile}${t.profileMissingOption}` : boundProfile.length > 0 ? `${t.profileBindingPrefix}${boundProfile}: ${endpointSummary(endpointState.endpoint ?? {})}` : useDefault ? `${t.useDefault}: ${defaultOrigin}` : `${route.protocol}://${route.host || "\u2014"}:${route.port || "\u2014"}`;
|
|
3019
|
+
const compactRowElement = h(
|
|
3020
|
+
"section",
|
|
3021
|
+
{
|
|
3022
|
+
className: rejected ? import_client.default.compactRowRejected : route.enabled ? import_client.default.compactRowActive : import_client.default.compactRow,
|
|
3023
|
+
style: rejected ? { ...S.compactRow, ...S.compactRowRejected } : route.enabled ? { ...S.compactRow, ...S.compactRowActive } : S.compactRow,
|
|
3024
|
+
key: `compact:${row.id}`
|
|
3025
|
+
},
|
|
3026
|
+
// Row 1 — who this is and which proxy actually carries it. The
|
|
3027
|
+
// endpoint shown is the RESOLVED one, so a preset-bound row never
|
|
3028
|
+
// claims an address it will not use.
|
|
3029
|
+
h(
|
|
3030
|
+
"div",
|
|
3031
|
+
{ style: S.compactLine, key: `compact:${row.id}:info` },
|
|
3032
|
+
h("span", { className: import_client.default.name, style: { ...S.name, maxWidth: "160px" } }, row.name),
|
|
3033
|
+
h("span", { className: import_client.default.id, style: S.id }, row.id),
|
|
3034
|
+
h(
|
|
3035
|
+
"span",
|
|
3036
|
+
{
|
|
3037
|
+
style: {
|
|
3038
|
+
...S.compactEndpoint,
|
|
3039
|
+
...endpointState.missingProfile !== void 0 || endpointState.why !== void 0 ? { color: "var(--dsw-alias-state-error-primary, #f85149)" } : {}
|
|
3040
|
+
},
|
|
3041
|
+
title: endpointText
|
|
3042
|
+
},
|
|
3043
|
+
endpointText
|
|
3044
|
+
),
|
|
3045
|
+
lastText === void 0 ? null : h(
|
|
3046
|
+
"span",
|
|
3047
|
+
{
|
|
3048
|
+
style: {
|
|
3049
|
+
...S.compactEndpoint,
|
|
3050
|
+
...lastUse[row.id]?.ok === false ? S.inheritWarning : {}
|
|
3051
|
+
}
|
|
3052
|
+
},
|
|
3053
|
+
lastText
|
|
3054
|
+
)
|
|
3055
|
+
),
|
|
3056
|
+
// Row 2 — everything actionable, including the endpoint source. The
|
|
3057
|
+
// source picker lives here rather than in row 1 because it is a
|
|
3058
|
+
// CONTROL: putting it among the read-only facts made the row look
|
|
3059
|
+
// uniform while one element was silently editable.
|
|
3060
|
+
h(
|
|
3061
|
+
"div",
|
|
3062
|
+
{ style: S.compactActions, key: `compact:${row.id}:actions` },
|
|
3063
|
+
h(
|
|
3064
|
+
"label",
|
|
3065
|
+
{
|
|
3066
|
+
style: { display: "inline-flex", alignItems: "center", gap: "6px", flex: "none" }
|
|
3067
|
+
},
|
|
3068
|
+
h("input", {
|
|
3069
|
+
type: "checkbox",
|
|
3070
|
+
checked: route.enabled,
|
|
3071
|
+
onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
|
|
3072
|
+
}),
|
|
3073
|
+
h("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary)" } }, t.useProxy)
|
|
3074
|
+
),
|
|
3075
|
+
h(
|
|
3076
|
+
"select",
|
|
3077
|
+
{
|
|
3078
|
+
className: import_client.default.input,
|
|
3079
|
+
style: { ...S.input, flex: "0 1 190px", minWidth: "120px", padding: "3px 6px" },
|
|
3080
|
+
"aria-label": t.profileBinding,
|
|
3081
|
+
value: boundProfile.length > 0 ? boundProfile : useDefault ? DEFAULT_PROFILE_BINDING : "",
|
|
3082
|
+
onChange: (event) => bindRouteProfile(row.id)(String(event.target.value))
|
|
3083
|
+
},
|
|
3084
|
+
h("option", { key: "own", value: "" }, t.profileBindingOwn),
|
|
3085
|
+
h(
|
|
3086
|
+
"option",
|
|
3087
|
+
{ key: "default", value: DEFAULT_PROFILE_BINDING },
|
|
3088
|
+
`${t.profileBindingPrefix}${DEFAULT_PROFILE_LABEL.replace(" (Default)", "")} (${endpointAddress(defaultView)})`
|
|
3089
|
+
),
|
|
3090
|
+
...profileEntries.map(
|
|
3091
|
+
(entry) => h(
|
|
3092
|
+
"option",
|
|
3093
|
+
{ key: entry.name, value: entry.name },
|
|
3094
|
+
`${t.profileBindingPrefix}${entry.name} (${endpointAddress(entry.value)})`
|
|
3095
|
+
)
|
|
3096
|
+
),
|
|
3097
|
+
boundProfile.length > 0 && !profileEntries.some((entry) => entry.name === boundProfile) ? h(
|
|
3098
|
+
"option",
|
|
3099
|
+
{ key: `missing:${boundProfile}`, value: boundProfile },
|
|
3100
|
+
`${t.profileBindingPrefix}${boundProfile}${t.profileMissingOption}`
|
|
3101
|
+
) : null
|
|
3102
|
+
),
|
|
3103
|
+
h("span", { className: pill.className, style: pill.style }, pill.text),
|
|
3104
|
+
showTest ? h(
|
|
3105
|
+
"button",
|
|
3106
|
+
{
|
|
3107
|
+
className: import_client.default.testButton,
|
|
3108
|
+
style: { ...S.testButton, padding: "3px 8px", fontSize: "11px" },
|
|
3109
|
+
type: "button",
|
|
3110
|
+
disabled: result?.pending === true,
|
|
3111
|
+
onClick: () => runTest(row.id)
|
|
3112
|
+
},
|
|
3113
|
+
result?.pending === true ? t.testRunning : t.testButton
|
|
3114
|
+
) : null,
|
|
3115
|
+
h(
|
|
3116
|
+
"button",
|
|
3117
|
+
{
|
|
3118
|
+
className: import_client.default.testButton,
|
|
3119
|
+
style: { ...S.testButton, padding: "3px 8px", fontSize: "11px" },
|
|
3120
|
+
type: "button",
|
|
3121
|
+
onClick: () => setExpandedCards((curr) => ({ ...curr, [row.id]: !curr[row.id] }))
|
|
3122
|
+
},
|
|
3123
|
+
isExpanded ? t.collapseDetails : t.expandDetails
|
|
3124
|
+
)
|
|
3125
|
+
),
|
|
3126
|
+
// Row 3 — the self-check outcome, on its OWN line.
|
|
3127
|
+
//
|
|
3128
|
+
// Kept out of the control line so a running or failing check can never
|
|
3129
|
+
// shift the picker or the buttons under the cursor, and so a long
|
|
3130
|
+
// failure message has the full width instead of being squeezed
|
|
3131
|
+
// between two controls. It appears only when there is something to
|
|
3132
|
+
// report: an always-present empty row is just extra height.
|
|
3133
|
+
result === void 0 || result.ok === void 0 ? null : h(
|
|
3134
|
+
"div",
|
|
3135
|
+
{ style: S.compactResultLine, key: `compact:${row.id}:result` },
|
|
3136
|
+
result.pending === true ? h("span", { style: { color: "var(--dsw-alias-label-secondary)" } }, t.testRunning) : result.ok === true ? [
|
|
3137
|
+
h(
|
|
3138
|
+
"span",
|
|
3139
|
+
{ key: "lat", style: { color: latencyColor(result.ms), fontWeight: "500" } },
|
|
3140
|
+
`\u2713 ${result.ms ?? "\u2014"} ms`
|
|
3141
|
+
),
|
|
3142
|
+
typeof result.status === "number" ? h("span", { key: "status" }, `HTTP ${result.status}`) : null,
|
|
3143
|
+
result.ip === void 0 ? null : h(
|
|
3144
|
+
"span",
|
|
3145
|
+
{ key: "ip", style: { display: "inline-flex", alignItems: "center", gap: "3px" } },
|
|
3146
|
+
`${t.exitIpLabel}${result.ip}`,
|
|
3147
|
+
copyButton(`${row.id}:compact:ip`, String(result.ip), t.copy)
|
|
3148
|
+
),
|
|
3149
|
+
result.direct === true ? h("span", { key: "direct" }, t.directLabel) : null,
|
|
3150
|
+
result.cached === true ? h("span", { key: "cached" }, t.cachedLabel) : null
|
|
3151
|
+
] : [
|
|
3152
|
+
h(
|
|
3153
|
+
"span",
|
|
3154
|
+
{
|
|
3155
|
+
key: "err",
|
|
3156
|
+
style: { ...S.testFailure, display: "inline-flex", alignItems: "center", gap: "3px" }
|
|
3157
|
+
},
|
|
3158
|
+
`\u2717 ${result.error}`,
|
|
3159
|
+
copyButton(`${row.id}:compact:err`, String(result.error), t.copy)
|
|
3160
|
+
),
|
|
3161
|
+
typeof result.status === "number" ? h("span", { key: "status" }, `HTTP ${result.status}`) : null
|
|
3162
|
+
]
|
|
3163
|
+
)
|
|
3164
|
+
);
|
|
3165
|
+
if (viewMode === "compact") {
|
|
3166
|
+
return isExpanded ? h("div", { key: `compact-wrap:${row.id}` }, compactRowElement, cardSection) : compactRowElement;
|
|
3167
|
+
}
|
|
3168
|
+
return cardSection;
|
|
1591
3169
|
});
|
|
1592
3170
|
return h(
|
|
1593
3171
|
"div",
|
|
@@ -1625,44 +3203,42 @@ function renderPage(ctx) {
|
|
|
1625
3203
|
t.close
|
|
1626
3204
|
)
|
|
1627
3205
|
),
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
catalogueNotice,
|
|
1631
|
-
filterRow,
|
|
1632
|
-
...body,
|
|
3206
|
+
// Page-level settings, as one group: they apply to the whole page, not to
|
|
3207
|
+
// one provider.
|
|
1633
3208
|
h(
|
|
1634
|
-
"
|
|
1635
|
-
{
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
},
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
3209
|
+
"div",
|
|
3210
|
+
{ style: S.group, key: "page-settings" },
|
|
3211
|
+
h("h3", { style: S.groupTitle }, t.sectionSettings),
|
|
3212
|
+
defaultBlock,
|
|
3213
|
+
profileManager,
|
|
3214
|
+
globalBlock,
|
|
3215
|
+
backupBlock
|
|
3216
|
+
),
|
|
3217
|
+
// The provider list, as its own group: heading, toolbar, then the cards.
|
|
3218
|
+
h(
|
|
3219
|
+
"div",
|
|
3220
|
+
{ style: S.group, key: "provider-list" },
|
|
3221
|
+
h("h3", { style: S.groupTitle }, t.sectionProviders),
|
|
3222
|
+
catalogueNotice,
|
|
3223
|
+
listToolbar,
|
|
3224
|
+
...body
|
|
3225
|
+
),
|
|
3226
|
+
panel({
|
|
3227
|
+
key: "diag",
|
|
3228
|
+
summary: problemCount === 0 ? t.diagnostics : `${t.diagnostics} (${problemCount})`,
|
|
3229
|
+
children: [
|
|
3230
|
+
h(
|
|
3231
|
+
"ul",
|
|
3232
|
+
{ className: import_client.default.diagList, style: { margin: "6px 0 0", paddingLeft: "18px" } },
|
|
3233
|
+
...problemCount === 0 && diagnostics.length === 0 && savedNote === void 0 ? [h("li", { key: "none" }, t.diagPending)] : [
|
|
3234
|
+
...problemLines,
|
|
3235
|
+
...catalogueMismatchLine === void 0 ? [] : [catalogueMismatchLine],
|
|
3236
|
+
...savedNote === void 0 ? [] : [h("li", { key: "saved" }, savedNote)],
|
|
3237
|
+
...diagnostics.map((line, index) => h("li", { key: `d${index}` }, line))
|
|
3238
|
+
]
|
|
3239
|
+
)
|
|
3240
|
+
]
|
|
3241
|
+
})
|
|
1666
3242
|
);
|
|
1667
3243
|
}
|
|
1668
3244
|
var EN = {
|
|
@@ -1679,6 +3255,10 @@ var EN = {
|
|
|
1679
3255
|
defaultMissing: "The default proxy configuration is incomplete, so this provider is not proxied: ",
|
|
1680
3256
|
filterLabel: "Filter",
|
|
1681
3257
|
filterPlaceholder: "Filter by name or id\u2026",
|
|
3258
|
+
filterStatusAll: "All",
|
|
3259
|
+
filterStatusProxied: "Proxied",
|
|
3260
|
+
filterStatusDirect: "Direct",
|
|
3261
|
+
filterStatusRejected: "Rejected",
|
|
1682
3262
|
noMatch: "No provider matches the filter.",
|
|
1683
3263
|
setAsDefault: "Set as the default proxy",
|
|
1684
3264
|
applyDefaultAll: "Use the default proxy for all providers",
|
|
@@ -1690,7 +3270,7 @@ var EN = {
|
|
|
1690
3270
|
showPassword: "Show",
|
|
1691
3271
|
hidePassword: "Hide",
|
|
1692
3272
|
confirmPromoteToDefault: "The default proxy is already in use by one or more providers. Replace it with this card's endpoint?",
|
|
1693
|
-
hostHint: "
|
|
3273
|
+
hostHint: "IP or domain only; paste full URL supported",
|
|
1694
3274
|
passwordHint: "Sent to the SOCKS5 proxy for authentication; stored with this setting.",
|
|
1695
3275
|
passwordHintProxy: "Sent to the proxy as an authentication password; stored with this setting.",
|
|
1696
3276
|
hitCountPrefix: "",
|
|
@@ -1703,6 +3283,12 @@ var EN = {
|
|
|
1703
3283
|
minutesAgo: " min ago",
|
|
1704
3284
|
hoursAgo: " h ago",
|
|
1705
3285
|
daysAgo: " d ago",
|
|
3286
|
+
viewCard: "Card view",
|
|
3287
|
+
viewCompact: "Compact view",
|
|
3288
|
+
expandDetails: "Edit",
|
|
3289
|
+
collapseDetails: "Collapse",
|
|
3290
|
+
batchTest: "Test active",
|
|
3291
|
+
batchTesting: "Testing",
|
|
1706
3292
|
testButton: "Test",
|
|
1707
3293
|
testRunning: "Testing\u2026",
|
|
1708
3294
|
testDisabledNote: "Not enabled \u2014 test only: ",
|
|
@@ -1744,7 +3330,52 @@ var EN = {
|
|
|
1744
3330
|
catalogueMismatch: "Saved but not listed by the Host catalogue: ",
|
|
1745
3331
|
catalogueMismatchSuffix: ". Their proxy settings are kept.",
|
|
1746
3332
|
saveFailedPrefix: "Save failed: ",
|
|
1747
|
-
close: "Close"
|
|
3333
|
+
close: "Close",
|
|
3334
|
+
copy: "Copy",
|
|
3335
|
+
copied: "Copied",
|
|
3336
|
+
profileManagerTitle: "Proxy presets",
|
|
3337
|
+
profileManagerLead: 'Named endpoints you can reuse across providers. The built-in "Default preset" is the default proxy block above; edit it there.',
|
|
3338
|
+
profileAdd: "Add preset",
|
|
3339
|
+
profileRename: "Rename",
|
|
3340
|
+
profileDelete: "Delete",
|
|
3341
|
+
profileNameLabel: "Preset name",
|
|
3342
|
+
profileNamePlaceholder: "New preset name, e.g. HK-Proxy",
|
|
3343
|
+
profileNamePrompt: "Name for the new proxy preset:",
|
|
3344
|
+
profileRenamePrompt: "New name for this proxy preset:",
|
|
3345
|
+
profileRenameConfirm: "Save name",
|
|
3346
|
+
cancel: "Cancel",
|
|
3347
|
+
profileNameTaken: "A preset with that name already exists.",
|
|
3348
|
+
profileDeleteConfirm: "Delete this proxy preset?",
|
|
3349
|
+
profileDeleteInUse: "These providers are bound to this preset and will stop being proxied until they are rebound:\n",
|
|
3350
|
+
profileUsedBy: "Used by: ",
|
|
3351
|
+
profileBinding: "Endpoint",
|
|
3352
|
+
profileBindingOwn: "Custom (this card)",
|
|
3353
|
+
profileBindingPrefix: "Preset: ",
|
|
3354
|
+
inheritingProfile: "Using preset ",
|
|
3355
|
+
profileMissing: 'The bound preset "',
|
|
3356
|
+
profileMissingSuffix: '" no longer exists, so this provider is not proxied.',
|
|
3357
|
+
profileMissingOption: " (missing)",
|
|
3358
|
+
profileUnusable: 'The bound preset "',
|
|
3359
|
+
profileUnusableSuffix: '" is not usable, so this provider is not proxied: ',
|
|
3360
|
+
quickImport: "Quick import\u2026",
|
|
3361
|
+
quickImportEnv: "From environment: ",
|
|
3362
|
+
quickImportEnvNone: "From environment (none detected)",
|
|
3363
|
+
quickImportEnvCredentials: "The environment variable also carried credentials; enter them below.",
|
|
3364
|
+
loadHostTemplate: "Load official upstream hosts: ",
|
|
3365
|
+
backupExport: "Back up configuration",
|
|
3366
|
+
backupImport: "Restore configuration",
|
|
3367
|
+
backupIncludePasswords: "Include proxy passwords in the backup?\n\nOK = include, Cancel = leave every password empty.",
|
|
3368
|
+
backupWithPasswords: "Exported backup (passwords included \u2014 store it safely)",
|
|
3369
|
+
backupWithoutPasswords: "Exported backup (passwords omitted)",
|
|
3370
|
+
backupConfirm: "Restore this backup? The current configuration will be replaced.\n\nProviders: ",
|
|
3371
|
+
backupConfirmProviders: ", presets: ",
|
|
3372
|
+
backupConfirmProfiles: " entry/entries.",
|
|
3373
|
+
backupRestored: "Restored from backup, providers: ",
|
|
3374
|
+
backupTitle: "Backup and restore",
|
|
3375
|
+
backupLead: "Save the whole proxy configuration to a JSON file, or load one back.",
|
|
3376
|
+
sectionSettings: "Page settings",
|
|
3377
|
+
sectionProviders: "Providers",
|
|
3378
|
+
enableLabel: "Enabled"
|
|
1748
3379
|
};
|
|
1749
3380
|
var ZH = {
|
|
1750
3381
|
lead: "\u8BA9\u6307\u5B9A\u7684 provider \u8D70\u4F60\u81EA\u5DF1\u7684\u6B63\u5411\u4EE3\u7406\uFF0C\u5176\u5B83 provider \u4FDD\u6301\u76F4\u8FDE\u3002\u6307\u5411\u672C\u673A\u6216\u5185\u7F51\u5730\u5740\uFF08localhost\u3001127.x\u3001\u79C1\u6709\u7F51\u6BB5\uFF09\u7684\u8BF7\u6C42\u9ED8\u8BA4\u76F4\u8FDE\uFF0C\u9876\u5C42 localDirect \u8BBE\u4E3A false \u65F6\u6062\u590D\u8D70\u4EE3\u7406\u2014\u2014\u6B63\u5411\u4EE3\u7406\u901A\u5E38\u591F\u4E0D\u5230\u4F60\u81EA\u5DF1\u7684\u673A\u5668\u3002",
|
|
@@ -1760,6 +3391,10 @@ var ZH = {
|
|
|
1760
3391
|
defaultMissing: "\u9876\u5C42\u9ED8\u8BA4\u4EE3\u7406\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF0C\u8BE5 provider \u4E0D\u4F1A\u8D70\u4EE3\u7406\uFF1A",
|
|
1761
3392
|
filterLabel: "\u8FC7\u6EE4",
|
|
1762
3393
|
filterPlaceholder: "\u6309\u540D\u79F0\u6216 id \u8FC7\u6EE4\u2026",
|
|
3394
|
+
filterStatusAll: "\u5168\u90E8",
|
|
3395
|
+
filterStatusProxied: "\u8D70\u4EE3\u7406",
|
|
3396
|
+
filterStatusDirect: "\u76F4\u8FDE",
|
|
3397
|
+
filterStatusRejected: "\u914D\u7F6E\u88AB\u62D2",
|
|
1763
3398
|
noMatch: "\u6CA1\u6709\u5339\u914D\u7684 provider\u3002",
|
|
1764
3399
|
setAsDefault: "\u8BBE\u4E3A\u9ED8\u8BA4\u4EE3\u7406",
|
|
1765
3400
|
applyDefaultAll: "\u6240\u6709 provider \u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406",
|
|
@@ -1771,7 +3406,7 @@ var ZH = {
|
|
|
1771
3406
|
showPassword: "\u663E\u793A",
|
|
1772
3407
|
hidePassword: "\u9690\u85CF",
|
|
1773
3408
|
confirmPromoteToDefault: "\u5DF2\u6709 provider \u6B63\u5728\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406\u3002\u8981\u7528\u8FD9\u5F20\u5361\u7247\u7684\u7AEF\u70B9\u8986\u76D6\u5B83\u5417\uFF1F",
|
|
1774
|
-
hostHint: "\
|
|
3409
|
+
hostHint: "\u4EC5\u9650 IP \u6216\u57DF\u540D\uFF0C\u652F\u6301\u7C98\u8D34\u5B8C\u6574\u4EE3\u7406 URL",
|
|
1775
3410
|
passwordHint: "\u4EC5\u4F5C\u4E3A SOCKS5 \u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
|
|
1776
3411
|
passwordHintProxy: "\u4F5C\u4E3A\u4EE3\u7406\u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
|
|
1777
3412
|
hitCountPrefix: "\u5DF2\u4EE3\u7406 ",
|
|
@@ -1784,6 +3419,12 @@ var ZH = {
|
|
|
1784
3419
|
minutesAgo: " \u5206\u949F\u524D",
|
|
1785
3420
|
hoursAgo: " \u5C0F\u65F6\u524D",
|
|
1786
3421
|
daysAgo: " \u5929\u524D",
|
|
3422
|
+
viewCard: "\u5361\u7247\u89C6\u56FE",
|
|
3423
|
+
viewCompact: "\u7D27\u51D1\u89C6\u56FE",
|
|
3424
|
+
expandDetails: "\u7F16\u8F91",
|
|
3425
|
+
collapseDetails: "\u6536\u8D77",
|
|
3426
|
+
batchTest: "\u6D4B\u8BD5\u5DF2\u542F\u7528",
|
|
3427
|
+
batchTesting: "\u6B63\u5728\u6D4B\u8BD5",
|
|
1787
3428
|
testButton: "\u6D4B\u8BD5",
|
|
1788
3429
|
testRunning: "\u6D4B\u8BD5\u4E2D\u2026",
|
|
1789
3430
|
testDisabledNote: "\u672A\u542F\u7528\uFF0C\u4EC5\u6D4B\u8BD5\uFF1A",
|
|
@@ -1825,7 +3466,52 @@ var ZH = {
|
|
|
1825
3466
|
catalogueMismatch: "\u5DF2\u4FDD\u5B58\u4F46\u672A\u51FA\u73B0\u5728 Host \u76EE\u5F55\u4E2D\u7684 provider\uFF1A",
|
|
1826
3467
|
catalogueMismatchSuffix: "\u3002\u4EE3\u7406\u914D\u7F6E\u4ECD\u4F1A\u4FDD\u7559\u3002",
|
|
1827
3468
|
saveFailedPrefix: "\u4FDD\u5B58\u5931\u8D25\uFF1A",
|
|
1828
|
-
close: "\u5173\u95ED"
|
|
3469
|
+
close: "\u5173\u95ED",
|
|
3470
|
+
copy: "\u590D\u5236",
|
|
3471
|
+
copied: "\u5DF2\u590D\u5236",
|
|
3472
|
+
profileManagerTitle: "\u4EE3\u7406\u9884\u8BBE",
|
|
3473
|
+
profileManagerLead: "\u53EF\u88AB\u591A\u4E2A provider \u590D\u7528\u7684\u547D\u540D\u7AEF\u70B9\u3002\u5185\u7F6E\u7684\u300C\u9ED8\u8BA4\u9884\u8BBE\u300D\u5C31\u662F\u4E0A\u65B9\u300C\u9ED8\u8BA4\u4EE3\u7406\u300D\u533A\u5757\uFF0C\u76F4\u63A5\u5728\u90A3\u91CC\u7F16\u8F91\u3002",
|
|
3474
|
+
profileAdd: "\u6DFB\u52A0\u9884\u8BBE",
|
|
3475
|
+
profileRename: "\u91CD\u547D\u540D",
|
|
3476
|
+
profileDelete: "\u5220\u9664",
|
|
3477
|
+
profileNameLabel: "\u9884\u8BBE\u540D\u79F0",
|
|
3478
|
+
profileNamePlaceholder: "\u65B0\u9884\u8BBE\u540D\u79F0\uFF0C\u4F8B\u5982 HK-Proxy",
|
|
3479
|
+
profileNamePrompt: "\u65B0\u4EE3\u7406\u9884\u8BBE\u7684\u540D\u79F0\uFF1A",
|
|
3480
|
+
profileRenamePrompt: "\u8BE5\u4EE3\u7406\u9884\u8BBE\u7684\u65B0\u540D\u79F0\uFF1A",
|
|
3481
|
+
profileRenameConfirm: "\u4FDD\u5B58\u540D\u79F0",
|
|
3482
|
+
cancel: "\u53D6\u6D88",
|
|
3483
|
+
profileNameTaken: "\u5DF2\u5B58\u5728\u540C\u540D\u9884\u8BBE\u3002",
|
|
3484
|
+
profileDeleteConfirm: "\u786E\u8BA4\u5220\u9664\u8BE5\u4EE3\u7406\u9884\u8BBE\uFF1F",
|
|
3485
|
+
profileDeleteInUse: "\u4EE5\u4E0B provider \u7ED1\u5B9A\u5728\u8BE5\u9884\u8BBE\u4E0A\uFF0C\u5220\u9664\u540E\u5C06\u4E0D\u518D\u8D70\u4EE3\u7406\uFF0C\u76F4\u5230\u91CD\u65B0\u7ED1\u5B9A\uFF1A\n",
|
|
3486
|
+
profileUsedBy: "\u88AB\u4EE5\u4E0B provider \u4F7F\u7528\uFF1A",
|
|
3487
|
+
profileBinding: "\u7AEF\u70B9\u6765\u6E90",
|
|
3488
|
+
profileBindingOwn: "\u72EC\u7ACB\u81EA\u5B9A\u4E49\u914D\u7F6E",
|
|
3489
|
+
profileBindingPrefix: "\u9884\u8BBE\uFF1A",
|
|
3490
|
+
inheritingProfile: "\u4F7F\u7528\u9884\u8BBE ",
|
|
3491
|
+
profileMissing: "\u7ED1\u5B9A\u7684\u9884\u8BBE\u300C",
|
|
3492
|
+
profileMissingSuffix: "\u300D\u5DF2\u4E0D\u5B58\u5728\uFF0C\u8BE5 provider \u4E0D\u4F1A\u8D70\u4EE3\u7406\u3002",
|
|
3493
|
+
profileMissingOption: "\uFF08\u5DF2\u7F3A\u5931\uFF09",
|
|
3494
|
+
profileUnusable: "\u7ED1\u5B9A\u7684\u9884\u8BBE\u300C",
|
|
3495
|
+
profileUnusableSuffix: "\u300D\u4E0D\u53EF\u7528\uFF0C\u8BE5 provider \u4E0D\u4F1A\u8D70\u4EE3\u7406\uFF1A",
|
|
3496
|
+
quickImport: "\u5E38\u7528\u5BA2\u6237\u7AEF\u2026",
|
|
3497
|
+
quickImportEnv: "\u4ECE\u73AF\u5883\u53D8\u91CF\uFF1A",
|
|
3498
|
+
quickImportEnvNone: "\u4ECE\u73AF\u5883\u53D8\u91CF\uFF08\u672A\u68C0\u6D4B\u5230\uFF09",
|
|
3499
|
+
quickImportEnvCredentials: "\u8BE5\u73AF\u5883\u53D8\u91CF\u8FD8\u5E26\u4E86\u51ED\u636E\uFF0C\u8BF7\u5728\u4E0B\u65B9\u8865\u586B\u3002",
|
|
3500
|
+
loadHostTemplate: "\u8F7D\u5165\u5B98\u65B9\u4E0A\u6E38\u57DF\u540D\uFF1A",
|
|
3501
|
+
backupExport: "\u5907\u4EFD\u914D\u7F6E",
|
|
3502
|
+
backupImport: "\u6062\u590D\u914D\u7F6E",
|
|
3503
|
+
backupIncludePasswords: "\u5907\u4EFD\u4E2D\u662F\u5426\u5305\u542B\u4EE3\u7406\u5BC6\u7801\uFF1F\n\n\u786E\u5B9A = \u5305\u542B\uFF1B\u53D6\u6D88 = \u6240\u6709\u5BC6\u7801\u5B57\u6BB5\u7559\u7A7A\u3002",
|
|
3504
|
+
backupWithPasswords: "\u5DF2\u5BFC\u51FA\u5907\u4EFD\uFF08\u5305\u542B\u5BC6\u7801\uFF0C\u8BF7\u59A5\u5584\u4FDD\u7BA1\uFF09",
|
|
3505
|
+
backupWithoutPasswords: "\u5DF2\u5BFC\u51FA\u5907\u4EFD\uFF08\u672A\u5305\u542B\u5BC6\u7801\uFF09",
|
|
3506
|
+
backupConfirm: "\u786E\u8BA4\u7528\u8BE5\u5907\u4EFD\u8986\u76D6\u5F53\u524D\u914D\u7F6E\uFF1F\n\nprovider\uFF1A",
|
|
3507
|
+
backupConfirmProviders: "\uFF0C\u9884\u8BBE\uFF1A",
|
|
3508
|
+
backupConfirmProfiles: " \u4E2A\u3002",
|
|
3509
|
+
backupRestored: "\u5DF2\u4ECE\u5907\u4EFD\u6062\u590D\uFF0Cprovider\uFF1A",
|
|
3510
|
+
backupTitle: "\u5907\u4EFD\u4E0E\u6062\u590D",
|
|
3511
|
+
backupLead: "\u628A\u6574\u4EFD\u4EE3\u7406\u914D\u7F6E\u5B58\u6210 JSON \u6587\u4EF6\uFF0C\u6216\u4ECE\u6587\u4EF6\u6062\u590D\u3002",
|
|
3512
|
+
sectionSettings: "\u9875\u9762\u8BBE\u7F6E",
|
|
3513
|
+
sectionProviders: "Provider \u5217\u8868",
|
|
3514
|
+
enableLabel: "\u542F\u7528"
|
|
1829
3515
|
};
|
|
1830
3516
|
function stringsFor(ctx) {
|
|
1831
3517
|
const language = localeOf(ctx);
|