dsh-pocket 2.6.3 → 2.7.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
@@ -37,7 +37,7 @@ __export(index_exports, {
37
37
  redactStatus: () => redactStatus
38
38
  });
39
39
  module.exports = __toCommonJS(index_exports);
40
- var import_react3 = require("react");
40
+ var import_react2 = require("react");
41
41
 
42
42
  // client/api.js
43
43
  var POCKET_RPC_CHANNEL = "/dsh-pocket";
@@ -337,57 +337,85 @@ function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t
337
337
  ));
338
338
  }
339
339
 
340
- // client/mobile/MobileComposerFullscreen.tsx
341
- var import_react2 = require("react");
342
- var FULLSCREEN_ATTR = "data-dsh-pocket-composer-fullscreen";
343
- function isFullscreen() {
344
- return typeof document !== "undefined" && document.body?.getAttribute(FULLSCREEN_ATTR) === "1";
340
+ // client/mobile/fileCopy.ts
341
+ var BUTTON_ATTR = "data-mobile-nav";
342
+ var BUTTON_VALUE = "copy-file";
343
+ var MARKER = "data-mobile-nav-copy";
344
+ async function copyText(text) {
345
+ try {
346
+ if (navigator.clipboard && window.isSecureContext) {
347
+ await navigator.clipboard.writeText(text);
348
+ return true;
349
+ }
350
+ } catch {
351
+ }
352
+ try {
353
+ const ta = document.createElement("textarea");
354
+ ta.value = text;
355
+ ta.setAttribute("readonly", "");
356
+ ta.style.position = "fixed";
357
+ ta.style.top = "0";
358
+ ta.style.left = "0";
359
+ ta.style.opacity = "0";
360
+ document.body.appendChild(ta);
361
+ ta.select();
362
+ const ok = document.execCommand("copy");
363
+ ta.remove();
364
+ return ok;
365
+ } catch {
366
+ return false;
367
+ }
345
368
  }
346
- function setFullscreen(on) {
347
- if (typeof document === "undefined") return;
348
- if (on) document.body.setAttribute(FULLSCREEN_ATTR, "1");
349
- else document.body.removeAttribute(FULLSCREEN_ATTR);
369
+ function createCopyButton() {
370
+ const btn = document.createElement("button");
371
+ btn.type = "button";
372
+ btn.setAttribute(BUTTON_ATTR, BUTTON_VALUE);
373
+ btn.textContent = "\u590D\u5236";
374
+ btn.setAttribute("aria-label", "\u590D\u5236\u6587\u4EF6\u5185\u5BB9");
375
+ btn.addEventListener("click", async (event) => {
376
+ event.preventDefault();
377
+ event.stopPropagation();
378
+ const card = btn.parentElement;
379
+ const block = card?.querySelector("pre") ?? null;
380
+ const text = block?.textContent ?? "";
381
+ const ok = await copyText(text);
382
+ const prev = btn.textContent;
383
+ btn.textContent = ok ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u5931\u8D25";
384
+ btn.setAttribute("data-copied", ok ? "1" : "0");
385
+ window.setTimeout(() => {
386
+ btn.textContent = prev;
387
+ btn.removeAttribute("data-copied");
388
+ }, 1500);
389
+ });
390
+ return btn;
350
391
  }
351
- function MobileComposerFullscreen() {
352
- const [on, setOn] = (0, import_react2.useState)(isFullscreen);
353
- (0, import_react2.useEffect)(() => {
354
- const update = () => setOn(isFullscreen());
355
- const observer = new MutationObserver(update);
356
- observer.observe(document.body, { attributes: true, attributeFilter: [FULLSCREEN_ATTR] });
357
- return () => observer.disconnect();
358
- }, []);
359
- return (0, import_react2.createElement)("button", {
360
- type: "button",
361
- "aria-label": on ? "\u6536\u8D77\u8F93\u5165" : "\u653E\u5927\u8F93\u5165",
362
- "aria-pressed": on,
363
- title: on ? "\u6536\u8D77\u8F93\u5165" : "\u653E\u5927\u8F93\u5165",
364
- "data-mobile-nav": "composer-fullscreen",
365
- "data-mobile-nav-keep": "1",
366
- // 自己的按钮不该被 #23 的 slot 通用隐藏规则误伤
367
- onClick: () => {
368
- const next = !isFullscreen();
369
- setFullscreen(next);
370
- setOn(next);
371
- },
372
- style: {
373
- appearance: "none",
374
- WebkitAppearance: "none",
375
- border: "none",
376
- background: "transparent",
377
- color: "inherit",
378
- cursor: "pointer",
379
- padding: "0 8px",
380
- height: 32,
381
- minWidth: 32,
382
- borderRadius: 6,
383
- display: "inline-flex",
384
- alignItems: "center",
385
- justifyContent: "center",
386
- font: "inherit",
387
- fontSize: 16,
388
- lineHeight: 1
389
- }
390
- }, on ? "\u2921" : "\u2922");
392
+ function injectInto(card) {
393
+ if (card.hasAttribute(MARKER)) return;
394
+ card.setAttribute(MARKER, "1");
395
+ if (getComputedStyle(card).position === "static") card.style.position = "relative";
396
+ card.appendChild(createCopyButton());
397
+ }
398
+ function collectCodeBlocks(root) {
399
+ const out = [];
400
+ for (const pre of Array.from(root.querySelectorAll("pre"))) {
401
+ const card = pre.parentElement;
402
+ if (card === null) continue;
403
+ if (card.hasAttribute(MARKER)) continue;
404
+ if ((pre.textContent ?? "").trim().length < 1) continue;
405
+ out.push(card);
406
+ }
407
+ return out;
408
+ }
409
+ function startFileCopyInjection() {
410
+ const scan = () => {
411
+ for (const card of collectCodeBlocks(document)) injectInto(card);
412
+ };
413
+ const observer = new MutationObserver(scan);
414
+ observer.observe(document.body, { childList: true, subtree: true });
415
+ scan();
416
+ return () => {
417
+ observer.disconnect();
418
+ };
391
419
  }
392
420
 
393
421
  // client/mobile/mobile.css.ts
@@ -1250,6 +1278,42 @@ var MOBILE_CSS = `
1250
1278
  [data-phase="hero"] [class$="_stack"] {
1251
1279
  gap: 0 !important;
1252
1280
  }
1281
+
1282
+ /* ---------- \u590D\u5236\u6587\u4EF6\u5185\u5BB9\u6309\u94AE\uFF08issue #17\uFF09 ----------
1283
+ \u6302\u5728\u4EE3\u7801/\u6587\u4EF6\u5757\u5BB9\u5668\u53F3\u4E0A\u89D2\uFF08position:relative \u7531 JS \u5728\u6CE8\u5165\u65F6\u8865\u4E0A\uFF09\u3002
1284
+ \u53EA\u5C4F\u5185\u53EF\u89C1\uFF1A\u684C\u9762\u7AEF DSH \u81EA\u5E26\u590D\u5236\uFF0C\u4E14\u672C effect \u53EA\u5728 narrow \u4E0B\u6302\u8F7D\uFF0C\u6309\u94AE
1285
+ \u6839\u672C\u4E0D\u4F1A\u6CE8\u5165\uFF1B\u8FD9\u91CC\u518D\u515C\u5E95\u4E00\u5C42\uFF0C\u907F\u514D\u4EFB\u4F55\u9057\u6F0F\u3002 */
1286
+ [data-mobile-nav="copy-file"] {
1287
+ position: absolute !important;
1288
+ top: 6px !important;
1289
+ right: 6px !important;
1290
+ z-index: 6 !important;
1291
+ display: inline-flex !important;
1292
+ align-items: center !important;
1293
+ justify-content: center !important;
1294
+ height: 26px !important;
1295
+ padding: 0 10px !important;
1296
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)) !important;
1297
+ border-radius: 8px !important;
1298
+ background: var(--dsw-alias-bg-base, #ffffff) !important;
1299
+ color: var(--dsw-alias-label-primary, inherit) !important;
1300
+ font-family: inherit !important;
1301
+ font-size: 12px !important;
1302
+ line-height: 1 !important;
1303
+ cursor: pointer !important;
1304
+ -webkit-tap-highlight-color: transparent !important;
1305
+ box-shadow: 0 1px 4px rgba(0, 0, 0, .14) !important;
1306
+ }
1307
+ [data-mobile-nav="copy-file"]:active {
1308
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06)) !important;
1309
+ }
1310
+ [data-mobile-nav="copy-file"][data-copied="1"] {
1311
+ color: var(--dsw-alias-state-success-primary, #1a9d54) !important;
1312
+ border-color: var(--dsw-alias-state-success-primary, #1a9d54) !important;
1313
+ }
1314
+ [data-mobile-nav="copy-file"][data-copied="0"] {
1315
+ color: var(--dsw-alias-state-danger-primary, #e5484d) !important;
1316
+ }
1253
1317
  }
1254
1318
 
1255
1319
  /* ---------- desktop: the mobile controls must never appear ---------- */
@@ -1264,52 +1328,8 @@ var MOBILE_CSS = `
1264
1328
  [data-mobile-nav="drawer-actions"] {
1265
1329
  display: none !important;
1266
1330
  }
1267
- /* \u684C\u9762\u7AEF\u6C38\u8FDC\u4E0D\u9700\u8981\u300C\u26F6 \u653E\u5927\u8F93\u5165\u300D\u6309\u94AE\uFF08composer \u5DF2\u7ECF\u6709\u5B8C\u6574\u5DE5\u5177\u884C\uFF09 */
1268
- [data-mobile-nav="composer-fullscreen"] {
1269
- display: none !important;
1270
- }
1271
1331
  }
1272
1332
 
1273
- /* ---------- \u653E\u5927\u8F93\u5165\uFF08issue #23\uFF09 ----------
1274
- \u7A84\u5C4F\u9ED8\u8BA4\u9690\u85CF\u63D2\u4EF6\u6CE8\u518C\u5230 conversation.input.* slot \u7684 UI\uFF08\u4EC5\u7559\u5B98\u65B9 resident
1275
- chrome\uFF09\uFF0C\u70B9\u300C\u26F6\u300D\u6309\u94AE\u628A composer \u5361\u7247\u56FA\u5B9A\u5230\u5168\u5C4F\u540E\u6240\u6709\u63D2\u4EF6 UI \u6062\u590D\u663E\u793A\u2014\u2014
1276
- \u65B0\u63D2\u4EF6\u96F6\u9002\u914D\u3002
1277
- \u5B9E\u73B0\u8BF4\u660E\uFF1A\u5F53\u524D dsh 0.1.1 \u6CA1\u628A input slot \u6E32\u67D3\u6210 [data-slot] \u5305\u88C5\uFF080.1.2+ \u624D\u6709\uFF09\uFF0C
1278
- \u6240\u4EE5\u8FD9\u91CC\u7528\u300C\u767D\u540D\u5355\u300D\u53CD\u9009\uFF1AmobileApply \u6CE8\u518C\u7684 mobile \u81EA\u8EAB\u8282\u70B9\u52A0
1279
- data-mobile-nav="composer-fullscreen" \u4E0D\u88AB\u9690\u85CF\uFF1B\u5176\u5B83\u63D2\u4EF6 UI \u9690\u85CF\u3002 */
1280
- @media (max-width: 1023px) {
1281
- /* \u9ED8\u8BA4\uFF08\u975E\u5168\u5C4F\uFF09\uFF1A\u9690\u85CF input slot \u5BB9\u5668\u4E0B\u7684\u6240\u6709\u76F4\u63A5\u5B50\u5143\u7D20\uFF08\u5305\u542B\u63D2\u4EF6\u548C\u5B98\u65B9\uFF09
1282
- \u2014\u2014\u5B98\u65B9\u81EA\u5DF1\u5982\u679C\u60F3\u5728\u7A84\u5C4F\u4E5F\u9732\u51FA\uFF0C\u6CE8 input slot \u65F6\u52A0 data-mobile-nav-keep="1" */
1283
- [data-dsh-pocket-composer-fullscreen]:not([data-dsh-pocket-composer-fullscreen="1"])
1284
- [class$="_card"]:has(textarea) [data-slot^="conversation.input."] > :not([data-mobile-nav-keep]),
1285
- [data-dsh-pocket-composer-fullscreen]:not([data-dsh-pocket-composer-fullscreen="1"])
1286
- [class$="_card"]:has(textarea) [data-slot^="conversation.input."]:empty {
1287
- display: none !important;
1288
- }
1289
- /* \u653E\u5927\u6309\u94AE\uFF1A\u5E38\u9A7B\u5728 conversation.input.right slot \u65C1\uFF0C\u684C\u9762\u7AEF\u5DF2\u88AB\u4E0A\u9762\u89C4\u5219\u9690\u85CF */
1290
- [data-mobile-nav="composer-fullscreen"] {
1291
- display: inline-flex;
1292
- }
1293
- /* \u5168\u5C4F\u6A21\u5F0F\uFF1Acomposer \u5361\u7247\u56FA\u5B9A\u5230\u89C6\u53E3\uFF0Ctextarea \u62C9\u9AD8\uFF0C\u5DE5\u5177\u884C\u53EF\u6362\u884C */
1294
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) {
1295
- position: fixed !important;
1296
- inset: 0 !important;
1297
- z-index: 9999 !important;
1298
- border-radius: 0 !important;
1299
- margin: 0 !important;
1300
- height: 100dvh !important;
1301
- display: flex !important;
1302
- flex-direction: column !important;
1303
- }
1304
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) textarea {
1305
- flex: 1 1 auto !important;
1306
- min-height: 50dvh !important;
1307
- max-height: none !important;
1308
- }
1309
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) [data-slot^="conversation.input."] {
1310
- flex-wrap: wrap !important;
1311
- }
1312
- }
1313
1333
  `;
1314
1334
 
1315
1335
  // client/mobile/locales.ts
@@ -1525,6 +1545,11 @@ function mobileApply(ctx) {
1525
1545
  observer.disconnect();
1526
1546
  };
1527
1547
  }, "dsh-mobile-nav: sheet rise animation replay");
1548
+ ctx.effect(() => {
1549
+ if (!narrow.matches) return () => {
1550
+ };
1551
+ return startFileCopyInjection();
1552
+ }, "dsh-mobile-nav: file copy button (issue #17)");
1528
1553
  ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
1529
1554
  name: "conversation.session.header.actions",
1530
1555
  id: "mobile-nav-toggle",
@@ -1534,12 +1559,6 @@ function mobileApply(ctx) {
1534
1559
  toggleSidebar: () => ctx.layout.toggleSidebar()
1535
1560
  })
1536
1561
  }, MobileNavToggle));
1537
- ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
1538
- name: "conversation.input.right",
1539
- id: "mobile-composer-fullscreen",
1540
- order: 100,
1541
- locale: NS
1542
- }, MobileComposerFullscreen));
1543
1562
  ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1544
1563
  name: "shell.overlay",
1545
1564
  id: "mobile-nav-overlay",
@@ -1779,15 +1798,15 @@ var styles = {
1779
1798
  warn: { color: "var(--dsw-alias-state-warn-primary,#b45309)", fontSize: 12, lineHeight: 1.5 }
1780
1799
  };
1781
1800
  function PocketSettingsTab({ rpcCall, t }) {
1782
- const [status, setStatus] = (0, import_react3.useState)(null);
1783
- const [busy, setBusy] = (0, import_react3.useState)(false);
1784
- const [error, setError] = (0, import_react3.useState)(null);
1785
- const [tunnelState, setTunnelState] = (0, import_react3.useState)(null);
1786
- const [restartNotice, setRestartNotice] = (0, import_react3.useState)(false);
1787
- const [updateInfo, setUpdateInfo] = (0, import_react3.useState)(null);
1788
- const [isDesktop, setIsDesktop] = (0, import_react3.useState)(false);
1789
- const [now, setNow] = (0, import_react3.useState)(Date.now());
1790
- (0, import_react3.useEffect)(() => {
1801
+ const [status, setStatus] = (0, import_react2.useState)(null);
1802
+ const [busy, setBusy] = (0, import_react2.useState)(false);
1803
+ const [error, setError] = (0, import_react2.useState)(null);
1804
+ const [tunnelState, setTunnelState] = (0, import_react2.useState)(null);
1805
+ const [restartNotice, setRestartNotice] = (0, import_react2.useState)(false);
1806
+ const [updateInfo, setUpdateInfo] = (0, import_react2.useState)(null);
1807
+ const [isDesktop, setIsDesktop] = (0, import_react2.useState)(false);
1808
+ const [now, setNow] = (0, import_react2.useState)(Date.now());
1809
+ (0, import_react2.useEffect)(() => {
1791
1810
  const t2 = setInterval(() => setNow(Date.now()), 1e3);
1792
1811
  return () => clearInterval(t2);
1793
1812
  }, []);
@@ -1819,18 +1838,18 @@ function PocketSettingsTab({ rpcCall, t }) {
1819
1838
  } catch {
1820
1839
  }
1821
1840
  };
1822
- (0, import_react3.useEffect)(() => {
1841
+ (0, import_react2.useEffect)(() => {
1823
1842
  load();
1824
1843
  const t2 = setInterval(load, 3e3);
1825
1844
  return () => clearInterval(t2);
1826
1845
  }, []);
1827
- (0, import_react3.useEffect)(() => {
1846
+ (0, import_react2.useEffect)(() => {
1828
1847
  try {
1829
1848
  sessionStorage.removeItem("dshp-auto-reloaded");
1830
1849
  } catch {
1831
1850
  }
1832
1851
  }, []);
1833
- (0, import_react3.useEffect)(() => {
1852
+ (0, import_react2.useEffect)(() => {
1834
1853
  if (isDesktop) return;
1835
1854
  let alive = true;
1836
1855
  const check = async () => {
@@ -1886,8 +1905,8 @@ function PocketSettingsTab({ rpcCall, t }) {
1886
1905
  setUpdateInfo((u) => ({ ...u, updating: false, result: "fail", output: err.message }));
1887
1906
  }
1888
1907
  };
1889
- const [disclaimerOpen, setDisclaimerOpen] = (0, import_react3.useState)(false);
1890
- const [disclaimerChecked, setDisclaimerChecked] = (0, import_react3.useState)(false);
1908
+ const [disclaimerOpen, setDisclaimerOpen] = (0, import_react2.useState)(false);
1909
+ const [disclaimerChecked, setDisclaimerChecked] = (0, import_react2.useState)(false);
1891
1910
  const doStartTunnel = async () => {
1892
1911
  const cfg = status?.tunnelConfig;
1893
1912
  if (cfg?.mode === "named" && (!cfg.hostname || !cfg.tokenSet)) {
@@ -1920,7 +1939,7 @@ function PocketSettingsTab({ rpcCall, t }) {
1920
1939
  } catch {
1921
1940
  }
1922
1941
  };
1923
- const [tunnelCfg, setTunnelCfg] = (0, import_react3.useState)(null);
1942
+ const [tunnelCfg, setTunnelCfg] = (0, import_react2.useState)(null);
1924
1943
  const switchToQuick = async () => {
1925
1944
  try {
1926
1945
  setStatus(await call(POCKET_ENDPOINTS.tunnelSetConfig, { mode: "quick" }));
@@ -1941,7 +1960,7 @@ function PocketSettingsTab({ rpcCall, t }) {
1941
1960
  setTunnelCfg((c) => ({ ...c, err: err.message }));
1942
1961
  }
1943
1962
  };
1944
- const [resetOpen, setResetOpen] = (0, import_react3.useState)(false);
1963
+ const [resetOpen, setResetOpen] = (0, import_react2.useState)(false);
1945
1964
  const doFactoryReset = async () => {
1946
1965
  setResetOpen(false);
1947
1966
  setBusy(true);
@@ -1973,7 +1992,7 @@ function PocketSettingsTab({ rpcCall, t }) {
1973
1992
  } catch {
1974
1993
  }
1975
1994
  };
1976
- const [lanToggleOpen, setLanToggleOpen] = (0, import_react3.useState)(null);
1995
+ const [lanToggleOpen, setLanToggleOpen] = (0, import_react2.useState)(null);
1977
1996
  const requestLanToggle = (on) => setLanToggleOpen(on);
1978
1997
  const confirmLanToggle = async () => {
1979
1998
  const on = lanToggleOpen;
@@ -1993,7 +2012,7 @@ function PocketSettingsTab({ rpcCall, t }) {
1993
2012
  setError(err.message);
1994
2013
  }
1995
2014
  };
1996
- const [customPin, setCustomPin] = (0, import_react3.useState)(null);
2015
+ const [customPin, setCustomPin] = (0, import_react2.useState)(null);
1997
2016
  const saveCustomPin = async (which) => {
1998
2017
  try {
1999
2018
  const r = await call(POCKET_ENDPOINTS.pinSetCustom, { which, value: customPin?.value ?? "" });
@@ -2009,11 +2028,11 @@ function PocketSettingsTab({ rpcCall, t }) {
2009
2028
  setCustomPin((c) => ({ ...c, err: err.message }));
2010
2029
  }
2011
2030
  };
2012
- const customPinRow = (which) => (0, import_react3.createElement)(
2031
+ const customPinRow = (which) => (0, import_react2.createElement)(
2013
2032
  "div",
2014
2033
  { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
2015
2034
  t("customizing"),
2016
- (0, import_react3.createElement)("input", {
2035
+ (0, import_react2.createElement)("input", {
2017
2036
  style: { width: 130, margin: "0 6px", padding: "4px 8px", fontSize: 14, letterSpacing: 1, textAlign: "center", border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none" },
2018
2037
  type: "password",
2019
2038
  maxLength: 8,
@@ -2025,11 +2044,11 @@ function PocketSettingsTab({ rpcCall, t }) {
2025
2044
  if (e.key === "Escape") setCustomPin(null);
2026
2045
  }
2027
2046
  }),
2028
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t("save")),
2029
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setCustomPin(null) }, t("cancel")),
2030
- customPin?.err ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(customPin.err)) : null
2047
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t("save")),
2048
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setCustomPin(null) }, t("cancel")),
2049
+ customPin?.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(customPin.err)) : null
2031
2050
  );
2032
- const customBtn = (which) => (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setCustomPin({ which, value: "", err: null }) }, t("customize"));
2051
+ const customBtn = (which) => (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setCustomPin({ which, value: "", err: null }) }, t("customize"));
2033
2052
  const lanUrl = status?.lanUrl;
2034
2053
  const tunnelUrl = status?.tunnelUrl;
2035
2054
  const tunnelPhase = tunnelState?.phase ?? "idle";
@@ -2045,14 +2064,14 @@ function PocketSettingsTab({ rpcCall, t }) {
2045
2064
  if (i < 0) return s;
2046
2065
  return (t("ok") === zh2.ok ? s.slice(0, i) : s.slice(i + 3)).trim();
2047
2066
  };
2048
- const [toast, setToast] = (0, import_react3.useState)(null);
2049
- const toastTimer = (0, import_react3.useRef)(null);
2067
+ const [toast, setToast] = (0, import_react2.useState)(null);
2068
+ const toastTimer = (0, import_react2.useRef)(null);
2050
2069
  const showToast = (text) => {
2051
2070
  setToast(text);
2052
2071
  clearTimeout(toastTimer.current);
2053
2072
  toastTimer.current = setTimeout(() => setToast(null), 2600);
2054
2073
  };
2055
- (0, import_react3.useEffect)(() => () => clearTimeout(toastTimer.current), []);
2074
+ (0, import_react2.useEffect)(() => () => clearTimeout(toastTimer.current), []);
2056
2075
  const modeBtnStyle = (active) => ({
2057
2076
  ...styles.btn,
2058
2077
  height: 28,
@@ -2062,49 +2081,49 @@ function PocketSettingsTab({ rpcCall, t }) {
2062
2081
  background: active ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-bg-layer-1,#fff)",
2063
2082
  color: active ? "var(--dsw-alias-label-primary-foreground, #fff)" : "var(--dsw-alias-label-primary,inherit)"
2064
2083
  });
2065
- const Switch = (on, onClick) => (0, import_react3.createElement)("button", {
2084
+ const Switch = (on, onClick) => (0, import_react2.createElement)("button", {
2066
2085
  role: "switch",
2067
2086
  "aria-checked": !!on,
2068
2087
  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)" },
2069
2088
  onClick
2070
- }, (0, import_react3.createElement)("span", { style: { position: "absolute", top: 2, left: on ? 20 : 2, width: 18, height: 18, borderRadius: "50%", background: "#fff" } }));
2071
- const qrArea = (src, url, hint) => (0, import_react3.createElement)(
2089
+ }, (0, import_react2.createElement)("span", { style: { position: "absolute", top: 2, left: on ? 20 : 2, width: 18, height: 18, borderRadius: "50%", background: "#fff" } }));
2090
+ const qrArea = (src, url, hint) => (0, import_react2.createElement)(
2072
2091
  "div",
2073
2092
  { style: { background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", borderRadius: 10, padding: "10px 12px", textAlign: "center", margin: "10px 0" } },
2074
- (0, import_react3.createElement)("img", { src, alt: "QR", style: styles.qr }),
2075
- (0, import_react3.createElement)("div", { style: styles.code }, url),
2076
- (0, import_react3.createElement)("div", { style: styles.muted }, hint)
2093
+ (0, import_react2.createElement)("img", { src, alt: "QR", style: styles.qr }),
2094
+ (0, import_react2.createElement)("div", { style: styles.code }, url),
2095
+ (0, import_react2.createElement)("div", { style: styles.muted }, hint)
2077
2096
  );
2078
- const row = (label, control, extra) => (0, import_react3.createElement)(
2097
+ const row = (label, control, extra) => (0, import_react2.createElement)(
2079
2098
  "div",
2080
2099
  { style: { borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", paddingTop: 9, marginTop: 9 } },
2081
- (0, import_react3.createElement)(
2100
+ (0, import_react2.createElement)(
2082
2101
  "div",
2083
2102
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2084
- (0, import_react3.createElement)("span", { style: { fontSize: 13 } }, label),
2103
+ (0, import_react2.createElement)("span", { style: { fontSize: 13 } }, label),
2085
2104
  control
2086
2105
  ),
2087
2106
  extra ?? null
2088
2107
  );
2089
- const [advOpen, setAdvOpen] = (0, import_react3.useState)(false);
2090
- return (0, import_react3.createElement)(
2108
+ const [advOpen, setAdvOpen] = (0, import_react2.useState)(false);
2109
+ return (0, import_react2.createElement)(
2091
2110
  "div",
2092
2111
  { style: styles.card },
2093
- (0, import_react3.createElement)(
2112
+ (0, import_react2.createElement)(
2094
2113
  "div",
2095
2114
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2096
- (0, import_react3.createElement)(
2115
+ (0, import_react2.createElement)(
2097
2116
  "div",
2098
2117
  null,
2099
- (0, import_react3.createElement)("strong", null, t("title")),
2100
- (0, import_react3.createElement)("div", { style: styles.muted }, t("subtitle"))
2118
+ (0, import_react2.createElement)("strong", null, t("title")),
2119
+ (0, import_react2.createElement)("div", { style: styles.muted }, t("subtitle"))
2101
2120
  ),
2102
- (0, import_react3.createElement)(
2121
+ (0, import_react2.createElement)(
2103
2122
  "div",
2104
2123
  { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", textAlign: "right" } },
2105
- (0, import_react3.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("developer")),
2106
- (0, import_react3.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("starAsk")),
2107
- (0, import_react3.createElement)(
2124
+ (0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("developer")),
2125
+ (0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("starAsk")),
2126
+ (0, import_react2.createElement)(
2108
2127
  "a",
2109
2128
  { href: "https://github.com/shaobeichen/dsh-pocket", target: "_blank", rel: "noreferrer", style: { color: "var(--dsw-alias-brand-primary,#4f6ef7)", fontSize: 12, lineHeight: 1.6, textDecoration: "underline" } },
2110
2129
  t("starCta")
@@ -2113,49 +2132,49 @@ function PocketSettingsTab({ rpcCall, t }) {
2113
2132
  ),
2114
2133
  // 桌面端不显示更新/重启横幅(更新由 DSH Desktop 管理),也不需要额外提示
2115
2134
  // 重启后提示(进程在后台运行,停止方法)——左侧蓝色色条(桌面端不会触发本插件的自重启)
2116
- !isDesktop && restartNotice ? (0, import_react3.createElement)(
2135
+ !isDesktop && restartNotice ? (0, import_react2.createElement)(
2117
2136
  "div",
2118
2137
  { style: { ...styles.block, borderLeft: "4px solid var(--dsw-alias-brand-primary,#4f6ef7)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" } },
2119
- (0, import_react3.createElement)(
2138
+ (0, import_react2.createElement)(
2120
2139
  "div",
2121
2140
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2122
- (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("restarted")),
2123
- (0, import_react3.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, t("ok"))
2141
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("restarted")),
2142
+ (0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, t("ok"))
2124
2143
  ),
2125
- (0, import_react3.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, fmt(t, "bgHint", { cmd: status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9` }))
2144
+ (0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, fmt(t, "bgHint", { cmd: status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9` }))
2126
2145
  ) : null,
2127
2146
  // 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
2128
2147
  // 桌面端不渲染(更新由 DSH Desktop 管理)
2129
- !isDesktop && updateInfo ? (0, import_react3.createElement)(
2148
+ !isDesktop && updateInfo ? (0, import_react2.createElement)(
2130
2149
  "div",
2131
2150
  { style: { ...styles.block, borderLeft: "4px solid var(--dsw-alias-state-warn-primary,#b45309)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" } },
2132
- (0, import_react3.createElement)(
2151
+ (0, import_react2.createElement)(
2133
2152
  "div",
2134
2153
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2135
- (0, import_react3.createElement)(
2154
+ (0, import_react2.createElement)(
2136
2155
  "div",
2137
2156
  { style: { fontWeight: 600, fontSize: 13 } },
2138
2157
  updateInfo.updated ? fmt(t, "updatedRestart", { ver: updateInfo.current }) : updateInfo.result === "ok" ? updateInfo.autoRestart ? fmt(t, "updateAutoRestarting", { ver: updateInfo.latest }) : fmt(t, "updatedOk", { ver: updateInfo.latest }) : fmt(t, "updateAvailable", { ver: updateInfo.latest })
2139
2158
  ),
2140
- updateInfo.result !== "ok" ? (0, import_react3.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? t("updating") : fmt(t, "updateTo", { ver: updateInfo.latest })) : updateInfo.autoRestart ? (0, import_react3.createElement)("button", { style: styles.btn, disabled: true }, t("restartingNow")) : (0, import_react3.createElement)("button", { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? t("restarting") : t("restartNow"))
2159
+ updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? t("updating") : fmt(t, "updateTo", { ver: updateInfo.latest })) : updateInfo.autoRestart ? (0, import_react2.createElement)("button", { style: styles.btn, disabled: true }, t("restartingNow")) : (0, import_react2.createElement)("button", { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? t("restarting") : t("restartNow"))
2141
2160
  ),
2142
- (0, import_react3.createElement)(
2161
+ (0, import_react2.createElement)(
2143
2162
  "div",
2144
2163
  { style: styles.muted, marginTop: 4 },
2145
2164
  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: errText(updateInfo.output) || t("unknownError") }) : fmt(t, "versionRange", { cur: updateInfo.current, latest: updateInfo.latest })
2146
2165
  )
2147
2166
  ) : null,
2148
2167
  // 局域网:标题行自带总开关 → 二维码+地址 → 设置行(访问密码 / 高级·手动选地址)
2149
- (0, import_react3.createElement)(
2168
+ (0, import_react2.createElement)(
2150
2169
  "div",
2151
2170
  { style: styles.block },
2152
- (0, import_react3.createElement)(
2171
+ (0, import_react2.createElement)(
2153
2172
  "div",
2154
2173
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
2155
- (0, import_react3.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("lanAccess")),
2174
+ (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("lanAccess")),
2156
2175
  Switch(status?.lanEnabled !== false, () => requestLanToggle(status?.lanEnabled === false))
2157
2176
  ),
2158
- status?.lanEnabled === false ? (0, import_react3.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("lanDisabledHint")) : lanUrl ? (0, import_react3.createElement)(
2177
+ 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)(
2159
2178
  "div",
2160
2179
  null,
2161
2180
  qrArea(status.lanQr, lanUrl, t("lanHint")),
@@ -2163,100 +2182,100 @@ function PocketSettingsTab({ rpcCall, t }) {
2163
2182
  row(
2164
2183
  t("lanPin"),
2165
2184
  Switch(status?.lanAuthEnabled !== false, () => setLanAuth(status?.lanAuthEnabled === false)),
2166
- status?.lanAuthEnabled === false ? (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("lanPinOff")) : customPin?.which === "lan" ? customPinRow("lan") : (0, import_react3.createElement)(
2185
+ status?.lanAuthEnabled === false ? (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("lanPinOff")) : customPin?.which === "lan" ? customPinRow("lan") : (0, import_react2.createElement)(
2167
2186
  "div",
2168
2187
  { style: { marginTop: 6, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
2169
- (0, import_react3.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.lanToken),
2170
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: refreshLanPin }, t("refresh")),
2188
+ (0, import_react2.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.lanToken),
2189
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: refreshLanPin }, t("refresh")),
2171
2190
  customBtn("lan"),
2172
- status?.lanPinCustom ? (0, import_react3.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("pinCustomHint")) : null
2191
+ status?.lanPinCustom ? (0, import_react2.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("pinCustomHint")) : null
2173
2192
  )
2174
2193
  ),
2175
2194
  // 高级:手动选地址(默认收起)
2176
2195
  row(
2177
2196
  t("advAddress"),
2178
- (0, import_react3.createElement)(
2197
+ (0, import_react2.createElement)(
2179
2198
  "button",
2180
2199
  { style: { border: "none", background: "none", font: "inherit", cursor: "pointer", fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", padding: 0 }, onClick: () => setAdvOpen((v) => !v) },
2181
2200
  (status?.lanIpOverride || t("lanAddressAuto")) + " \u203A"
2182
2201
  ),
2183
- advOpen ? (0, import_react3.createElement)(
2202
+ advOpen ? (0, import_react2.createElement)(
2184
2203
  "div",
2185
2204
  { style: { marginTop: 8 } },
2186
- (0, import_react3.createElement)(
2205
+ (0, import_react2.createElement)(
2187
2206
  "label",
2188
2207
  { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
2189
2208
  t("lanAddress"),
2190
- (0, import_react3.createElement)(
2209
+ (0, import_react2.createElement)(
2191
2210
  "select",
2192
2211
  {
2193
2212
  value: status?.lanIpOverride || "",
2194
2213
  onChange: (e) => setLanAddress(e.target.value),
2195
2214
  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)" }
2196
2215
  },
2197
- (0, import_react3.createElement)("option", { value: "" }, t("lanAddressAuto")),
2198
- (status?.lanCandidates || []).map((ip) => (0, import_react3.createElement)("option", { key: ip, value: ip }, ip))
2216
+ (0, import_react2.createElement)("option", { value: "" }, t("lanAddressAuto")),
2217
+ (status?.lanCandidates || []).map((ip) => (0, import_react2.createElement)("option", { key: ip, value: ip }, ip))
2199
2218
  )
2200
2219
  )
2201
2220
  ) : null
2202
2221
  )
2203
- ) : (0, import_react3.createElement)("div", { style: styles.muted }, t("lanStarting"))
2222
+ ) : (0, import_react2.createElement)("div", { style: styles.muted }, t("lanStarting"))
2204
2223
  ),
2205
2224
  // 公网:标题行自带 开启/关闭 → 开启后:二维码+地址、地址模式行、访问密码行
2206
- (0, import_react3.createElement)(
2225
+ (0, import_react2.createElement)(
2207
2226
  "div",
2208
2227
  { style: styles.block },
2209
- (0, import_react3.createElement)(
2228
+ (0, import_react2.createElement)(
2210
2229
  "div",
2211
2230
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
2212
- (0, import_react3.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("wanAccess")),
2213
- tunnelUrl ? (0, import_react3.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_react3.createElement)("button", { style: { ...styles.primary, height: 28, padding: "0 14px", fontSize: 12 }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy || tunnelStarting ? t("opening") : t("enable"))
2231
+ (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("wanAccess")),
2232
+ 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"))
2214
2233
  ),
2215
- tunnelStarting ? (0, import_react3.createElement)(
2234
+ tunnelStarting ? (0, import_react2.createElement)(
2216
2235
  "div",
2217
2236
  { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
2218
2237
  tunnelPhase === "downloading" ? fmt(t, "downloading", { s: elapsed(tunnelStateStarted) }) : fmt(t, "connecting", { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t("slowHint") : "" })
2219
- ) : tunnelPhase === "error" ? (0, import_react3.createElement)(
2238
+ ) : tunnelPhase === "error" ? (0, import_react2.createElement)(
2220
2239
  "div",
2221
2240
  { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } },
2222
2241
  fmt(t, "error", { detail: errText(tunnelStateDetail) || t("unknownError") })
2223
- ) : !tunnelUrl && !isDesktop ? (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 8 } }, t("wanOffHint")) : null,
2224
- tunnelUrl ? (0, import_react3.createElement)(
2242
+ ) : !tunnelUrl && !isDesktop ? (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 8 } }, t("wanOffHint")) : null,
2243
+ tunnelUrl ? (0, import_react2.createElement)(
2225
2244
  "div",
2226
2245
  null,
2227
2246
  qrArea(status.tunnelQr, tunnelUrl, namedMode ? t("namedRunningHint") : t("wanHint")),
2228
2247
  // 地址模式行(随机/固定;固定域名选中或编辑时高亮)
2229
2248
  row(
2230
2249
  t("modeLabel"),
2231
- (0, import_react3.createElement)(
2250
+ (0, import_react2.createElement)(
2232
2251
  "span",
2233
2252
  { style: { display: "inline-flex", gap: 6 } },
2234
- (0, import_react3.createElement)("button", { style: modeBtnStyle(!namedActive), onClick: namedMode ? switchToQuick : tunnelCfg ? () => setTunnelCfg(null) : void 0 }, t("modeQuick")),
2235
- (0, import_react3.createElement)("button", { style: modeBtnStyle(namedActive), onClick: () => setTunnelCfg(tunnelCfg ? null : { hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("modeNamed"))
2253
+ (0, import_react2.createElement)("button", { style: modeBtnStyle(!namedActive), onClick: namedMode ? switchToQuick : tunnelCfg ? () => setTunnelCfg(null) : void 0 }, t("modeQuick")),
2254
+ (0, import_react2.createElement)("button", { style: modeBtnStyle(namedActive), onClick: () => setTunnelCfg(tunnelCfg ? null : { hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("modeNamed"))
2236
2255
  ),
2237
- (0, import_react3.createElement)(
2256
+ (0, import_react2.createElement)(
2238
2257
  "div",
2239
2258
  { style: { marginTop: 6 } },
2240
2259
  // 刚保存固定域名但当前连接仍是随机域名:需关闭后重新开启才生效
2241
- namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? "") ? (0, import_react3.createElement)("div", { style: { ...styles.warn } }, t("namedTakeEffect")) : null,
2260
+ namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? "") ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("namedTakeEffect")) : null,
2242
2261
  // 固定域名:已保存摘要 + 修改入口(非编辑态)
2243
- namedMode && !tunnelCfg ? (0, import_react3.createElement)(
2262
+ namedMode && !tunnelCfg ? (0, import_react2.createElement)(
2244
2263
  "div",
2245
2264
  { style: { ...styles.muted } },
2246
2265
  fmt(t, "namedSummary", { host: tunnelModeView.hostname || "\u2014", token: tunnelModeView.tokenSet ? t("namedTokenSet") : t("namedTokenMissing") }),
2247
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setTunnelCfg({ hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("namedEdit")),
2248
- (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 4 } }, t("namedHow")),
2249
- !tunnelModeView.tokenSet || !tunnelModeView.hostname ? (0, import_react3.createElement)("div", { style: { marginTop: 2, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("namedNeedCfg")) : null
2266
+ (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")),
2267
+ (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 4 } }, t("namedHow")),
2268
+ !tunnelModeView.tokenSet || !tunnelModeView.hostname ? (0, import_react2.createElement)("div", { style: { marginTop: 2, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("namedNeedCfg")) : null
2250
2269
  ) : null,
2251
2270
  // 固定域名:编辑表单(域名 + Tunnel Token,Token 留空保持不变)
2252
- tunnelCfg ? (0, import_react3.createElement)(
2271
+ tunnelCfg ? (0, import_react2.createElement)(
2253
2272
  "div",
2254
2273
  { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.6 } },
2255
- (0, import_react3.createElement)(
2274
+ (0, import_react2.createElement)(
2256
2275
  "div",
2257
2276
  null,
2258
2277
  t("namedHostnameLabel"),
2259
- (0, import_react3.createElement)("input", {
2278
+ (0, import_react2.createElement)("input", {
2260
2279
  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 },
2261
2280
  placeholder: "pocket.example.com",
2262
2281
  value: tunnelCfg.hostname ?? "",
@@ -2268,11 +2287,11 @@ function PocketSettingsTab({ rpcCall, t }) {
2268
2287
  }
2269
2288
  })
2270
2289
  ),
2271
- (0, import_react3.createElement)(
2290
+ (0, import_react2.createElement)(
2272
2291
  "div",
2273
2292
  { style: { marginTop: 6 } },
2274
2293
  t("namedTokenLabel"),
2275
- (0, import_react3.createElement)("input", {
2294
+ (0, import_react2.createElement)("input", {
2276
2295
  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" },
2277
2296
  type: "password",
2278
2297
  value: tunnelCfg.token ?? "",
@@ -2283,121 +2302,121 @@ function PocketSettingsTab({ rpcCall, t }) {
2283
2302
  }
2284
2303
  })
2285
2304
  ),
2286
- (0, import_react3.createElement)(
2305
+ (0, import_react2.createElement)(
2287
2306
  "div",
2288
2307
  { style: { marginTop: 6, display: "flex", gap: 8 } },
2289
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: saveNamedTunnel }, t("save")),
2290
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setTunnelCfg(null) }, t("cancel"))
2308
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: saveNamedTunnel }, t("save")),
2309
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setTunnelCfg(null) }, t("cancel"))
2291
2310
  ),
2292
- (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("namedHow")),
2293
- (0, import_react3.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("namedSecurity")),
2294
- tunnelCfg.err ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(tunnelCfg.err)) : null
2311
+ (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("namedHow")),
2312
+ (0, import_react2.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("namedSecurity")),
2313
+ tunnelCfg.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(tunnelCfg.err)) : null
2295
2314
  ) : null
2296
2315
  )
2297
2316
  ),
2298
2317
  // 访问密码行:值 + 自定义(自定义输入态整体替换)
2299
2318
  status.accessToken ? row(
2300
2319
  t("pinLabel"),
2301
- customPin?.which === "public" ? null : (0, import_react3.createElement)(
2320
+ customPin?.which === "public" ? null : (0, import_react2.createElement)(
2302
2321
  "span",
2303
2322
  { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
2304
- (0, import_react3.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.accessToken),
2323
+ (0, import_react2.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.accessToken),
2305
2324
  customBtn("public")
2306
2325
  ),
2307
- (0, import_react3.createElement)(
2326
+ (0, import_react2.createElement)(
2308
2327
  "div",
2309
2328
  { style: { marginTop: 6 } },
2310
2329
  customPin?.which === "public" ? customPinRow("public") : null,
2311
- status?.publicPinCustom ? (0, import_react3.createElement)("div", { style: { ...styles.warn } }, t("pinCustomHint")) : null,
2312
- namedMode ? (0, import_react3.createElement)("div", { style: { ...styles.warn } }, t("namedSecurity")) : null
2330
+ status?.publicPinCustom ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("pinCustomHint")) : null,
2331
+ namedMode ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("namedSecurity")) : null
2313
2332
  )
2314
2333
  ) : null
2315
2334
  ) : null
2316
2335
  ),
2317
- error ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${errText(error)}`) : null,
2336
+ error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${errText(error)}`) : null,
2318
2337
  // 恢复出厂设置:设置出问题时的临时兜底(最底部,避免误触)
2319
- (0, import_react3.createElement)(
2338
+ (0, import_react2.createElement)(
2320
2339
  "div",
2321
2340
  { style: styles.block },
2322
- (0, import_react3.createElement)(
2341
+ (0, import_react2.createElement)(
2323
2342
  "div",
2324
2343
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2325
- (0, import_react3.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("resetFactory")),
2326
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: () => setResetOpen(true) }, t("resetGo"))
2344
+ (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("resetFactory")),
2345
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: () => setResetOpen(true) }, t("resetGo"))
2327
2346
  ),
2328
- (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("resetIntro"))
2347
+ (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("resetIntro"))
2329
2348
  ),
2330
2349
  // 恢复出厂设置确认弹框
2331
- resetOpen ? (0, import_react3.createElement)(
2350
+ resetOpen ? (0, import_react2.createElement)(
2332
2351
  "div",
2333
2352
  { style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
2334
- (0, import_react3.createElement)(
2353
+ (0, import_react2.createElement)(
2335
2354
  "div",
2336
2355
  { style: { background: "var(--dsw-alias-bg-layer-1,#fff)", borderRadius: 12, maxWidth: 440, width: "100%", padding: "20px 22px", boxShadow: "0 8px 32px rgba(0,0,0,.18)" } },
2337
- (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("resetTitle")),
2338
- (0, import_react3.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)", whiteSpace: "pre-line" } }, t("resetBody")),
2339
- (0, import_react3.createElement)(
2356
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("resetTitle")),
2357
+ (0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)", whiteSpace: "pre-line" } }, t("resetBody")),
2358
+ (0, import_react2.createElement)(
2340
2359
  "div",
2341
2360
  { style: { display: "flex", gap: 8, marginTop: 16 } },
2342
- (0, import_react3.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setResetOpen(false) }, t("cancel")),
2343
- (0, import_react3.createElement)("button", { style: { ...styles.primary, flex: 1, background: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: doFactoryReset }, t("resetConfirm"))
2361
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setResetOpen(false) }, t("cancel")),
2362
+ (0, import_react2.createElement)("button", { style: { ...styles.primary, flex: 1, background: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: doFactoryReset }, t("resetConfirm"))
2344
2363
  )
2345
2364
  )
2346
2365
  ) : null,
2347
2366
  // Toast:重置等操作的即时反馈(固定屏幕正中央,2.6s 自动消失)
2348
- toast ? (0, import_react3.createElement)("div", {
2367
+ toast ? (0, import_react2.createElement)("div", {
2349
2368
  style: { position: "fixed", left: "50%", top: "50%", transform: "translate(-50%, -50%)", zIndex: 10001, width: "auto", maxWidth: 280, background: "rgba(17,24,39,.92)", color: "#fff", border: "none", borderRadius: 10, padding: "10px 16px", fontSize: 13, lineHeight: 1.5, textAlign: "center", boxShadow: "0 8px 24px rgba(0,0,0,.22)" }
2350
2369
  }, toast) : null,
2351
2370
  // 局域网访问开关确认弹框(关闭/打开时弹窗提醒)
2352
- lanToggleOpen !== null ? (0, import_react3.createElement)(
2371
+ lanToggleOpen !== null ? (0, import_react2.createElement)(
2353
2372
  "div",
2354
2373
  { style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
2355
- (0, import_react3.createElement)(
2374
+ (0, import_react2.createElement)(
2356
2375
  "div",
2357
2376
  { style: { background: "var(--dsw-alias-bg-layer-1,#fff)", borderRadius: 12, maxWidth: 420, width: "100%", padding: "20px 22px", boxShadow: "0 8px 32px rgba(0,0,0,.18)" } },
2358
- (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: lanToggleOpen ? "var(--dsw-alias-brand-primary,#4f6ef7)" : "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t(lanToggleOpen ? "lanToggleTitleOn" : "lanToggleTitleOff")),
2359
- (0, import_react3.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t(lanToggleOpen ? "lanToggleBodyOn" : "lanToggleBodyOff")),
2360
- (0, import_react3.createElement)(
2377
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: lanToggleOpen ? "var(--dsw-alias-brand-primary,#4f6ef7)" : "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t(lanToggleOpen ? "lanToggleTitleOn" : "lanToggleTitleOff")),
2378
+ (0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t(lanToggleOpen ? "lanToggleBodyOn" : "lanToggleBodyOff")),
2379
+ (0, import_react2.createElement)(
2361
2380
  "div",
2362
2381
  { style: { display: "flex", gap: 8, marginTop: 16 } },
2363
- (0, import_react3.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setLanToggleOpen(null) }, t("cancel")),
2364
- (0, import_react3.createElement)("button", { style: { ...styles.primary, flex: 1 }, onClick: confirmLanToggle }, t("confirm"))
2382
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setLanToggleOpen(null) }, t("cancel")),
2383
+ (0, import_react2.createElement)("button", { style: { ...styles.primary, flex: 1 }, onClick: confirmLanToggle }, t("confirm"))
2365
2384
  )
2366
2385
  )
2367
2386
  ) : null,
2368
2387
  // 安全免责声明弹框(issue #31):每次开启公网访问前确认
2369
- disclaimerOpen ? (0, import_react3.createElement)(
2388
+ disclaimerOpen ? (0, import_react2.createElement)(
2370
2389
  "div",
2371
2390
  { style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
2372
- (0, import_react3.createElement)(
2391
+ (0, import_react2.createElement)(
2373
2392
  "div",
2374
2393
  { style: { background: "var(--dsw-alias-bg-layer-1,#fff)", borderRadius: 12, maxWidth: 420, width: "100%", padding: "20px 22px", boxShadow: "0 8px 32px rgba(0,0,0,.18)" } },
2375
- (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("disclaimerTitle")),
2376
- (0, import_react3.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t("disclaimerBody")),
2377
- (0, import_react3.createElement)(
2394
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("disclaimerTitle")),
2395
+ (0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t("disclaimerBody")),
2396
+ (0, import_react2.createElement)(
2378
2397
  "label",
2379
2398
  { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 14, fontSize: 13, cursor: "pointer" } },
2380
- (0, import_react3.createElement)("input", { type: "checkbox", checked: disclaimerChecked, onChange: (e) => setDisclaimerChecked(e.target.checked), style: { width: 16, height: 16 } }),
2399
+ (0, import_react2.createElement)("input", { type: "checkbox", checked: disclaimerChecked, onChange: (e) => setDisclaimerChecked(e.target.checked), style: { width: 16, height: 16 } }),
2381
2400
  t("disclaimerAgree")
2382
2401
  ),
2383
- (0, import_react3.createElement)(
2402
+ (0, import_react2.createElement)(
2384
2403
  "div",
2385
2404
  { style: { display: "flex", gap: 8, marginTop: 16 } },
2386
- (0, import_react3.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setDisclaimerOpen(false) }, t("cancel")),
2387
- (0, import_react3.createElement)("button", {
2405
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setDisclaimerOpen(false) }, t("cancel")),
2406
+ (0, import_react2.createElement)("button", {
2388
2407
  style: { ...styles.primary, flex: 1, opacity: disclaimerChecked ? 1 : 0.5 },
2389
2408
  disabled: !disclaimerChecked,
2390
2409
  onClick: confirmDisclaimer
2391
2410
  }, t("disclaimerAgree"))
2392
2411
  ),
2393
- !disclaimerChecked ? (0, import_react3.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("disclaimerHint")) : null
2412
+ !disclaimerChecked ? (0, import_react2.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("disclaimerHint")) : null
2394
2413
  )
2395
2414
  ) : null,
2396
2415
  // 页面最底部:反馈入口
2397
- (0, import_react3.createElement)(
2416
+ (0, import_react2.createElement)(
2398
2417
  "div",
2399
2418
  { style: { ...styles.block, textAlign: "center" } },
2400
- (0, import_react3.createElement)(
2419
+ (0, import_react2.createElement)(
2401
2420
  "a",
2402
2421
  { href: "https://github.com/shaobeichen/dsh-pocket/issues", target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", textDecoration: "none" } },
2403
2422
  t("feedback")
@@ -0,0 +1,115 @@
1
+ // dsh-web-mobile 移植(MIT,见 LICENSE.dsh-web-mobile):移动端「复制文件内容」按钮。
2
+ //
3
+ // issue #17:用户希望手机上能拿到 DSH 在会话窗口里生成的文件。但当前 dsh-web
4
+ // 的 web UI 没有任何下载能力(全部插件 bundle 里搜不到 download / blob: /
5
+ // saveAs / createObjectURL),桌面端的下载按钮在手机经隧道/局域网访问时也拿
6
+ // 不到文件内容。退而求其次——在「文件内容块」(对话里渲染为 <pre> 代码块)的
7
+ // 右上角注入一个复制按钮,把块内文本(即文件内容)写入剪贴板。
8
+ //
9
+ // 关键:只依赖稳定结构 `<pre>`(fenced code 渲染为 `<pre><code>`),不依赖任何
10
+ // hash 类名。dsh-web 每次构建都会换类名,按类名挂会随版本失效。
11
+
12
+ /** 我们注入的按钮标记(复用 mobile-nav 的 data 属性命名空间,避免与插件自身控件冲突)。 */
13
+ const BUTTON_ATTR = 'data-mobile-nav';
14
+ const BUTTON_VALUE = 'copy-file';
15
+ /** 打在代码块容器上:已注入过按钮就不再重复注入(React 重渲染时自愈)。 */
16
+ const MARKER = 'data-mobile-nav-copy';
17
+
18
+ /**
19
+ * 把文本写入剪贴板。优先用 async Clipboard API(隧道/https 下可用);非安全
20
+ * 上下文(局域网 http)下 Clipboard API 会被浏览器拒绝,回退到隐藏 textarea +
21
+ * execCommand('copy')。两者都失败返回 false。
22
+ */
23
+ export async function copyText(text: string): Promise<boolean> {
24
+ try {
25
+ if (navigator.clipboard && window.isSecureContext) {
26
+ await navigator.clipboard.writeText(text);
27
+ return true;
28
+ }
29
+ } catch {
30
+ /* 落到下面的回退路径 */
31
+ }
32
+ try {
33
+ const ta = document.createElement('textarea');
34
+ ta.value = text;
35
+ ta.setAttribute('readonly', '');
36
+ ta.style.position = 'fixed';
37
+ ta.style.top = '0';
38
+ ta.style.left = '0';
39
+ ta.style.opacity = '0';
40
+ document.body.appendChild(ta);
41
+ ta.select();
42
+ const ok = document.execCommand('copy');
43
+ ta.remove();
44
+ return ok;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /** 造一个复制按钮。点击时复制其所在代码卡里的 <pre> 文本。 */
51
+ function createCopyButton(): HTMLButtonElement {
52
+ const btn = document.createElement('button');
53
+ btn.type = 'button';
54
+ btn.setAttribute(BUTTON_ATTR, BUTTON_VALUE);
55
+ btn.textContent = '复制';
56
+ btn.setAttribute('aria-label', '复制文件内容');
57
+ btn.addEventListener('click', async (event) => {
58
+ event.preventDefault();
59
+ event.stopPropagation();
60
+ const card = btn.parentElement;
61
+ const block = card?.querySelector('pre') ?? null;
62
+ const text = block?.textContent ?? '';
63
+ const ok = await copyText(text);
64
+ const prev = btn.textContent;
65
+ btn.textContent = ok ? '已复制' : '复制失败';
66
+ btn.setAttribute('data-copied', ok ? '1' : '0');
67
+ window.setTimeout(() => {
68
+ btn.textContent = prev;
69
+ btn.removeAttribute('data-copied');
70
+ }, 1500);
71
+ });
72
+ return btn;
73
+ }
74
+
75
+ /** 给一个代码块容器注入复制按钮(幂等)。 */
76
+ function injectInto(card: HTMLElement): void {
77
+ if (card.hasAttribute(MARKER)) return;
78
+ card.setAttribute(MARKER, '1');
79
+ if (getComputedStyle(card).position === 'static') card.style.position = 'relative';
80
+ card.appendChild(createCopyButton());
81
+ }
82
+
83
+ /**
84
+ * 收集对话里需要挂复制按钮的代码块容器。候选 = 任何 `<pre>`(fenced code 与
85
+ * 工具结果/错误块都渲染为 <pre>),跳过已标记 / 空块。返回的是其「容器」
86
+ * (pre 的父元素),按钮挂在容器上、定位在块右上角。
87
+ */
88
+ export function collectCodeBlocks(root: ParentNode): HTMLElement[] {
89
+ const out: HTMLElement[] = [];
90
+ for (const pre of Array.from(root.querySelectorAll('pre'))) {
91
+ const card = pre.parentElement;
92
+ if (card === null) continue;
93
+ if (card.hasAttribute(MARKER)) continue;
94
+ if ((pre.textContent ?? '').trim().length < 1) continue;
95
+ out.push(card);
96
+ }
97
+ return out;
98
+ }
99
+
100
+ /**
101
+ * 启动注入:MutationObserver 监听全文 DOM,新出现的代码块立即挂按钮;React
102
+ * 重渲染替换了容器时旧按钮随容器消失、新容器会在下次回调里补上(自愈)。
103
+ * @returns 清理函数(断开 observer)。
104
+ */
105
+ export function startFileCopyInjection(): () => void {
106
+ const scan = (): void => {
107
+ for (const card of collectCodeBlocks(document)) injectInto(card);
108
+ };
109
+ const observer = new MutationObserver(scan);
110
+ observer.observe(document.body, { childList: true, subtree: true });
111
+ scan();
112
+ return () => {
113
+ observer.disconnect();
114
+ };
115
+ }
@@ -3,7 +3,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
3
3
  import { MobileNavToggle } from './MobileNavToggle.tsx'
4
4
  import { MobileNavOverlay } from './MobileNavOverlay.tsx'
5
5
  import { MobileDrawerFooter } from './MobileDrawerFooter.tsx'
6
- import { MobileComposerFullscreen } from './MobileComposerFullscreen.tsx'
6
+ import { startFileCopyInjection } from './fileCopy.ts'
7
7
  import { MOBILE_CSS } from './mobile.css.ts'
8
8
  import { NS, en, zh } from './locales.ts'
9
9
  import type { MobileNavKey } from './locales.ts'
@@ -263,6 +263,14 @@ export function mobileApply(ctx): void {
263
263
  }
264
264
  }, 'dsh-mobile-nav: sheet rise animation replay')
265
265
 
266
+ // 复制文件内容按钮(issue #17):dsh-web 在手机上没有可用的下载入口,于是在
267
+ // 会话里的代码/文件块右上角注入一个「复制」按钮,把块内文本(文件内容)写
268
+ // 入剪贴板。只挂窄屏——桌面端 DSH 自带复制,不需要。
269
+ ctx.effect(() => {
270
+ if (!narrow.matches) return () => {}
271
+ return startFileCopyInjection()
272
+ }, 'dsh-mobile-nav: file copy button (issue #17)')
273
+
266
274
  ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
267
275
  name: 'conversation.session.header.actions',
268
276
  id: 'mobile-nav-toggle',
@@ -273,16 +281,6 @@ export function mobileApply(ctx): void {
273
281
  }),
274
282
  }, MobileNavToggle))
275
283
 
276
- // 「⛶ 放大输入」按钮(issue #23):注册到 conversation.input.right(发送键旁)
277
- // ——桌面端由 mobile.css.ts 隐藏(min-width:1024px)。点按切换 body 上的
278
- // data-dsh-pocket-composer-fullscreen 标记,由 CSS 把 composer 卡片全屏化。
279
- ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
280
- name: 'conversation.input.right',
281
- id: 'mobile-composer-fullscreen',
282
- order: 100,
283
- locale: NS,
284
- }, MobileComposerFullscreen))
285
-
286
284
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({
287
285
  name: 'shell.overlay',
288
286
  id: 'mobile-nav-overlay',
@@ -872,6 +872,42 @@ export const MOBILE_CSS = `
872
872
  [data-phase="hero"] [class$="_stack"] {
873
873
  gap: 0 !important;
874
874
  }
875
+
876
+ /* ---------- 复制文件内容按钮(issue #17) ----------
877
+ 挂在代码/文件块容器右上角(position:relative 由 JS 在注入时补上)。
878
+ 只屏内可见:桌面端 DSH 自带复制,且本 effect 只在 narrow 下挂载,按钮
879
+ 根本不会注入;这里再兜底一层,避免任何遗漏。 */
880
+ [data-mobile-nav="copy-file"] {
881
+ position: absolute !important;
882
+ top: 6px !important;
883
+ right: 6px !important;
884
+ z-index: 6 !important;
885
+ display: inline-flex !important;
886
+ align-items: center !important;
887
+ justify-content: center !important;
888
+ height: 26px !important;
889
+ padding: 0 10px !important;
890
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)) !important;
891
+ border-radius: 8px !important;
892
+ background: var(--dsw-alias-bg-base, #ffffff) !important;
893
+ color: var(--dsw-alias-label-primary, inherit) !important;
894
+ font-family: inherit !important;
895
+ font-size: 12px !important;
896
+ line-height: 1 !important;
897
+ cursor: pointer !important;
898
+ -webkit-tap-highlight-color: transparent !important;
899
+ box-shadow: 0 1px 4px rgba(0, 0, 0, .14) !important;
900
+ }
901
+ [data-mobile-nav="copy-file"]:active {
902
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06)) !important;
903
+ }
904
+ [data-mobile-nav="copy-file"][data-copied="1"] {
905
+ color: var(--dsw-alias-state-success-primary, #1a9d54) !important;
906
+ border-color: var(--dsw-alias-state-success-primary, #1a9d54) !important;
907
+ }
908
+ [data-mobile-nav="copy-file"][data-copied="0"] {
909
+ color: var(--dsw-alias-state-danger-primary, #e5484d) !important;
910
+ }
875
911
  }
876
912
 
877
913
  /* ---------- desktop: the mobile controls must never appear ---------- */
@@ -886,50 +922,6 @@ export const MOBILE_CSS = `
886
922
  [data-mobile-nav="drawer-actions"] {
887
923
  display: none !important;
888
924
  }
889
- /* 桌面端永远不需要「⛶ 放大输入」按钮(composer 已经有完整工具行) */
890
- [data-mobile-nav="composer-fullscreen"] {
891
- display: none !important;
892
- }
893
925
  }
894
926
 
895
- /* ---------- 放大输入(issue #23) ----------
896
- 窄屏默认隐藏插件注册到 conversation.input.* slot 的 UI(仅留官方 resident
897
- chrome),点「⛶」按钮把 composer 卡片固定到全屏后所有插件 UI 恢复显示——
898
- 新插件零适配。
899
- 实现说明:当前 dsh 0.1.1 没把 input slot 渲染成 [data-slot] 包装(0.1.2+ 才有),
900
- 所以这里用「白名单」反选:mobileApply 注册的 mobile 自身节点加
901
- data-mobile-nav="composer-fullscreen" 不被隐藏;其它插件 UI 隐藏。 */
902
- @media (max-width: 1023px) {
903
- /* 默认(非全屏):隐藏 input slot 容器下的所有直接子元素(包含插件和官方)
904
- ——官方自己如果想在窄屏也露出,注 input slot 时加 data-mobile-nav-keep="1" */
905
- [data-dsh-pocket-composer-fullscreen]:not([data-dsh-pocket-composer-fullscreen="1"])
906
- [class$="_card"]:has(textarea) [data-slot^="conversation.input."] > :not([data-mobile-nav-keep]),
907
- [data-dsh-pocket-composer-fullscreen]:not([data-dsh-pocket-composer-fullscreen="1"])
908
- [class$="_card"]:has(textarea) [data-slot^="conversation.input."]:empty {
909
- display: none !important;
910
- }
911
- /* 放大按钮:常驻在 conversation.input.right slot 旁,桌面端已被上面规则隐藏 */
912
- [data-mobile-nav="composer-fullscreen"] {
913
- display: inline-flex;
914
- }
915
- /* 全屏模式:composer 卡片固定到视口,textarea 拉高,工具行可换行 */
916
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) {
917
- position: fixed !important;
918
- inset: 0 !important;
919
- z-index: 9999 !important;
920
- border-radius: 0 !important;
921
- margin: 0 !important;
922
- height: 100dvh !important;
923
- display: flex !important;
924
- flex-direction: column !important;
925
- }
926
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) textarea {
927
- flex: 1 1 auto !important;
928
- min-height: 50dvh !important;
929
- max-height: none !important;
930
- }
931
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) [data-slot^="conversation.input."] {
932
- flex-wrap: wrap !important;
933
- }
934
- }
935
927
  `
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "2.6.3"
83
+ "version": "2.7.0"
84
84
  }
@@ -1,77 +0,0 @@
1
- // 手机端「⛶ 放大输入」开关(issue #23)
2
- //
3
- // 行为:
4
- // - 点击切换 body 属性 `data-dsh-pocket-composer-fullscreen`;
5
- // - mobile.css.ts 监听这个属性,把 composer 卡片固定到全屏(textarea 拉高、工具行
6
- // 全部展开),插件 UI 注册到 conversation.input.* slot 的按钮全部恢复显示;
7
- // - 默认(无 fullscreen 属性)下,CSS 把这些 slot 全部隐藏,只保留官方 resident
8
- // chrome(权限/plan/模型/发送)。新插件只要注册到 input slot 就自动遵守此规则,
9
- // 无需逐个适配。
10
- //
11
- // 实现细节:
12
- // - 不依赖 dsh web 把 data-slot 包装暴露给选择器(0.1.1 没,0.1.2+ 才有)。我们用
13
- // `:scope > :not([data-mobile-nav-keep])` 这样的「保留」标记:所有插件如果
14
- // 想在手机未放大时也露出,加 data-mobile-nav-keep="1" 即可。默认隐藏的是
15
- // slot 容器下的所有直接子元素,**包含 dsh web 官方自己注入的**(这些是
16
- // 我们想隐藏的"插件 UI"——只要不冲突)。所以需要 dsh web 在官方自己的 UI 上
17
- // 加 `data-mobile-nav-keep` 标记——等 dsh 0.1.2+ data-slot 上线后改用那个
18
- // 更优雅(CSS 选择器变成 `[data-slot=...] > :not([data-mobile-nav-keep])`)。
19
- import { createElement as h, useEffect, useState } from 'react';
20
-
21
- const FULLSCREEN_ATTR = 'data-dsh-pocket-composer-fullscreen';
22
-
23
- function isFullscreen() {
24
- return typeof document !== 'undefined' && document.body?.getAttribute(FULLSCREEN_ATTR) === '1';
25
- }
26
-
27
- function setFullscreen(on) {
28
- if (typeof document === 'undefined') return;
29
- if (on) document.body.setAttribute(FULLSCREEN_ATTR, '1');
30
- else document.body.removeAttribute(FULLSCREEN_ATTR);
31
- }
32
-
33
- /**
34
- * 放大输入按钮:注册到 `conversation.input.right`,id `mobile-composer-fullscreen`。
35
- * 仅在窄屏(max-width:1023px)显示,桌面端由 CSS 隐藏。
36
- */
37
- export function MobileComposerFullscreen() {
38
- const [on, setOn] = useState(isFullscreen);
39
- useEffect(() => {
40
- const update = () => setOn(isFullscreen());
41
- // 与其它 effect 协调:监听任何对 body 属性的修改即可(设置项自己改也算)
42
- const observer = new MutationObserver(update);
43
- observer.observe(document.body, { attributes: true, attributeFilter: [FULLSCREEN_ATTR] });
44
- return () => observer.disconnect();
45
- }, []);
46
- return h('button', {
47
- type: 'button',
48
- 'aria-label': on ? '收起输入' : '放大输入',
49
- 'aria-pressed': on,
50
- title: on ? '收起输入' : '放大输入',
51
- 'data-mobile-nav': 'composer-fullscreen',
52
- 'data-mobile-nav-keep': '1', // 自己的按钮不该被 #23 的 slot 通用隐藏规则误伤
53
- onClick: () => {
54
- const next = !isFullscreen();
55
- setFullscreen(next);
56
- setOn(next);
57
- },
58
- style: {
59
- appearance: 'none',
60
- WebkitAppearance: 'none',
61
- border: 'none',
62
- background: 'transparent',
63
- color: 'inherit',
64
- cursor: 'pointer',
65
- padding: '0 8px',
66
- height: 32,
67
- minWidth: 32,
68
- borderRadius: 6,
69
- display: 'inline-flex',
70
- alignItems: 'center',
71
- justifyContent: 'center',
72
- font: 'inherit',
73
- fontSize: 16,
74
- lineHeight: 1,
75
- },
76
- }, on ? '⤡' : '⤢');
77
- }