dsh-provider-proxy 0.8.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 +79 -5
- package/lib/client.js +1532 -185
- 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.\r\n Tokens only: no literal colours, matching the shared --dsw-* system. */\r\n\r\n.1c0df77c_root {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 12px;\r\n max-width: 640px;\r\n}\r\n\r\n.5d248416_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.1710aef8_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.b4a56347_card,\r\n.5d17f1d6_cardActive,\r\n.0b12be1b_cardRejected {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 10px;\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}\r\n\r\n.5d17f1d6_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.0b12be1b_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.4dc8a19a_defaultCard {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 10px;\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.593693ac_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.0c3d7976_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.ba5490f7_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.fc223200_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.b54fcb50_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.2d0e393c_pillOn,\r\n.ed98388d_pillLive,\r\n.07959fb6_pillOff,\r\n.23916369_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.2d0e393c_pillOn {\r\n background: var(--dsw-alias-state-success-primary);\r\n color: var(--dsw-alias-bg-base);\r\n}\r\n\r\n.ed98388d_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.07959fb6_pillOff {\r\n color: var(--dsw-alias-state-idle-primary);\r\n}\r\n\r\n.23916369_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.44052e5b_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.44052e5b_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.7826b254_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.fd3de42a_label {\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.9c1dd04c_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.0cb12e78_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.0cb12e78_input:hover:not(:disabled) {\r\n border-color: var(--dsw-alias-border-l3) !important;\r\n}\r\n\r\n.0cb12e78_input:focus {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n}\r\n\r\n.0cb12e78_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.e7ed8e99_passwordToggle {\r\n transition: background 120ms ease, color 120ms ease;\r\n}\r\n\r\n.e7ed8e99_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.e7ed8e99_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.3ea31502_testButton {\r\n transition: border-color 120ms ease, background 120ms ease;\r\n}\r\n\r\n.3ea31502_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.8093bbdb_diag {\r\n margin-top: 2px;\r\n padding: 8px 10px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 8px;\r\n font-size: 11px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.bb056060_diagSummary {\r\n cursor: pointer;\r\n color: var(--dsw-alias-state-warn-primary);\r\n}\r\n\r\n.643d5158_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.cf7604fc_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.69584a20_chip {\r\n transition: border-color 120ms ease, background 120ms ease, color 120ms ease;\r\n}\r\n\r\n.69584a20_chip: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 color: var(--dsw-alias-label-primary) !important;\r\n}\r\n\r\n.9e9e3b3e_chipActive {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-brand-primary) !important;\r\n}\r\n\r\n/* ---- v0.8.0 N11 additions: form grid ---- */\r\n\r\n.07283bc3_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.31635992_formGridFull {\r\n grid-column: 1 / -1;\r\n}\r\n\r\n@media (max-width: 520px) {\r\n .07283bc3_formGrid {\r\n grid-template-columns: 1fr !important;\r\n }\r\n .31635992_formGridFull {\r\n grid-column: auto !important;\r\n }\r\n}\r\n\r\n/* ---- v0.8.0 N10 additions: compact view ---- */\r\n\r\n.10f37453_compactRow,\r\n.b8d0ab91_compactRowActive,\r\n.518f0c81_compactRowRejected {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 10px;\r\n padding: 10px 14px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 8px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n margin-bottom: 8px;\r\n}\r\n\r\n.b8d0ab91_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.518f0c81_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.4b86d627_copyButton {\r\n transition: background 120ms ease, color 120ms ease;\r\n}\r\n\r\n.4b86d627_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';
|
|
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",
|
|
@@ -672,6 +1004,21 @@ var S = {
|
|
|
672
1004
|
fontSize: "12px",
|
|
673
1005
|
minWidth: "0"
|
|
674
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"
|
|
1021
|
+
},
|
|
675
1022
|
label: { color: "var(--dsw-alias-label-secondary)" },
|
|
676
1023
|
input: {
|
|
677
1024
|
boxSizing: "border-box",
|
|
@@ -687,10 +1034,9 @@ var S = {
|
|
|
687
1034
|
fontSize: "12px"
|
|
688
1035
|
},
|
|
689
1036
|
diag: {
|
|
690
|
-
|
|
691
|
-
padding: "8px 10px",
|
|
1037
|
+
padding: "10px 14px",
|
|
692
1038
|
border: "1px solid var(--dsw-alias-border-l1)",
|
|
693
|
-
borderRadius: "
|
|
1039
|
+
borderRadius: "10px",
|
|
694
1040
|
fontSize: "11px",
|
|
695
1041
|
color: "var(--dsw-alias-label-secondary)"
|
|
696
1042
|
},
|
|
@@ -699,6 +1045,42 @@ var S = {
|
|
|
699
1045
|
fontSize: "11px",
|
|
700
1046
|
color: "var(--dsw-alias-state-warn-primary, #d29922)"
|
|
701
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
|
+
},
|
|
702
1084
|
chipRow: {
|
|
703
1085
|
display: "flex",
|
|
704
1086
|
alignItems: "center",
|
|
@@ -711,30 +1093,84 @@ var S = {
|
|
|
711
1093
|
alignItems: "center",
|
|
712
1094
|
padding: "3px 10px",
|
|
713
1095
|
borderRadius: "999px",
|
|
714
|
-
|
|
715
|
-
|
|
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",
|
|
716
1116
|
color: "var(--dsw-alias-label-secondary)",
|
|
717
1117
|
fontSize: "11px",
|
|
718
1118
|
fontWeight: "500",
|
|
719
1119
|
cursor: "pointer",
|
|
720
1120
|
userSelect: "none"
|
|
721
1121
|
},
|
|
1122
|
+
/** The one selected chip: a solid fill, unmistakable at a glance. */
|
|
722
1123
|
chipActive: {
|
|
723
1124
|
borderColor: "var(--dsw-alias-brand-primary)",
|
|
724
|
-
background: "var(--dsw-alias-
|
|
725
|
-
color: "var(--dsw-alias-
|
|
1125
|
+
background: "var(--dsw-alias-brand-primary)",
|
|
1126
|
+
color: "var(--dsw-alias-bg-base)",
|
|
726
1127
|
fontWeight: "600"
|
|
727
1128
|
},
|
|
728
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: {
|
|
729
1142
|
display: "flex",
|
|
730
1143
|
alignItems: "center",
|
|
731
|
-
|
|
732
|
-
gap: "
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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"
|
|
738
1174
|
},
|
|
739
1175
|
compactRowActive: {
|
|
740
1176
|
borderColor: "var(--dsw-alias-state-success-primary)",
|
|
@@ -808,6 +1244,7 @@ function renderPage(ctx) {
|
|
|
808
1244
|
hits: {},
|
|
809
1245
|
problems: {},
|
|
810
1246
|
lastUse: {},
|
|
1247
|
+
environmentProxy: null,
|
|
811
1248
|
error: void 0,
|
|
812
1249
|
pending: true
|
|
813
1250
|
};
|
|
@@ -818,6 +1255,7 @@ function renderPage(ctx) {
|
|
|
818
1255
|
hits: catalog.hits,
|
|
819
1256
|
problems: catalog.problems,
|
|
820
1257
|
lastUse: catalog.lastUse,
|
|
1258
|
+
environmentProxy: catalog.environmentProxy ?? null,
|
|
821
1259
|
error: catalog.error,
|
|
822
1260
|
pending: false
|
|
823
1261
|
});
|
|
@@ -854,6 +1292,24 @@ function renderPage(ctx) {
|
|
|
854
1292
|
const [defaultDraft, setDefaultDraft] = (React.useState ?? ((v) => [v, () => {
|
|
855
1293
|
}]))({});
|
|
856
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
|
+
);
|
|
857
1313
|
const [passwordVisible, setPasswordVisible] = (React.useState ?? ((v) => [v, () => {
|
|
858
1314
|
}]))({});
|
|
859
1315
|
const [defaultPasswordVisible, setDefaultPasswordVisible] = (React.useState ?? ((v) => [v, () => {
|
|
@@ -921,9 +1377,13 @@ function renderPage(ctx) {
|
|
|
921
1377
|
const [filter, setFilter] = (React.useState ?? ((v) => [v, () => {
|
|
922
1378
|
}]))("");
|
|
923
1379
|
const [statusFilter, setStatusFilter] = (React.useState ?? ((v) => [v, () => {
|
|
924
|
-
}]))(
|
|
1380
|
+
}]))(
|
|
1381
|
+
() => readViewPrefs().statusFilter ?? "all"
|
|
1382
|
+
);
|
|
925
1383
|
const [viewMode, setViewMode] = (React.useState ?? ((v) => [v, () => {
|
|
926
|
-
}]))(
|
|
1384
|
+
}]))(
|
|
1385
|
+
() => readViewPrefs().viewMode ?? "card"
|
|
1386
|
+
);
|
|
927
1387
|
const [expandedCards, setExpandedCards] = (React.useState ?? ((v) => [v, () => {
|
|
928
1388
|
}]))({});
|
|
929
1389
|
const [batchProgress, setBatchProgress] = (React.useState ?? ((v) => [v, () => {
|
|
@@ -1211,6 +1671,50 @@ function renderPage(ctx) {
|
|
|
1211
1671
|
event.preventDefault?.();
|
|
1212
1672
|
apply2(parts);
|
|
1213
1673
|
};
|
|
1674
|
+
const quickImportSelect = (key, apply2) => {
|
|
1675
|
+
const detected = state.environmentProxy ?? null;
|
|
1676
|
+
const select = h(
|
|
1677
|
+
"select",
|
|
1678
|
+
{
|
|
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
|
+
};
|
|
1214
1718
|
const field = (label, control, hint, hintError, fullWidth) => h(
|
|
1215
1719
|
"label",
|
|
1216
1720
|
{
|
|
@@ -1238,12 +1742,206 @@ function renderPage(ctx) {
|
|
|
1238
1742
|
hint
|
|
1239
1743
|
)
|
|
1240
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
|
+
);
|
|
1241
1767
|
const storedDefault = snapshot.value?.defaultProxy ?? {};
|
|
1242
1768
|
const defaultView = view({ ...storedDefault, ...defaultDraft });
|
|
1243
1769
|
const defaultWhy = defaultProxyProblem(defaultView);
|
|
1244
1770
|
const defaultHostWhy = defaultView.host.trim().length === 0 ? void 0 : proxyHostProblem(defaultView.host);
|
|
1245
1771
|
const defaultComplete = defaultWhy === void 0;
|
|
1246
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
|
+
};
|
|
1247
1945
|
const defaultBlock = h(
|
|
1248
1946
|
"div",
|
|
1249
1947
|
{ className: import_client.default.defaultCard, style: S.defaultCard, key: "default-proxy" },
|
|
@@ -1265,6 +1963,11 @@ function renderPage(ctx) {
|
|
|
1265
1963
|
)
|
|
1266
1964
|
),
|
|
1267
1965
|
h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, t.defaultLead),
|
|
1966
|
+
h(
|
|
1967
|
+
"div",
|
|
1968
|
+
{ style: S.testRow, key: "default:quick-import" },
|
|
1969
|
+
quickImportSelect("default:quick-import", applyDefaultUrl)
|
|
1970
|
+
),
|
|
1268
1971
|
h(
|
|
1269
1972
|
"div",
|
|
1270
1973
|
{ className: import_client.default.formGrid, style: S.formGrid, key: "default:form-grid" },
|
|
@@ -1278,7 +1981,7 @@ function renderPage(ctx) {
|
|
|
1278
1981
|
value: defaultView.protocol,
|
|
1279
1982
|
onChange: (event) => bindDefault("protocol")(event.target.value)
|
|
1280
1983
|
},
|
|
1281
|
-
...
|
|
1984
|
+
...PROTOCOLS2.map(
|
|
1282
1985
|
(protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
|
|
1283
1986
|
)
|
|
1284
1987
|
)
|
|
@@ -1363,79 +2066,446 @@ function renderPage(ctx) {
|
|
|
1363
2066
|
)
|
|
1364
2067
|
);
|
|
1365
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
|
+
});
|
|
1366
2340
|
const storedLocalDirect = snapshot.value?.localDirect !== false;
|
|
1367
2341
|
const globalTestUrl = typeof globalDraft.testUrl === "string" ? globalDraft.testUrl : storedTestUrl;
|
|
1368
2342
|
const globalLocalDirect = typeof globalDraft.localDirect === "boolean" ? globalDraft.localDirect : storedLocalDirect;
|
|
1369
2343
|
const globalTestUrlHint = testUrlHint(globalTestUrl, t);
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
...
|
|
1376
|
-
|
|
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
|
|
1377
2353
|
},
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
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" },
|
|
1382
2406
|
h(
|
|
1383
|
-
"
|
|
1384
|
-
{
|
|
1385
|
-
h("span", {
|
|
1386
|
-
h("
|
|
1387
|
-
className: import_client.default.input,
|
|
1388
|
-
style: S.input,
|
|
1389
|
-
value: globalTestUrl,
|
|
1390
|
-
onChange: (event) => bindGlobal("testUrl")(event.target.value)
|
|
1391
|
-
}),
|
|
1392
|
-
globalTestUrlHint === void 0 ? h("span", { className: import_client.default.hint, style: S.hint }, t.testUrlHint) : h(
|
|
1393
|
-
"span",
|
|
1394
|
-
{
|
|
1395
|
-
className: import_client.default.hint,
|
|
1396
|
-
style: {
|
|
1397
|
-
...S.hint,
|
|
1398
|
-
color: "var(--dsw-alias-state-error-primary, #f85149)"
|
|
1399
|
-
}
|
|
1400
|
-
},
|
|
1401
|
-
globalTestUrlHint
|
|
1402
|
-
)
|
|
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)
|
|
1403
2411
|
),
|
|
1404
2412
|
h(
|
|
1405
2413
|
"div",
|
|
1406
|
-
{ style: S.testRow },
|
|
2414
|
+
{ style: { ...S.testRow, flex: "none" } },
|
|
1407
2415
|
h(
|
|
1408
2416
|
"button",
|
|
1409
2417
|
{
|
|
1410
2418
|
className: import_client.default.testButton,
|
|
1411
2419
|
style: S.testButton,
|
|
1412
2420
|
type: "button",
|
|
1413
|
-
onClick:
|
|
2421
|
+
onClick: exportBackup
|
|
2422
|
+
},
|
|
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"
|
|
1414
2431
|
},
|
|
1415
|
-
t.
|
|
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
|
+
})
|
|
1416
2443
|
)
|
|
1417
|
-
),
|
|
1418
|
-
h(
|
|
1419
|
-
"label",
|
|
1420
|
-
{ className: import_client.default.toggle, style: S.toggle },
|
|
1421
|
-
h("input", {
|
|
1422
|
-
type: "checkbox",
|
|
1423
|
-
checked: globalLocalDirect,
|
|
1424
|
-
onChange: (event) => bindGlobal("localDirect")(event.target.checked === true)
|
|
1425
|
-
}),
|
|
1426
|
-
h("span", null, t.localDirect)
|
|
1427
|
-
),
|
|
1428
|
-
h(
|
|
1429
|
-
"p",
|
|
1430
|
-
{ className: import_client.default.hint, style: { ...S.hint, margin: "2px 0 0" } },
|
|
1431
|
-
globalLocalDirect ? t.localDirectOnHint : t.localDirectOffHint
|
|
1432
2444
|
)
|
|
1433
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
|
+
});
|
|
1434
2504
|
const routeStateOf = (row) => {
|
|
1435
2505
|
const route = view({ ...configured[row.id], ...draft[row.id] });
|
|
1436
2506
|
const recorded = problems[row.id];
|
|
1437
|
-
const
|
|
1438
|
-
const problemText = recorded !== void 0 ? recorded :
|
|
2507
|
+
const state2 = cardEndpoint(route);
|
|
2508
|
+
const problemText = recorded !== void 0 ? recorded : route.enabled ? cardProblemText(state2) : void 0;
|
|
1439
2509
|
if (route.enabled && problemText !== void 0) return "rejected";
|
|
1440
2510
|
if (route.enabled) return "proxied";
|
|
1441
2511
|
return "direct";
|
|
@@ -1449,40 +2519,50 @@ function renderPage(ctx) {
|
|
|
1449
2519
|
if (statusFilter === "all") return true;
|
|
1450
2520
|
return routeStateOf(row) === statusFilter;
|
|
1451
2521
|
});
|
|
1452
|
-
const
|
|
2522
|
+
const listToolbar = rows.length === 0 ? null : h(
|
|
1453
2523
|
"div",
|
|
1454
|
-
{
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
{
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
"
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
+
}
|
|
1470
2553
|
},
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
label
|
|
2554
|
+
label
|
|
2555
|
+
)
|
|
1474
2556
|
)
|
|
1475
|
-
)
|
|
1476
|
-
);
|
|
1477
|
-
const filterRow = rows.length === 0 ? null : field(
|
|
1478
|
-
t.filterLabel,
|
|
2557
|
+
),
|
|
1479
2558
|
h(
|
|
1480
2559
|
"div",
|
|
1481
|
-
{
|
|
2560
|
+
{ className: import_client.default.toolbarRow, style: S.toolbarRow, key: "list-actions" },
|
|
1482
2561
|
h("input", {
|
|
1483
2562
|
className: import_client.default.input,
|
|
1484
|
-
style: S.input,
|
|
2563
|
+
style: { ...S.input, flex: "1 1 180px", minWidth: "0" },
|
|
1485
2564
|
placeholder: t.filterPlaceholder,
|
|
2565
|
+
"aria-label": t.filterLabel,
|
|
1486
2566
|
value: filter,
|
|
1487
2567
|
onChange: (event) => setFilter(event.target.value)
|
|
1488
2568
|
}),
|
|
@@ -1503,7 +2583,11 @@ function renderPage(ctx) {
|
|
|
1503
2583
|
className: import_client.default.testButton,
|
|
1504
2584
|
style: { ...S.testButton, flex: "none", whiteSpace: "nowrap" },
|
|
1505
2585
|
type: "button",
|
|
1506
|
-
onClick: () => setViewMode((m) =>
|
|
2586
|
+
onClick: () => setViewMode((m) => {
|
|
2587
|
+
const next = m === "card" ? "compact" : "card";
|
|
2588
|
+
writeViewPrefs({ viewMode: next });
|
|
2589
|
+
return next;
|
|
2590
|
+
})
|
|
1507
2591
|
},
|
|
1508
2592
|
viewMode === "card" ? t.viewCompact : t.viewCard
|
|
1509
2593
|
)
|
|
@@ -1540,14 +2624,16 @@ function renderPage(ctx) {
|
|
|
1540
2624
|
const result = tests[row.id];
|
|
1541
2625
|
const onlyDead = deadRuleEntries(route.onlyHosts);
|
|
1542
2626
|
const bypassDead = deadRuleEntries(route.bypassHosts);
|
|
2627
|
+
const hostTemplate = providerHostTemplate(row.id);
|
|
1543
2628
|
const useDefault = route.useDefault === true;
|
|
1544
|
-
const
|
|
2629
|
+
const boundProfile = route.profileId.trim();
|
|
2630
|
+
const endpointState = cardEndpoint(route);
|
|
2631
|
+
const endpointStored = endpointState.complete;
|
|
1545
2632
|
const showTest = route.enabled || endpointStored;
|
|
1546
2633
|
const problemText = (() => {
|
|
1547
2634
|
const recorded = problems[row.id];
|
|
1548
2635
|
if (recorded !== void 0) return recorded;
|
|
1549
|
-
|
|
1550
|
-
return route.enabled ? defaultProxyProblem(route) : void 0;
|
|
2636
|
+
return route.enabled ? cardProblemText(endpointState) : void 0;
|
|
1551
2637
|
})();
|
|
1552
2638
|
const rejected = route.enabled && problemText !== void 0;
|
|
1553
2639
|
const endpointFields = [
|
|
@@ -1561,7 +2647,7 @@ function renderPage(ctx) {
|
|
|
1561
2647
|
value: route.protocol,
|
|
1562
2648
|
onChange: (event) => bind(row.id, "protocol")(event.target.value)
|
|
1563
2649
|
},
|
|
1564
|
-
...
|
|
2650
|
+
...PROTOCOLS2.map(
|
|
1565
2651
|
(protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
|
|
1566
2652
|
)
|
|
1567
2653
|
),
|
|
@@ -1669,36 +2755,94 @@ function renderPage(ctx) {
|
|
|
1669
2755
|
),
|
|
1670
2756
|
h("span", { className: pill.className, style: pill.style }, pill.text)
|
|
1671
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.
|
|
1672
2762
|
h(
|
|
1673
|
-
"
|
|
1674
|
-
{ className: import_client.default.
|
|
1675
|
-
h(
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
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
|
+
)
|
|
1695
2828
|
),
|
|
1696
|
-
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(
|
|
1697
2834
|
"p",
|
|
1698
2835
|
{ key: `${row.id}:inherit`, style: S.inheritNote },
|
|
1699
2836
|
`${t.inheritingDefault}${defaultOrigin}`
|
|
1700
2837
|
) : null,
|
|
1701
|
-
...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
|
+
],
|
|
1702
2846
|
// The self-check is offered whenever there is something to check: an
|
|
1703
2847
|
// enabled route (the Host's table carries it, and a rejected one still
|
|
1704
2848
|
// reports WHY), or a disabled one whose endpoint is already complete —
|
|
@@ -1794,7 +2938,7 @@ function renderPage(ctx) {
|
|
|
1794
2938
|
// Promote this card's endpoint to the shared default. Offered only
|
|
1795
2939
|
// when the card carries its OWN endpoint: an inheriting card would
|
|
1796
2940
|
// just be copying the default onto itself.
|
|
1797
|
-
endpointStored && !useDefault ? h(
|
|
2941
|
+
endpointStored && !useDefault && boundProfile.length === 0 ? h(
|
|
1798
2942
|
"button",
|
|
1799
2943
|
{
|
|
1800
2944
|
className: import_client.default.testButton,
|
|
@@ -1822,6 +2966,26 @@ function renderPage(ctx) {
|
|
|
1822
2966
|
onlyDead.length === 0 ? t.onlyHostsHint : `${t.ruleDeadHint}${onlyDead.join("\u3001")}`,
|
|
1823
2967
|
onlyDead.length > 0
|
|
1824
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
|
+
),
|
|
1825
2989
|
field(
|
|
1826
2990
|
t.bypassHosts,
|
|
1827
2991
|
h("input", {
|
|
@@ -1851,6 +3015,7 @@ function renderPage(ctx) {
|
|
|
1851
3015
|
)
|
|
1852
3016
|
);
|
|
1853
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"}`;
|
|
1854
3019
|
const compactRowElement = h(
|
|
1855
3020
|
"section",
|
|
1856
3021
|
{
|
|
@@ -1858,22 +3023,83 @@ function renderPage(ctx) {
|
|
|
1858
3023
|
style: rejected ? { ...S.compactRow, ...S.compactRowRejected } : route.enabled ? { ...S.compactRow, ...S.compactRowActive } : S.compactRow,
|
|
1859
3024
|
key: `compact:${row.id}`
|
|
1860
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.
|
|
1861
3029
|
h(
|
|
1862
3030
|
"div",
|
|
1863
|
-
{ style:
|
|
1864
|
-
h("
|
|
1865
|
-
type: "checkbox",
|
|
1866
|
-
checked: route.enabled,
|
|
1867
|
-
onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
|
|
1868
|
-
}),
|
|
1869
|
-
h("span", { className: import_client.default.name, style: { ...S.name, maxWidth: "140px" } }, row.name),
|
|
3031
|
+
{ style: S.compactLine, key: `compact:${row.id}:info` },
|
|
3032
|
+
h("span", { className: import_client.default.name, style: { ...S.name, maxWidth: "160px" } }, row.name),
|
|
1870
3033
|
h("span", { className: import_client.default.id, style: S.id }, row.id),
|
|
1871
|
-
|
|
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
|
+
)
|
|
1872
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.
|
|
1873
3060
|
h(
|
|
1874
3061
|
"div",
|
|
1875
|
-
{ style:
|
|
1876
|
-
|
|
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
|
+
),
|
|
1877
3103
|
h("span", { className: pill.className, style: pill.style }, pill.text),
|
|
1878
3104
|
showTest ? h(
|
|
1879
3105
|
"button",
|
|
@@ -1896,6 +3122,44 @@ function renderPage(ctx) {
|
|
|
1896
3122
|
},
|
|
1897
3123
|
isExpanded ? t.collapseDetails : t.expandDetails
|
|
1898
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
|
+
]
|
|
1899
3163
|
)
|
|
1900
3164
|
);
|
|
1901
3165
|
if (viewMode === "compact") {
|
|
@@ -1939,45 +3203,42 @@ function renderPage(ctx) {
|
|
|
1939
3203
|
t.close
|
|
1940
3204
|
)
|
|
1941
3205
|
),
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
catalogueNotice,
|
|
1945
|
-
chipRow,
|
|
1946
|
-
filterRow,
|
|
1947
|
-
...body,
|
|
3206
|
+
// Page-level settings, as one group: they apply to the whole page, not to
|
|
3207
|
+
// one provider.
|
|
1948
3208
|
h(
|
|
1949
|
-
"
|
|
1950
|
-
{
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
},
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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
|
+
})
|
|
1981
3242
|
);
|
|
1982
3243
|
}
|
|
1983
3244
|
var EN = {
|
|
@@ -2071,7 +3332,50 @@ var EN = {
|
|
|
2071
3332
|
saveFailedPrefix: "Save failed: ",
|
|
2072
3333
|
close: "Close",
|
|
2073
3334
|
copy: "Copy",
|
|
2074
|
-
copied: "Copied"
|
|
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"
|
|
2075
3379
|
};
|
|
2076
3380
|
var ZH = {
|
|
2077
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",
|
|
@@ -2164,7 +3468,50 @@ var ZH = {
|
|
|
2164
3468
|
saveFailedPrefix: "\u4FDD\u5B58\u5931\u8D25\uFF1A",
|
|
2165
3469
|
close: "\u5173\u95ED",
|
|
2166
3470
|
copy: "\u590D\u5236",
|
|
2167
|
-
copied: "\u5DF2\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"
|
|
2168
3515
|
};
|
|
2169
3516
|
function stringsFor(ctx) {
|
|
2170
3517
|
const language = localeOf(ctx);
|