dsh-pocket 1.16.1 → 2.0.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/client/client.js CHANGED
@@ -1365,6 +1365,11 @@ var zh2 = {
1365
1365
  "updatedRestartDetail": "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u91CD\u542F dsh web \u751F\u6548",
1366
1366
  "updateFailed": "\u274C \u5931\u8D25\uFF1A{err}\uFF08\u624B\u52A8\u66F4\u65B0\uFF1Adsh plugin --profile web update dsh-pocket --latest -w\uFF09",
1367
1367
  "versionRange": "\u5F53\u524D v{cur} \u2192 \u6700\u65B0 v{latest}",
1368
+ "wanAccess": "\u516C\u7F51\u8BBF\u95EE",
1369
+ "pinLabel": "\u8BBF\u95EE\u5BC6\u7801",
1370
+ "modeLabel": "\u5730\u5740\u6A21\u5F0F",
1371
+ "advAddress": "\u9AD8\u7EA7 \xB7 \u624B\u52A8\u9009\u5730\u5740",
1372
+ "wanOffHint": "\u5F00\u542F\u540E\u53EF\u4ECE\u4EFB\u4F55\u7F51\u7EDC\u8BBF\u95EE\uFF08\u6BCF\u6B21\u5F00\u542F\u9700\u786E\u8BA4\u514D\u8D23\u58F0\u660E\uFF09",
1368
1373
  "lanTitle": "\u{1F4F6} \u5C40\u57DF\u7F51\uFF08\u540C\u4E00 WiFi\uFF09",
1369
1374
  "lanHint": "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00",
1370
1375
  "lanAccess": "\u5C40\u57DF\u7F51\u8BBF\u95EE",
@@ -1447,6 +1452,11 @@ var en2 = {
1447
1452
  "updatedRestartDetail": "\u2705 Updated \u2014 restart dsh web to apply",
1448
1453
  "updateFailed": "\u274C Failed: {err} (manual update: dsh plugin --profile web update dsh-pocket --latest -w)",
1449
1454
  "versionRange": "Current v{cur} \u2192 latest v{latest}",
1455
+ "wanAccess": "Public access",
1456
+ "pinLabel": "Access PIN",
1457
+ "modeLabel": "Address mode",
1458
+ "advAddress": "Advanced \xB7 Pick address",
1459
+ "wanOffHint": "Reachable from any network once enabled (a disclaimer is confirmed on each enable)",
1450
1460
  "lanTitle": "\u{1F4F6} LAN (same Wi-Fi)",
1451
1461
  "lanHint": "Scan to open once your phone is on the same Wi-Fi",
1452
1462
  "lanAccess": "LAN access",
@@ -1781,6 +1791,31 @@ function PocketSettingsTab({ rpcCall, t }) {
1781
1791
  background: active ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-bg-layer-1,#fff)",
1782
1792
  color: active ? "var(--dsw-alias-label-primary-foreground, #fff)" : "var(--dsw-alias-label-primary,inherit)"
1783
1793
  });
1794
+ const Switch = (on, onClick) => (0, import_react2.createElement)("button", {
1795
+ role: "switch",
1796
+ "aria-checked": !!on,
1797
+ style: { flexShrink: 0, width: 40, height: 22, borderRadius: 11, border: "none", padding: 0, position: "relative", cursor: "pointer", font: "inherit", background: on ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-border-l2,#d1d5db)" },
1798
+ onClick
1799
+ }, (0, import_react2.createElement)("span", { style: { position: "absolute", top: 2, left: on ? 20 : 2, width: 18, height: 18, borderRadius: "50%", background: "#fff" } }));
1800
+ const qrArea = (src, url, hint) => (0, import_react2.createElement)(
1801
+ "div",
1802
+ { style: { background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", borderRadius: 10, padding: "10px 12px", textAlign: "center", margin: "10px 0" } },
1803
+ (0, import_react2.createElement)("img", { src, alt: "QR", style: styles.qr }),
1804
+ (0, import_react2.createElement)("div", { style: styles.code }, url),
1805
+ (0, import_react2.createElement)("div", { style: styles.muted }, hint)
1806
+ );
1807
+ const row = (label, control, extra) => (0, import_react2.createElement)(
1808
+ "div",
1809
+ { style: { borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", paddingTop: 9, marginTop: 9 } },
1810
+ (0, import_react2.createElement)(
1811
+ "div",
1812
+ { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
1813
+ (0, import_react2.createElement)("span", { style: { fontSize: 13 } }, label),
1814
+ control
1815
+ ),
1816
+ extra ?? null
1817
+ );
1818
+ const [advOpen, setAdvOpen] = (0, import_react2.useState)(false);
1784
1819
  return (0, import_react2.createElement)(
1785
1820
  "div",
1786
1821
  { style: styles.card },
@@ -1839,181 +1874,174 @@ function PocketSettingsTab({ rpcCall, t }) {
1839
1874
  updateInfo.updating ? fmt(t, "updatingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.restarting ? fmt(t, "restartingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.result === "ok" ? updateInfo.autoRestart ? t("updatedAutoDetail") : t("updatedRestartDetail") : updateInfo.result === "fail" ? fmt(t, "updateFailed", { err: updateInfo.output || t("unknownError") }) : fmt(t, "versionRange", { cur: updateInfo.current, latest: updateInfo.latest })
1840
1875
  )
1841
1876
  ) : null,
1842
- // 局域网
1877
+ // 局域网:标题行自带总开关 → 二维码+地址 → 设置行(访问密码 / 高级·手动选地址)
1843
1878
  (0, import_react2.createElement)(
1844
1879
  "div",
1845
1880
  { style: styles.block },
1846
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("lanTitle")),
1847
- // 局域网访问总开关:关闭后扫码/链接直接失效(公网不受影响)
1848
1881
  (0, import_react2.createElement)(
1849
1882
  "div",
1850
- { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 8 } },
1851
- (0, import_react2.createElement)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } }, t("lanAccess")),
1852
- (0, import_react2.createElement)("button", {
1853
- style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, fontWeight: status?.lanEnabled !== false ? 600 : 400, background: status?.lanEnabled !== false ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-bg-layer-1,#fff)", color: status?.lanEnabled !== false ? "var(--dsw-alias-label-primary-foreground, #fff)" : "var(--dsw-alias-label-primary,inherit)" },
1854
- onClick: () => requestLanToggle(true)
1855
- }, t("on")),
1856
- (0, import_react2.createElement)("button", {
1857
- style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, fontWeight: status?.lanEnabled === false ? 600 : 400, background: status?.lanEnabled === false ? "var(--dsw-alias-state-error-primary,#dc2626)" : "var(--dsw-alias-bg-layer-1,#fff)", color: status?.lanEnabled === false ? "#fff" : "var(--dsw-alias-label-primary,inherit)" },
1858
- onClick: () => requestLanToggle(false)
1859
- }, t("off"))
1883
+ { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
1884
+ (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("lanAccess")),
1885
+ Switch(status?.lanEnabled !== false, () => requestLanToggle(status?.lanEnabled === false))
1860
1886
  ),
1861
1887
  status?.lanEnabled === false ? (0, import_react2.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("lanDisabledHint")) : lanUrl ? (0, import_react2.createElement)(
1862
1888
  "div",
1863
1889
  null,
1864
- (0, import_react2.createElement)("img", { src: status.lanQr, alt: "LAN QR", style: styles.qr }),
1865
- (0, import_react2.createElement)("div", { style: styles.code }, lanUrl),
1866
- (0, import_react2.createElement)("div", { style: styles.muted }, t("lanHint")),
1867
- (0, import_react2.createElement)(
1868
- "label",
1869
- { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 10, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
1870
- t("lanAddress"),
1871
- (0, import_react2.createElement)(
1872
- "select",
1873
- {
1874
- value: status?.lanIpOverride || "",
1875
- onChange: (e) => setLanAddress(e.target.value),
1876
- style: { font: "inherit", height: 30, padding: "0 8px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", background: "var(--dsw-alias-bg-layer-1,#fff)", color: "var(--dsw-alias-label-primary,inherit)" }
1877
- },
1878
- (0, import_react2.createElement)("option", { value: "" }, t("lanAddressAuto")),
1879
- (status?.lanCandidates || []).map((ip) => (0, import_react2.createElement)("option", { key: ip, value: ip }, ip))
1890
+ qrArea(status.lanQr, lanUrl, t("lanHint")),
1891
+ // 访问密码行:开关 + 值(关闭时提示直连)
1892
+ row(
1893
+ t("lanPin"),
1894
+ Switch(status?.lanAuthEnabled !== false, () => setLanAuth(status?.lanAuthEnabled === false)),
1895
+ status?.lanAuthEnabled === false ? (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("lanPinOff")) : customPin?.which === "lan" ? customPinRow("lan") : (0, import_react2.createElement)(
1896
+ "div",
1897
+ { style: { marginTop: 6, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
1898
+ (0, import_react2.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.lanToken),
1899
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: refreshLanPin }, t("refresh")),
1900
+ customBtn("lan"),
1901
+ status?.lanPinCustom ? (0, import_react2.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("pinCustomHint")) : null
1880
1902
  )
1881
1903
  ),
1882
- // 访问密码开关(issue #24):默认开启;关闭后扫码直连(仅同一局域网设备可访问)
1883
- (0, import_react2.createElement)(
1884
- "div",
1885
- { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 8 } },
1886
- (0, import_react2.createElement)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } }, t("lanPin")),
1887
- (0, import_react2.createElement)("button", {
1888
- style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, fontWeight: status?.lanAuthEnabled !== false ? 600 : 400, background: status?.lanAuthEnabled !== false ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-bg-layer-1,#fff)", color: status?.lanAuthEnabled !== false ? "var(--dsw-alias-label-primary-foreground, #fff)" : "var(--dsw-alias-label-primary,inherit)" },
1889
- onClick: () => setLanAuth(true)
1890
- }, t("on")),
1891
- (0, import_react2.createElement)("button", {
1892
- style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, fontWeight: status?.lanAuthEnabled === false ? 600 : 400, background: status?.lanAuthEnabled === false ? "var(--dsw-alias-state-error-primary,#dc2626)" : "var(--dsw-alias-bg-layer-1,#fff)", color: status?.lanAuthEnabled === false ? "#fff" : "var(--dsw-alias-label-primary,inherit)" },
1893
- onClick: () => setLanAuth(false)
1894
- }, t("off"))
1895
- ),
1896
- status?.lanAuthEnabled !== false ? customPin?.which === "lan" ? customPinRow("lan") : (0, import_react2.createElement)(
1897
- "div",
1898
- { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
1899
- fmt(t, status?.lanPinCustom ? "lanPinCustomValue" : "lanPinValue", { pin: status.lanToken }),
1900
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, t("refresh")),
1901
- customBtn("lan")
1902
- ) : (0, import_react2.createElement)(
1903
- "div",
1904
- { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } },
1905
- t("lanPinOff")
1904
+ // 高级:手动选地址(默认收起)
1905
+ row(
1906
+ t("advAddress"),
1907
+ (0, import_react2.createElement)(
1908
+ "button",
1909
+ { style: { border: "none", background: "none", font: "inherit", cursor: "pointer", fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", padding: 0 }, onClick: () => setAdvOpen((v) => !v) },
1910
+ (status?.lanIpOverride || t("lanAddressAuto")) + " \u203A"
1911
+ ),
1912
+ advOpen ? (0, import_react2.createElement)(
1913
+ "div",
1914
+ { style: { marginTop: 8 } },
1915
+ (0, import_react2.createElement)(
1916
+ "label",
1917
+ { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
1918
+ t("lanAddress"),
1919
+ (0, import_react2.createElement)(
1920
+ "select",
1921
+ {
1922
+ value: status?.lanIpOverride || "",
1923
+ onChange: (e) => setLanAddress(e.target.value),
1924
+ style: { font: "inherit", height: 30, padding: "0 8px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", background: "var(--dsw-alias-bg-layer-1,#fff)", color: "var(--dsw-alias-label-primary,inherit)" }
1925
+ },
1926
+ (0, import_react2.createElement)("option", { value: "" }, t("lanAddressAuto")),
1927
+ (status?.lanCandidates || []).map((ip) => (0, import_react2.createElement)("option", { key: ip, value: ip }, ip))
1928
+ )
1929
+ )
1930
+ ) : null
1906
1931
  )
1907
1932
  ) : (0, import_react2.createElement)("div", { style: styles.muted }, t("lanStarting"))
1908
1933
  ),
1909
- // 公网
1934
+ // 公网:标题行自带 开启/关闭 → 开启后:二维码+地址、地址模式行、访问密码行
1910
1935
  (0, import_react2.createElement)(
1911
1936
  "div",
1912
1937
  { style: styles.block },
1913
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("wanTitle")),
1938
+ (0, import_react2.createElement)(
1939
+ "div",
1940
+ { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
1941
+ (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("wanAccess")),
1942
+ tunnelUrl ? (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: stopTunnel }, t("stopTunnel")) : (0, import_react2.createElement)("button", { style: { ...styles.primary, height: 28, padding: "0 14px", fontSize: 12 }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy || tunnelStarting ? t("opening") : t("enable"))
1943
+ ),
1944
+ tunnelStarting ? (0, import_react2.createElement)(
1945
+ "div",
1946
+ { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
1947
+ tunnelPhase === "downloading" ? fmt(t, "downloading", { s: elapsed(tunnelStateStarted) }) : fmt(t, "connecting", { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t("slowHint") : "" })
1948
+ ) : tunnelPhase === "error" ? (0, import_react2.createElement)(
1949
+ "div",
1950
+ { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } },
1951
+ fmt(t, "error", { detail: tunnelStateDetail || t("unknownError") })
1952
+ ) : !tunnelUrl && !isDesktop ? (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 8 } }, t("wanOffHint")) : null,
1914
1953
  tunnelUrl ? (0, import_react2.createElement)(
1915
1954
  "div",
1916
1955
  null,
1917
- (0, import_react2.createElement)("img", { src: status.tunnelQr, alt: "Tunnel QR", style: styles.qr }),
1918
- (0, import_react2.createElement)("div", { style: styles.code }, tunnelUrl),
1919
- (0, import_react2.createElement)("div", { style: styles.muted }, namedMode ? t("namedRunningHint") : t("wanHint")),
1920
- status.accessToken ? customPin?.which === "public" ? customPinRow("public") : (0, import_react2.createElement)(
1921
- "div",
1922
- { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
1923
- fmt(t, status?.publicPinCustom ? "wanPinCustom" : "wanPin", { pin: status.accessToken }),
1924
- customBtn("public"),
1925
- status?.publicPinCustom ? (0, import_react2.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("pinCustomHint")) : null,
1926
- namedMode ? (0, import_react2.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("namedSecurity")) : null
1927
- ) : null,
1928
- (0, import_react2.createElement)("button", { style: styles.btn, onClick: stopTunnel }, t("stopTunnel")),
1929
- // 公网模式(issue #66):仅在公网开启后才显示/可选
1930
- (0, import_react2.createElement)(
1931
- "div",
1932
- { style: { marginTop: 10 } },
1956
+ qrArea(status.tunnelQr, tunnelUrl, namedMode ? t("namedRunningHint") : t("wanHint")),
1957
+ // 地址模式行(随机/固定;固定域名选中或编辑时高亮)
1958
+ row(
1959
+ t("modeLabel"),
1933
1960
  (0, import_react2.createElement)(
1934
- "div",
1935
- { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
1936
- (0, import_react2.createElement)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } }, t("tunnelMode")),
1937
- (0, import_react2.createElement)("button", {
1938
- style: modeBtnStyle(!namedActive),
1939
- onClick: namedMode ? switchToQuick : tunnelCfg ? () => setTunnelCfg(null) : void 0
1940
- }, t("modeQuick")),
1941
- (0, import_react2.createElement)("button", {
1942
- style: modeBtnStyle(namedActive),
1943
- onClick: () => setTunnelCfg(tunnelCfg ? null : { hostname: tunnelModeView.hostname ?? "", token: "", err: null })
1944
- }, t("modeNamed"))
1961
+ "span",
1962
+ { style: { display: "inline-flex", gap: 6 } },
1963
+ (0, import_react2.createElement)("button", { style: modeBtnStyle(!namedActive), onClick: namedMode ? switchToQuick : tunnelCfg ? () => setTunnelCfg(null) : void 0 }, t("modeQuick")),
1964
+ (0, import_react2.createElement)("button", { style: modeBtnStyle(namedActive), onClick: () => setTunnelCfg(tunnelCfg ? null : { hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("modeNamed"))
1945
1965
  ),
1946
- // 刚保存固定域名但当前连接仍是随机域名:需关闭后重新开启才生效
1947
- namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? "") ? (0, import_react2.createElement)("div", { style: { marginTop: 4, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("namedTakeEffect")) : null,
1948
- // 固定域名:已保存的摘要 + 「修改」入口(非编辑态)
1949
- namedMode && !tunnelCfg ? (0, import_react2.createElement)(
1950
- "div",
1951
- { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
1952
- fmt(t, "namedSummary", { host: tunnelModeView.hostname || "\u2014", token: tunnelModeView.tokenSet ? t("namedTokenSet") : t("namedTokenMissing") }),
1953
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setTunnelCfg({ hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("namedEdit")),
1954
- (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 4 } }, t("namedHow")),
1955
- !tunnelModeView.tokenSet || !tunnelModeView.hostname ? (0, import_react2.createElement)("div", { style: { marginTop: 2, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("namedNeedCfg")) : null
1956
- ) : null,
1957
- // 固定域名:编辑表单(域名 + Tunnel Token,Token 留空保持不变)
1958
- tunnelCfg ? (0, import_react2.createElement)(
1966
+ (0, import_react2.createElement)(
1959
1967
  "div",
1960
- { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.6 } },
1961
- (0, import_react2.createElement)(
1968
+ { style: { marginTop: 6 } },
1969
+ // 刚保存固定域名但当前连接仍是随机域名:需关闭后重新开启才生效
1970
+ namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? "") ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("namedTakeEffect")) : null,
1971
+ // 固定域名:已保存摘要 + 修改入口(非编辑态)
1972
+ namedMode && !tunnelCfg ? (0, import_react2.createElement)(
1962
1973
  "div",
1963
- null,
1964
- t("namedHostnameLabel"),
1965
- (0, import_react2.createElement)("input", {
1966
- style: { margin: "4px 0 0 6px", padding: "4px 8px", fontSize: 13, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none", width: 200 },
1967
- placeholder: "pocket.example.com",
1968
- value: tunnelCfg.hostname ?? "",
1969
- autoFocus: true,
1970
- onChange: (e) => setTunnelCfg((c) => ({ ...c, hostname: e.target.value.trim(), err: null })),
1971
- onKeyDown: (e) => {
1972
- if (e.key === "Enter") saveNamedTunnel();
1973
- if (e.key === "Escape") setTunnelCfg(null);
1974
- }
1975
- })
1976
- ),
1977
- (0, import_react2.createElement)(
1978
- "div",
1979
- { style: { marginTop: 6 } },
1980
- t("namedTokenLabel"),
1981
- (0, import_react2.createElement)("input", {
1982
- style: { margin: "4px 0 0 6px", padding: "4px 8px", fontSize: 13, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none", width: 240, fontFamily: "ui-monospace,Menlo,monospace" },
1983
- type: "password",
1984
- value: tunnelCfg.token ?? "",
1985
- onChange: (e) => setTunnelCfg((c) => ({ ...c, token: e.target.value.trim(), err: null })),
1986
- onKeyDown: (e) => {
1987
- if (e.key === "Enter") saveNamedTunnel();
1988
- if (e.key === "Escape") setTunnelCfg(null);
1989
- }
1990
- })
1991
- ),
1992
- (0, import_react2.createElement)(
1974
+ { style: { ...styles.muted } },
1975
+ fmt(t, "namedSummary", { host: tunnelModeView.hostname || "\u2014", token: tunnelModeView.tokenSet ? t("namedTokenSet") : t("namedTokenMissing") }),
1976
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setTunnelCfg({ hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("namedEdit")),
1977
+ (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 4 } }, t("namedHow")),
1978
+ !tunnelModeView.tokenSet || !tunnelModeView.hostname ? (0, import_react2.createElement)("div", { style: { marginTop: 2, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("namedNeedCfg")) : null
1979
+ ) : null,
1980
+ // 固定域名:编辑表单(域名 + Tunnel Token,Token 留空保持不变)
1981
+ tunnelCfg ? (0, import_react2.createElement)(
1993
1982
  "div",
1994
- { style: { marginTop: 6, display: "flex", gap: 8 } },
1995
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: saveNamedTunnel }, t("save")),
1996
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setTunnelCfg(null) }, t("cancel"))
1997
- ),
1998
- (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("namedHow")),
1999
- (0, import_react2.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("namedSecurity")),
2000
- tunnelCfg.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, tunnelCfg.err) : null
2001
- ) : null
2002
- )
2003
- ) : (0, import_react2.createElement)(
2004
- "div",
2005
- null,
2006
- (0, import_react2.createElement)("button", { style: { ...styles.primary, margin: "8px 0" }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? t("opening") : t("enable")),
2007
- tunnelStarting ? (0, import_react2.createElement)(
2008
- "div",
2009
- { style: { marginTop: 4, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
2010
- tunnelPhase === "downloading" ? fmt(t, "downloading", { s: elapsed(tunnelStateStarted) }) : fmt(t, "connecting", { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t("slowHint") : "" })
2011
- ) : tunnelPhase === "error" ? (0, import_react2.createElement)(
2012
- "div",
2013
- { style: { marginTop: 4, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } },
2014
- fmt(t, "error", { detail: tunnelStateDetail || t("unknownError") })
1983
+ { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.6 } },
1984
+ (0, import_react2.createElement)(
1985
+ "div",
1986
+ null,
1987
+ t("namedHostnameLabel"),
1988
+ (0, import_react2.createElement)("input", {
1989
+ style: { margin: "4px 0 0 6px", padding: "4px 8px", fontSize: 13, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none", width: 200 },
1990
+ placeholder: "pocket.example.com",
1991
+ value: tunnelCfg.hostname ?? "",
1992
+ autoFocus: true,
1993
+ onChange: (e) => setTunnelCfg((c) => ({ ...c, hostname: e.target.value.trim(), err: null })),
1994
+ onKeyDown: (e) => {
1995
+ if (e.key === "Enter") saveNamedTunnel();
1996
+ if (e.key === "Escape") setTunnelCfg(null);
1997
+ }
1998
+ })
1999
+ ),
2000
+ (0, import_react2.createElement)(
2001
+ "div",
2002
+ { style: { marginTop: 6 } },
2003
+ t("namedTokenLabel"),
2004
+ (0, import_react2.createElement)("input", {
2005
+ style: { margin: "4px 0 0 6px", padding: "4px 8px", fontSize: 13, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none", width: 240, fontFamily: "ui-monospace,Menlo,monospace" },
2006
+ type: "password",
2007
+ value: tunnelCfg.token ?? "",
2008
+ onChange: (e) => setTunnelCfg((c) => ({ ...c, token: e.target.value.trim(), err: null })),
2009
+ onKeyDown: (e) => {
2010
+ if (e.key === "Enter") saveNamedTunnel();
2011
+ if (e.key === "Escape") setTunnelCfg(null);
2012
+ }
2013
+ })
2014
+ ),
2015
+ (0, import_react2.createElement)(
2016
+ "div",
2017
+ { style: { marginTop: 6, display: "flex", gap: 8 } },
2018
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: saveNamedTunnel }, t("save")),
2019
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setTunnelCfg(null) }, t("cancel"))
2020
+ ),
2021
+ (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("namedHow")),
2022
+ (0, import_react2.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("namedSecurity")),
2023
+ tunnelCfg.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, tunnelCfg.err) : null
2024
+ ) : null
2025
+ )
2026
+ ),
2027
+ // 访问密码行:值 + 自定义(自定义输入态整体替换)
2028
+ status.accessToken ? row(
2029
+ t("pinLabel"),
2030
+ customPin?.which === "public" ? null : (0, import_react2.createElement)(
2031
+ "span",
2032
+ { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
2033
+ (0, import_react2.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.accessToken),
2034
+ customBtn("public")
2035
+ ),
2036
+ (0, import_react2.createElement)(
2037
+ "div",
2038
+ { style: { marginTop: 6 } },
2039
+ customPin?.which === "public" ? customPinRow("public") : null,
2040
+ status?.publicPinCustom ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("pinCustomHint")) : null,
2041
+ namedMode ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("namedSecurity")) : null
2042
+ )
2015
2043
  ) : null
2016
- )
2044
+ ) : null
2017
2045
  ),
2018
2046
  error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null,
2019
2047
  // 局域网访问开关确认弹框(关闭/打开时弹窗提醒)
package/client/index.jsx CHANGED
@@ -315,6 +315,23 @@ function PocketSettingsTab({ rpcCall, t }) {
315
315
  background: active ? 'var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))' : 'var(--dsw-alias-bg-layer-1,#fff)',
316
316
  color: active ? 'var(--dsw-alias-label-primary-foreground, #fff)' : 'var(--dsw-alias-label-primary,inherit)',
317
317
  });
318
+ // iOS 风格小开关(重排后统一用:局域网总开关 / 局域网密码开关)
319
+ const Switch = (on, onClick) => h('button', {
320
+ role: 'switch', 'aria-checked': !!on,
321
+ style: { flexShrink: 0, width: 40, height: 22, borderRadius: 11, border: 'none', padding: 0, position: 'relative', cursor: 'pointer', font: 'inherit', background: on ? 'var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))' : 'var(--dsw-alias-border-l2,#d1d5db)' },
322
+ onClick,
323
+ }, h('span', { style: { position: 'absolute', top: 2, left: on ? 20 : 2, width: 18, height: 18, borderRadius: '50%', background: '#fff' } }));
324
+ // 卡片内主内容:二维码 + 地址 + 提示
325
+ const qrArea = (src, url, hint) => h('div', { style: { background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', borderRadius: 10, padding: '10px 12px', textAlign: 'center', margin: '10px 0' } },
326
+ h('img', { src, alt: 'QR', style: styles.qr }),
327
+ h('div', { style: styles.code }, url),
328
+ h('div', { style: styles.muted }, hint));
329
+ // 设置行:上分隔线,内部第一行 = 左标签 + 右操作;extra 作为第二段渲染
330
+ const row = (label, control, extra) => h('div', { style: { borderTop: '1px solid var(--dsw-alias-border-l2,#e5e7eb)', paddingTop: 9, marginTop: 9 } },
331
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
332
+ h('span', { style: { fontSize: 13 } }, label), control), extra ?? null);
333
+ // 高级(手动选地址)展开态
334
+ const [advOpen, setAdvOpen] = useState(false);
318
335
 
319
336
  return h('div', { style: styles.card },
320
337
  h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
@@ -369,151 +386,134 @@ function PocketSettingsTab({ rpcCall, t }) {
369
386
  : fmt(t, 'versionRange', { cur: updateInfo.current, latest: updateInfo.latest })),
370
387
  ) : null,
371
388
 
372
- // 局域网
389
+ // 局域网:标题行自带总开关 → 二维码+地址 → 设置行(访问密码 / 高级·手动选地址)
373
390
  h('div', { style: styles.block },
374
- h('div', { style: { fontWeight: 600, fontSize: 13 } }, t('lanTitle')),
375
- // 局域网访问总开关:关闭后扫码/链接直接失效(公网不受影响)
376
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 } },
377
- h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } }, t('lanAccess')),
378
- h('button', {
379
- style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, fontWeight: status?.lanEnabled !== false ? 600 : 400, background: status?.lanEnabled !== false ? 'var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))' : 'var(--dsw-alias-bg-layer-1,#fff)', color: status?.lanEnabled !== false ? 'var(--dsw-alias-label-primary-foreground, #fff)' : 'var(--dsw-alias-label-primary,inherit)' },
380
- onClick: () => requestLanToggle(true),
381
- }, t('on')),
382
- h('button', {
383
- style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, fontWeight: status?.lanEnabled === false ? 600 : 400, background: status?.lanEnabled === false ? 'var(--dsw-alias-state-error-primary,#dc2626)' : 'var(--dsw-alias-bg-layer-1,#fff)', color: status?.lanEnabled === false ? '#fff' : 'var(--dsw-alias-label-primary,inherit)' },
384
- onClick: () => requestLanToggle(false),
385
- }, t('off')),
391
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
392
+ h('span', { style: { fontWeight: 600, fontSize: 13 } }, t('lanAccess')),
393
+ Switch(status?.lanEnabled !== false, () => requestLanToggle(status?.lanEnabled === false)),
386
394
  ),
387
395
  status?.lanEnabled === false
388
396
  ? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#b45309)', lineHeight: 1.5 } }, t('lanDisabledHint'))
389
397
  : (lanUrl
390
398
  ? h('div', null,
391
- h('img', { src: status.lanQr, alt: 'LAN QR', style: styles.qr }),
392
- h('div', { style: styles.code }, lanUrl),
393
- h('div', { style: styles.muted }, t('lanHint')),
394
- h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 10, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } },
395
- t('lanAddress'),
396
- h('select', {
397
- value: status?.lanIpOverride || '',
398
- onChange: (e) => setLanAddress(e.target.value),
399
- style: { font: 'inherit', height: 30, padding: '0 8px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', background: 'var(--dsw-alias-bg-layer-1,#fff)', color: 'var(--dsw-alias-label-primary,inherit)' },
400
- },
401
- h('option', { value: '' }, t('lanAddressAuto')),
402
- (status?.lanCandidates || []).map((ip) => h('option', { key: ip, value: ip }, ip)),
403
- ),
404
- ),
405
- // 访问密码开关(issue #24):默认开启;关闭后扫码直连(仅同一局域网设备可访问)
406
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 } },
407
- h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } }, t('lanPin')),
408
- h('button', {
409
- style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, fontWeight: status?.lanAuthEnabled !== false ? 600 : 400, background: status?.lanAuthEnabled !== false ? 'var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))' : 'var(--dsw-alias-bg-layer-1,#fff)', color: status?.lanAuthEnabled !== false ? 'var(--dsw-alias-label-primary-foreground, #fff)' : 'var(--dsw-alias-label-primary,inherit)' },
410
- onClick: () => setLanAuth(true),
411
- }, t('on')),
412
- h('button', {
413
- style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, fontWeight: status?.lanAuthEnabled === false ? 600 : 400, background: status?.lanAuthEnabled === false ? 'var(--dsw-alias-state-error-primary,#dc2626)' : 'var(--dsw-alias-bg-layer-1,#fff)', color: status?.lanAuthEnabled === false ? '#fff' : 'var(--dsw-alias-label-primary,inherit)' },
414
- onClick: () => setLanAuth(false),
415
- }, t('off')),
416
- ),
417
- status?.lanAuthEnabled !== false
418
- ? (customPin?.which === 'lan'
419
- ? customPinRow('lan')
420
- : h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
421
- fmt(t, status?.lanPinCustom ? 'lanPinCustomValue' : 'lanPinValue', { pin: status.lanToken }),
422
- h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, t('refresh')),
423
- customBtn('lan'),
424
- ))
425
- : h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#b45309)', lineHeight: 1.5 } },
426
- t('lanPinOff')),
399
+ qrArea(status.lanQr, lanUrl, t('lanHint')),
400
+ // 访问密码行:开关 + 值(关闭时提示直连)
401
+ row(t('lanPin'), Switch(status?.lanAuthEnabled !== false, () => setLanAuth(status?.lanAuthEnabled === false)),
402
+ status?.lanAuthEnabled === false
403
+ ? h('div', { style: { ...styles.muted, marginTop: 6 } }, t('lanPinOff'))
404
+ : (customPin?.which === 'lan'
405
+ ? customPinRow('lan')
406
+ : h('div', { style: { marginTop: 6, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
407
+ h('span', { style: { fontFamily: 'ui-monospace,Menlo,monospace', fontSize: 13, letterSpacing: 1 } }, status.lanToken),
408
+ h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12 }, onClick: refreshLanPin }, t('refresh')),
409
+ customBtn('lan'),
410
+ status?.lanPinCustom ? h('span', { style: { fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b45309)' } }, t('pinCustomHint')) : null,
411
+ ))),
412
+ // 高级:手动选地址(默认收起)
413
+ row(t('advAddress'),
414
+ h('button', { style: { border: 'none', background: 'none', font: 'inherit', cursor: 'pointer', fontSize: 12, color: 'var(--dsw-alias-label-tertiary,#8b93a1)', padding: 0 }, onClick: () => setAdvOpen((v) => !v) },
415
+ (status?.lanIpOverride || t('lanAddressAuto')) + ' '),
416
+ advOpen ? h('div', { style: { marginTop: 8 } },
417
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } },
418
+ t('lanAddress'),
419
+ h('select', {
420
+ value: status?.lanIpOverride || '',
421
+ onChange: (e) => setLanAddress(e.target.value),
422
+ style: { font: 'inherit', height: 30, padding: '0 8px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', background: 'var(--dsw-alias-bg-layer-1,#fff)', color: 'var(--dsw-alias-label-primary,inherit)' },
423
+ },
424
+ h('option', { value: '' }, t('lanAddressAuto')),
425
+ (status?.lanCandidates || []).map((ip) => h('option', { key: ip, value: ip }, ip)),
426
+ ),
427
+ ),
428
+ ) : null),
427
429
  )
428
430
  : h('div', { style: styles.muted }, t('lanStarting'))),
429
431
  ),
430
432
 
431
- // 公网
433
+ // 公网:标题行自带 开启/关闭 → 开启后:二维码+地址、地址模式行、访问密码行
432
434
  h('div', { style: styles.block },
433
- h('div', { style: { fontWeight: 600, fontSize: 13 } }, t('wanTitle')),
435
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
436
+ h('span', { style: { fontWeight: 600, fontSize: 13 } }, t('wanAccess')),
437
+ tunnelUrl
438
+ ? h('button', { style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' }, onClick: stopTunnel }, t('stopTunnel'))
439
+ : h('button', { style: { ...styles.primary, height: 28, padding: '0 14px', fontSize: 12 }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy || tunnelStarting ? t('opening') : t('enable')),
440
+ ),
441
+ tunnelStarting
442
+ ? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } },
443
+ tunnelPhase === 'downloading'
444
+ ? fmt(t, 'downloading', { s: elapsed(tunnelStateStarted) })
445
+ : fmt(t, 'connecting', { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t('slowHint') : '' }))
446
+ : tunnelPhase === 'error'
447
+ ? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' } },
448
+ fmt(t, 'error', { detail: tunnelStateDetail || t('unknownError') }))
449
+ : (!tunnelUrl && !isDesktop ? h('div', { style: { ...styles.muted, marginTop: 8 } }, t('wanOffHint')) : null),
434
450
  tunnelUrl
435
451
  ? h('div', null,
436
- h('img', { src: status.tunnelQr, alt: 'Tunnel QR', style: styles.qr }),
437
- h('div', { style: styles.code }, tunnelUrl),
438
- h('div', { style: styles.muted }, namedMode ? t('namedRunningHint') : t('wanHint')),
439
- status.accessToken
440
- ? (customPin?.which === 'public'
441
- ? customPinRow('public')
442
- : h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
443
- fmt(t, status?.publicPinCustom ? 'wanPinCustom' : 'wanPin', { pin: status.accessToken }),
444
- customBtn('public'),
445
- status?.publicPinCustom ? h('div', { style: { marginTop: 2, fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b45309)' } }, t('pinCustomHint')) : null,
446
- namedMode ? h('div', { style: { marginTop: 2, fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b45309)' } }, t('namedSecurity')) : null,
447
- ))
448
- : null,
449
- h('button', { style: styles.btn, onClick: stopTunnel }, t('stopTunnel')),
450
- // 公网模式(issue #66):仅在公网开启后才显示/可选
451
- h('div', { style: { marginTop: 10 } },
452
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
453
- h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } }, t('tunnelMode')),
454
- h('button', {
455
- style: modeBtnStyle(!namedActive),
456
- onClick: namedMode ? switchToQuick : (tunnelCfg ? () => setTunnelCfg(null) : undefined),
457
- }, t('modeQuick')),
458
- h('button', {
459
- style: modeBtnStyle(namedActive),
460
- onClick: () => setTunnelCfg(tunnelCfg ? null : { hostname: tunnelModeView.hostname ?? '', token: '', err: null }),
461
- }, t('modeNamed')),
452
+ qrArea(status.tunnelQr, tunnelUrl, namedMode ? t('namedRunningHint') : t('wanHint')),
453
+ // 地址模式行(随机/固定;固定域名选中或编辑时高亮)
454
+ row(t('modeLabel'),
455
+ h('span', { style: { display: 'inline-flex', gap: 6 } },
456
+ h('button', { style: modeBtnStyle(!namedActive), onClick: namedMode ? switchToQuick : (tunnelCfg ? () => setTunnelCfg(null) : undefined) }, t('modeQuick')),
457
+ h('button', { style: modeBtnStyle(namedActive), onClick: () => setTunnelCfg(tunnelCfg ? null : { hostname: tunnelModeView.hostname ?? '', token: '', err: null }) }, t('modeNamed')),
458
+ ),
459
+ h('div', { style: { marginTop: 6 } },
460
+ // 刚保存固定域名但当前连接仍是随机域名:需关闭后重新开启才生效
461
+ namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? '') ? h('div', { style: { ...styles.warn } }, t('namedTakeEffect')) : null,
462
+ // 固定域名:已保存摘要 + 修改入口(非编辑态)
463
+ namedMode && !tunnelCfg ? h('div', { style: { ...styles.muted } },
464
+ fmt(t, 'namedSummary', { host: tunnelModeView.hostname || '—', token: tunnelModeView.tokenSet ? t('namedTokenSet') : t('namedTokenMissing') }),
465
+ h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: () => setTunnelCfg({ hostname: tunnelModeView.hostname ?? '', token: '', err: null }) }, t('namedEdit')),
466
+ h('div', { style: { ...styles.muted, marginTop: 4 } }, t('namedHow')),
467
+ !tunnelModeView.tokenSet || !tunnelModeView.hostname ? h('div', { style: { marginTop: 2, color: 'var(--dsw-alias-state-error-primary,#dc2626)' } }, t('namedNeedCfg')) : null,
468
+ ) : null,
469
+ // 固定域名:编辑表单(域名 + Tunnel Token,Token 留空保持不变)
470
+ tunnelCfg ? h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.6 } },
471
+ h('div', null,
472
+ t('namedHostnameLabel'),
473
+ h('input', {
474
+ style: { margin: '4px 0 0 6px', padding: '4px 8px', fontSize: 13, border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', borderRadius: 6, outline: 'none', width: 200 },
475
+ placeholder: 'pocket.example.com',
476
+ value: tunnelCfg.hostname ?? '',
477
+ autoFocus: true,
478
+ onChange: (e) => setTunnelCfg((c) => ({ ...c, hostname: e.target.value.trim(), err: null })),
479
+ onKeyDown: (e) => { if (e.key === 'Enter') saveNamedTunnel(); if (e.key === 'Escape') setTunnelCfg(null); },
480
+ }),
481
+ ),
482
+ h('div', { style: { marginTop: 6 } },
483
+ t('namedTokenLabel'),
484
+ h('input', {
485
+ style: { margin: '4px 0 0 6px', padding: '4px 8px', fontSize: 13, border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', borderRadius: 6, outline: 'none', width: 240, fontFamily: 'ui-monospace,Menlo,monospace' },
486
+ type: 'password',
487
+ value: tunnelCfg.token ?? '',
488
+ onChange: (e) => setTunnelCfg((c) => ({ ...c, token: e.target.value.trim(), err: null })),
489
+ onKeyDown: (e) => { if (e.key === 'Enter') saveNamedTunnel(); if (e.key === 'Escape') setTunnelCfg(null); },
490
+ }),
491
+ ),
492
+ h('div', { style: { marginTop: 6, display: 'flex', gap: 8 } },
493
+ h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12 }, onClick: saveNamedTunnel }, t('save')),
494
+ h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12 }, onClick: () => setTunnelCfg(null) }, t('cancel')),
495
+ ),
496
+ h('div', { style: { ...styles.muted, marginTop: 6 } }, t('namedHow')),
497
+ h('div', { style: { marginTop: 2, fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b45309)', lineHeight: 1.5 } }, t('namedSecurity')),
498
+ tunnelCfg.err ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', marginTop: 4 } }, tunnelCfg.err) : null,
499
+ ) : null,
462
500
  ),
463
- // 刚保存固定域名但当前连接仍是随机域名:需关闭后重新开启才生效
464
- namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? '') ? h('div', { style: { marginTop: 4, fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b45309)' } }, t('namedTakeEffect')) : null,
465
- // 固定域名:已保存的摘要 + 「修改」入口(非编辑态)
466
- namedMode && !tunnelCfg ? h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
467
- fmt(t, 'namedSummary', { host: tunnelModeView.hostname || '—', token: tunnelModeView.tokenSet ? t('namedTokenSet') : t('namedTokenMissing') }),
468
- h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: () => setTunnelCfg({ hostname: tunnelModeView.hostname ?? '', token: '', err: null }) }, t('namedEdit')),
469
- h('div', { style: { ...styles.muted, marginTop: 4 } }, t('namedHow')),
470
- !tunnelModeView.tokenSet || !tunnelModeView.hostname ? h('div', { style: { marginTop: 2, fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' } }, t('namedNeedCfg')) : null,
471
- ) : null,
472
- // 固定域名:编辑表单(域名 + Tunnel Token,Token 留空保持不变)
473
- tunnelCfg ? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.6 } },
474
- h('div', null,
475
- t('namedHostnameLabel'),
476
- h('input', {
477
- style: { margin: '4px 0 0 6px', padding: '4px 8px', fontSize: 13, border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', borderRadius: 6, outline: 'none', width: 200 },
478
- placeholder: 'pocket.example.com',
479
- value: tunnelCfg.hostname ?? '',
480
- autoFocus: true,
481
- onChange: (e) => setTunnelCfg((c) => ({ ...c, hostname: e.target.value.trim(), err: null })),
482
- onKeyDown: (e) => { if (e.key === 'Enter') saveNamedTunnel(); if (e.key === 'Escape') setTunnelCfg(null); },
483
- }),
484
- ),
485
- h('div', { style: { marginTop: 6 } },
486
- t('namedTokenLabel'),
487
- h('input', {
488
- style: { margin: '4px 0 0 6px', padding: '4px 8px', fontSize: 13, border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', borderRadius: 6, outline: 'none', width: 240, fontFamily: 'ui-monospace,Menlo,monospace' },
489
- type: 'password',
490
- value: tunnelCfg.token ?? '',
491
- onChange: (e) => setTunnelCfg((c) => ({ ...c, token: e.target.value.trim(), err: null })),
492
- onKeyDown: (e) => { if (e.key === 'Enter') saveNamedTunnel(); if (e.key === 'Escape') setTunnelCfg(null); },
493
- }),
494
- ),
495
- h('div', { style: { marginTop: 6, display: 'flex', gap: 8 } },
496
- h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12 }, onClick: saveNamedTunnel }, t('save')),
497
- h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12 }, onClick: () => setTunnelCfg(null) }, t('cancel')),
498
- ),
499
- h('div', { style: { ...styles.muted, marginTop: 6 } }, t('namedHow')),
500
- h('div', { style: { marginTop: 2, fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b45309)', lineHeight: 1.5 } }, t('namedSecurity')),
501
- tunnelCfg.err ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', marginTop: 4 } }, tunnelCfg.err) : null,
502
- ) : null,
503
501
  ),
502
+ // 访问密码行:值 + 自定义(自定义输入态整体替换)
503
+ status.accessToken
504
+ ? row(t('pinLabel'),
505
+ customPin?.which === 'public'
506
+ ? null
507
+ : h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 8 } },
508
+ h('span', { style: { fontFamily: 'ui-monospace,Menlo,monospace', fontSize: 13, letterSpacing: 1 } }, status.accessToken),
509
+ customBtn('public')),
510
+ h('div', { style: { marginTop: 6 } },
511
+ customPin?.which === 'public' ? customPinRow('public') : null,
512
+ status?.publicPinCustom ? h('div', { style: { ...styles.warn } }, t('pinCustomHint')) : null,
513
+ namedMode ? h('div', { style: { ...styles.warn } }, t('namedSecurity')) : null))
514
+ : null,
504
515
  )
505
- : h('div', null,
506
- h('button', { style: { ...styles.primary, margin: '8px 0' }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? t('opening') : t('enable')),
507
- tunnelStarting
508
- ? h('div', { style: { marginTop: 4, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } },
509
- tunnelPhase === 'downloading'
510
- ? fmt(t, 'downloading', { s: elapsed(tunnelStateStarted) })
511
- : fmt(t, 'connecting', { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t('slowHint') : '' }))
512
- : tunnelPhase === 'error'
513
- ? h('div', { style: { marginTop: 4, fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' } },
514
- fmt(t, 'error', { detail: tunnelStateDetail || t('unknownError') }))
515
- : null,
516
- ),
516
+ : null,
517
517
  ),
518
518
 
519
519
  error ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, marginTop: 8 } }, `❌ ${error}`) : null,
@@ -28,6 +28,11 @@ export const zh = {
28
28
  'updatedRestartDetail': '✅ 已更新,重启 dsh web 生效',
29
29
  'updateFailed': '❌ 失败:{err}(手动更新:dsh plugin --profile web update dsh-pocket --latest -w)',
30
30
  'versionRange': '当前 v{cur} → 最新 v{latest}',
31
+ 'wanAccess': '公网访问',
32
+ 'pinLabel': '访问密码',
33
+ 'modeLabel': '地址模式',
34
+ 'advAddress': '高级 · 手动选地址',
35
+ 'wanOffHint': '开启后可从任何网络访问(每次开启需确认免责声明)',
31
36
  'lanTitle': '📶 局域网(同一 WiFi)',
32
37
  'lanHint': '手机连接同一 WiFi 后扫码即可打开',
33
38
  'lanAccess': '局域网访问',
@@ -112,6 +117,11 @@ export const en = {
112
117
  'updatedRestartDetail': '✅ Updated — restart dsh web to apply',
113
118
  'updateFailed': '❌ Failed: {err} (manual update: dsh plugin --profile web update dsh-pocket --latest -w)',
114
119
  'versionRange': 'Current v{cur} → latest v{latest}',
120
+ 'wanAccess': 'Public access',
121
+ 'pinLabel': 'Access PIN',
122
+ 'modeLabel': 'Address mode',
123
+ 'advAddress': 'Advanced · Pick address',
124
+ 'wanOffHint': 'Reachable from any network once enabled (a disclaimer is confirmed on each enable)',
115
125
  'lanTitle': '📶 LAN (same Wi-Fi)',
116
126
  'lanHint': 'Scan to open once your phone is on the same Wi-Fi',
117
127
  'lanAccess': 'LAN access',
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "1.16.1"
83
+ "version": "2.0.0"
84
84
  }