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 +256 -237
- package/client/mobile/fileCopy.ts +115 -0
- package/client/mobile/mobile-apply.tsx +9 -11
- package/client/mobile/mobile.css.ts +36 -44
- package/package.json +1 -1
- package/client/mobile/MobileComposerFullscreen.tsx +0 -77
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
|
|
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/
|
|
341
|
-
var
|
|
342
|
-
var
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
|
352
|
-
|
|
353
|
-
(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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,
|
|
1783
|
-
const [busy, setBusy] = (0,
|
|
1784
|
-
const [error, setError] = (0,
|
|
1785
|
-
const [tunnelState, setTunnelState] = (0,
|
|
1786
|
-
const [restartNotice, setRestartNotice] = (0,
|
|
1787
|
-
const [updateInfo, setUpdateInfo] = (0,
|
|
1788
|
-
const [isDesktop, setIsDesktop] = (0,
|
|
1789
|
-
const [now, setNow] = (0,
|
|
1790
|
-
(0,
|
|
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,
|
|
1841
|
+
(0, import_react2.useEffect)(() => {
|
|
1823
1842
|
load();
|
|
1824
1843
|
const t2 = setInterval(load, 3e3);
|
|
1825
1844
|
return () => clearInterval(t2);
|
|
1826
1845
|
}, []);
|
|
1827
|
-
(0,
|
|
1846
|
+
(0, import_react2.useEffect)(() => {
|
|
1828
1847
|
try {
|
|
1829
1848
|
sessionStorage.removeItem("dshp-auto-reloaded");
|
|
1830
1849
|
} catch {
|
|
1831
1850
|
}
|
|
1832
1851
|
}, []);
|
|
1833
|
-
(0,
|
|
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,
|
|
1890
|
-
const [disclaimerChecked, setDisclaimerChecked] = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
2029
|
-
(0,
|
|
2030
|
-
customPin?.err ? (0,
|
|
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,
|
|
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,
|
|
2049
|
-
const toastTimer = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
2071
|
-
const qrArea = (src, url, hint) => (0,
|
|
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,
|
|
2075
|
-
(0,
|
|
2076
|
-
(0,
|
|
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,
|
|
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,
|
|
2100
|
+
(0, import_react2.createElement)(
|
|
2082
2101
|
"div",
|
|
2083
2102
|
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
|
2084
|
-
(0,
|
|
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,
|
|
2090
|
-
return (0,
|
|
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,
|
|
2112
|
+
(0, import_react2.createElement)(
|
|
2094
2113
|
"div",
|
|
2095
2114
|
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
|
2096
|
-
(0,
|
|
2115
|
+
(0, import_react2.createElement)(
|
|
2097
2116
|
"div",
|
|
2098
2117
|
null,
|
|
2099
|
-
(0,
|
|
2100
|
-
(0,
|
|
2118
|
+
(0, import_react2.createElement)("strong", null, t("title")),
|
|
2119
|
+
(0, import_react2.createElement)("div", { style: styles.muted }, t("subtitle"))
|
|
2101
2120
|
),
|
|
2102
|
-
(0,
|
|
2121
|
+
(0, import_react2.createElement)(
|
|
2103
2122
|
"div",
|
|
2104
2123
|
{ style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", textAlign: "right" } },
|
|
2105
|
-
(0,
|
|
2106
|
-
(0,
|
|
2107
|
-
(0,
|
|
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,
|
|
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,
|
|
2138
|
+
(0, import_react2.createElement)(
|
|
2120
2139
|
"div",
|
|
2121
2140
|
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
|
2122
|
-
(0,
|
|
2123
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
2151
|
+
(0, import_react2.createElement)(
|
|
2133
2152
|
"div",
|
|
2134
2153
|
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
|
2135
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
2168
|
+
(0, import_react2.createElement)(
|
|
2150
2169
|
"div",
|
|
2151
2170
|
{ style: styles.block },
|
|
2152
|
-
(0,
|
|
2171
|
+
(0, import_react2.createElement)(
|
|
2153
2172
|
"div",
|
|
2154
2173
|
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
|
|
2155
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
2170
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
2202
|
+
advOpen ? (0, import_react2.createElement)(
|
|
2184
2203
|
"div",
|
|
2185
2204
|
{ style: { marginTop: 8 } },
|
|
2186
|
-
(0,
|
|
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,
|
|
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,
|
|
2198
|
-
(status?.lanCandidates || []).map((ip) => (0,
|
|
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,
|
|
2222
|
+
) : (0, import_react2.createElement)("div", { style: styles.muted }, t("lanStarting"))
|
|
2204
2223
|
),
|
|
2205
2224
|
// 公网:标题行自带 开启/关闭 → 开启后:二维码+地址、地址模式行、访问密码行
|
|
2206
|
-
(0,
|
|
2225
|
+
(0, import_react2.createElement)(
|
|
2207
2226
|
"div",
|
|
2208
2227
|
{ style: styles.block },
|
|
2209
|
-
(0,
|
|
2228
|
+
(0, import_react2.createElement)(
|
|
2210
2229
|
"div",
|
|
2211
2230
|
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
|
|
2212
|
-
(0,
|
|
2213
|
-
tunnelUrl ? (0,
|
|
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,
|
|
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,
|
|
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,
|
|
2224
|
-
tunnelUrl ? (0,
|
|
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,
|
|
2250
|
+
(0, import_react2.createElement)(
|
|
2232
2251
|
"span",
|
|
2233
2252
|
{ style: { display: "inline-flex", gap: 6 } },
|
|
2234
|
-
(0,
|
|
2235
|
-
(0,
|
|
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,
|
|
2256
|
+
(0, import_react2.createElement)(
|
|
2238
2257
|
"div",
|
|
2239
2258
|
{ style: { marginTop: 6 } },
|
|
2240
2259
|
// 刚保存固定域名但当前连接仍是随机域名:需关闭后重新开启才生效
|
|
2241
|
-
namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? "") ? (0,
|
|
2260
|
+
namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? "") ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("namedTakeEffect")) : null,
|
|
2242
2261
|
// 固定域名:已保存摘要 + 修改入口(非编辑态)
|
|
2243
|
-
namedMode && !tunnelCfg ? (0,
|
|
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,
|
|
2248
|
-
(0,
|
|
2249
|
-
!tunnelModeView.tokenSet || !tunnelModeView.hostname ? (0,
|
|
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,
|
|
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,
|
|
2274
|
+
(0, import_react2.createElement)(
|
|
2256
2275
|
"div",
|
|
2257
2276
|
null,
|
|
2258
2277
|
t("namedHostnameLabel"),
|
|
2259
|
-
(0,
|
|
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,
|
|
2290
|
+
(0, import_react2.createElement)(
|
|
2272
2291
|
"div",
|
|
2273
2292
|
{ style: { marginTop: 6 } },
|
|
2274
2293
|
t("namedTokenLabel"),
|
|
2275
|
-
(0,
|
|
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,
|
|
2305
|
+
(0, import_react2.createElement)(
|
|
2287
2306
|
"div",
|
|
2288
2307
|
{ style: { marginTop: 6, display: "flex", gap: 8 } },
|
|
2289
|
-
(0,
|
|
2290
|
-
(0,
|
|
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,
|
|
2293
|
-
(0,
|
|
2294
|
-
tunnelCfg.err ? (0,
|
|
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,
|
|
2320
|
+
customPin?.which === "public" ? null : (0, import_react2.createElement)(
|
|
2302
2321
|
"span",
|
|
2303
2322
|
{ style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
2304
|
-
(0,
|
|
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,
|
|
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,
|
|
2312
|
-
namedMode ? (0,
|
|
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,
|
|
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,
|
|
2338
|
+
(0, import_react2.createElement)(
|
|
2320
2339
|
"div",
|
|
2321
2340
|
{ style: styles.block },
|
|
2322
|
-
(0,
|
|
2341
|
+
(0, import_react2.createElement)(
|
|
2323
2342
|
"div",
|
|
2324
2343
|
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
|
2325
|
-
(0,
|
|
2326
|
-
(0,
|
|
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,
|
|
2347
|
+
(0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("resetIntro"))
|
|
2329
2348
|
),
|
|
2330
2349
|
// 恢复出厂设置确认弹框
|
|
2331
|
-
resetOpen ? (0,
|
|
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,
|
|
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,
|
|
2338
|
-
(0,
|
|
2339
|
-
(0,
|
|
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,
|
|
2343
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
2359
|
-
(0,
|
|
2360
|
-
(0,
|
|
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,
|
|
2364
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
2376
|
-
(0,
|
|
2377
|
-
(0,
|
|
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,
|
|
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,
|
|
2402
|
+
(0, import_react2.createElement)(
|
|
2384
2403
|
"div",
|
|
2385
2404
|
{ style: { display: "flex", gap: 8, marginTop: 16 } },
|
|
2386
|
-
(0,
|
|
2387
|
-
(0,
|
|
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,
|
|
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,
|
|
2416
|
+
(0, import_react2.createElement)(
|
|
2398
2417
|
"div",
|
|
2399
2418
|
{ style: { ...styles.block, textAlign: "center" } },
|
|
2400
|
-
(0,
|
|
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 {
|
|
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
|
@@ -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
|
-
}
|