dsh-pocket 2.7.0 → 2.7.1
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 +100 -110
- package/client/mobile/fileGuard.ts +118 -0
- package/client/mobile/mobile-apply.tsx +7 -6
- package/client/mobile/mobile.css.ts +9 -34
- package/package.json +1 -1
- package/client/mobile/fileCopy.ts +0 -115
package/client/client.js
CHANGED
|
@@ -337,84 +337,99 @@ function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t
|
|
|
337
337
|
));
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
// client/mobile/
|
|
341
|
-
var
|
|
342
|
-
var
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
}
|
|
340
|
+
// client/mobile/fileGuard.ts
|
|
341
|
+
var GUARD_MSG = "\u624B\u673A\u4E0A\u65E0\u6CD5\u76F4\u63A5\u6253\u5F00\u7535\u8111\u4E0A\u7684\u6587\u4EF6";
|
|
342
|
+
var WS_LABELS = ["\u6DFB\u52A0\u5DE5\u4F5C\u533A", "\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026", "Add workspace", "Add workspace\u2026"];
|
|
343
|
+
function looksLikeFilePath(text) {
|
|
344
|
+
const t = (text ?? "").trim();
|
|
345
|
+
if (t.length < 3 || t.length > 320) return false;
|
|
346
|
+
if (/^(\/|~\/|\.\.?\/|[A-Za-z]:\\)/.test(t)) return true;
|
|
347
|
+
if (/\/[\w.\-]+\.\w{1,12}$/.test(t)) return true;
|
|
348
|
+
if (/[\w.\-]+\/[\w.\-]+\.\w{1,12}/.test(t)) return true;
|
|
349
|
+
return false;
|
|
368
350
|
}
|
|
369
|
-
function
|
|
370
|
-
|
|
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;
|
|
351
|
+
function isInsidePocket(el) {
|
|
352
|
+
return el !== null && el.closest('[data-mobile-nav="frame"]') !== null;
|
|
391
353
|
}
|
|
392
|
-
function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
354
|
+
function startFileGuard() {
|
|
355
|
+
let toastEl = null;
|
|
356
|
+
let toastTimer = null;
|
|
357
|
+
const showToast = (text) => {
|
|
358
|
+
if (toastEl === null) {
|
|
359
|
+
toastEl = document.createElement("div");
|
|
360
|
+
toastEl.setAttribute("data-mobile-nav", "file-guard-toast");
|
|
361
|
+
Object.assign(toastEl.style, {
|
|
362
|
+
position: "fixed",
|
|
363
|
+
left: "50%",
|
|
364
|
+
bottom: "64px",
|
|
365
|
+
transform: "translateX(-50%)",
|
|
366
|
+
maxWidth: "84vw",
|
|
367
|
+
zIndex: "9999",
|
|
368
|
+
padding: "10px 14px",
|
|
369
|
+
borderRadius: "10px",
|
|
370
|
+
background: "rgba(20,22,28,.92)",
|
|
371
|
+
color: "#fff",
|
|
372
|
+
fontSize: "13px",
|
|
373
|
+
lineHeight: "1.4",
|
|
374
|
+
textAlign: "center",
|
|
375
|
+
fontFamily: "inherit",
|
|
376
|
+
boxShadow: "0 4px 16px rgba(0,0,0,.28)",
|
|
377
|
+
pointerEvents: "none",
|
|
378
|
+
opacity: "0",
|
|
379
|
+
transition: "opacity .18s ease"
|
|
380
|
+
});
|
|
381
|
+
document.body.appendChild(toastEl);
|
|
382
|
+
}
|
|
383
|
+
toastEl.textContent = text;
|
|
384
|
+
requestAnimationFrame(() => {
|
|
385
|
+
if (toastEl !== null) toastEl.style.opacity = "1";
|
|
386
|
+
});
|
|
387
|
+
if (toastTimer !== null) window.clearTimeout(toastTimer);
|
|
388
|
+
toastTimer = window.setTimeout(() => {
|
|
389
|
+
if (toastEl !== null) toastEl.style.opacity = "0";
|
|
390
|
+
}, 2600);
|
|
391
|
+
};
|
|
392
|
+
const onClick = (event) => {
|
|
393
|
+
const target = event.target;
|
|
394
|
+
if (target === null || isInsidePocket(target)) return;
|
|
395
|
+
const el = target.closest("button, a");
|
|
396
|
+
if (el === null) return;
|
|
397
|
+
if (!looksLikeFilePath(el.textContent)) return;
|
|
398
|
+
event.preventDefault();
|
|
399
|
+
event.stopImmediatePropagation();
|
|
400
|
+
showToast(GUARD_MSG);
|
|
401
|
+
};
|
|
402
|
+
document.addEventListener("click", onClick, true);
|
|
403
|
+
const hideWsEntries = () => {
|
|
404
|
+
const checkOne = (node) => {
|
|
405
|
+
if (node.nodeType !== 1) return;
|
|
406
|
+
const el = node;
|
|
407
|
+
const txt = (el.getAttribute("aria-label") ?? el.textContent ?? "").trim();
|
|
408
|
+
if (WS_LABELS.includes(txt)) {
|
|
409
|
+
el.style.display = "none";
|
|
410
|
+
el.setAttribute("data-mobile-nav-hide", "add-workspace");
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const sel = '[role="menuitem"],[role="option"],li,button,a';
|
|
414
|
+
document.querySelectorAll(sel).forEach(checkOne);
|
|
415
|
+
const observer = new MutationObserver((mutations) => {
|
|
416
|
+
for (const m of mutations) {
|
|
417
|
+
m.addedNodes.forEach((n) => {
|
|
418
|
+
if (n.nodeType !== 1) return;
|
|
419
|
+
checkOne(n);
|
|
420
|
+
n.querySelectorAll?.(sel).forEach(checkOne);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
425
|
+
return () => observer.disconnect();
|
|
412
426
|
};
|
|
413
|
-
const
|
|
414
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
415
|
-
scan();
|
|
427
|
+
const disconnectWs = hideWsEntries();
|
|
416
428
|
return () => {
|
|
417
|
-
|
|
429
|
+
document.removeEventListener("click", onClick, true);
|
|
430
|
+
disconnectWs();
|
|
431
|
+
if (toastTimer !== null) window.clearTimeout(toastTimer);
|
|
432
|
+
toastEl?.remove();
|
|
418
433
|
};
|
|
419
434
|
}
|
|
420
435
|
|
|
@@ -1279,40 +1294,15 @@ var MOBILE_CSS = `
|
|
|
1279
1294
|
gap: 0 !important;
|
|
1280
1295
|
}
|
|
1281
1296
|
|
|
1282
|
-
/* ---------- \
|
|
1283
|
-
\
|
|
1284
|
-
\
|
|
1285
|
-
\
|
|
1286
|
-
[
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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;
|
|
1297
|
+
/* ---------- \u9690\u85CF\u300C\u6DFB\u52A0\u5DE5\u4F5C\u533A\u300D\u5165\u53E3\uFF08\u624B\u673A\u4E0A\u914D\u5DE5\u4F5C\u533A\u65E0\u610F\u4E49\uFF0Cissue #17 \u4FEE\u6B63\uFF09 ----------
|
|
1298
|
+
\u56FE\u6807\u6309\u94AE\u7684 aria-label \u968F\u8BED\u8A00\u53D8\u5316\uFF08zh\u300C\u6DFB\u52A0\u5DE5\u4F5C\u533A\u300D/ en\u300CAdd workspace\u300D\uFF09\uFF0C
|
|
1299
|
+
\u4E24\u79CD\u90FD\u8986\u76D6\uFF1B\u4E0B\u62C9\u83DC\u5355\u91CC\u7684\u300C\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026\u300D\u9879\u7531 fileGuard.ts \u7684 MutationObserver
|
|
1300
|
+
\u6309\u6587\u6848\u515C\u5E95\u9690\u85CF\uFF08CSS \u9009\u4E0D\u5230\u7EAF\u6587\u672C\u8282\u70B9\uFF09\u3002\u53EA\u5728\u7A84\u5C4F\u751F\u6548\u2014\u2014\u684C\u9762\u7AEF\u7167\u5E38\u4FDD\u7559\u3002 */
|
|
1301
|
+
button[aria-label="\u6DFB\u52A0\u5DE5\u4F5C\u533A"],
|
|
1302
|
+
button[aria-label="\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026"],
|
|
1303
|
+
button[aria-label="Add workspace"],
|
|
1304
|
+
button[aria-label="Add workspace\u2026"] {
|
|
1305
|
+
display: none !important;
|
|
1316
1306
|
}
|
|
1317
1307
|
}
|
|
1318
1308
|
|
|
@@ -1548,8 +1538,8 @@ function mobileApply(ctx) {
|
|
|
1548
1538
|
ctx.effect(() => {
|
|
1549
1539
|
if (!narrow.matches) return () => {
|
|
1550
1540
|
};
|
|
1551
|
-
return
|
|
1552
|
-
}, "dsh-mobile-nav: file
|
|
1541
|
+
return startFileGuard();
|
|
1542
|
+
}, "dsh-mobile-nav: file open guard + hide add-workspace (issue #17)");
|
|
1553
1543
|
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
|
|
1554
1544
|
name: "conversation.session.header.actions",
|
|
1555
1545
|
id: "mobile-nav-toggle",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// 移动端文件守卫(issue #17 修正):dsh-web 在手机上点「文件链接」会触发桌面端
|
|
2
|
+
// workspaces.openPath(open <path>),既打不开(文件在电脑上),又会抛
|
|
3
|
+
// "path open failed: ...". 这里在捕获阶段拦截这类激活(点击 / 键盘),改为弹一个
|
|
4
|
+
// 提示;并隐藏「添加工作区」入口(手机上配工作区无意义)。
|
|
5
|
+
// 移植自 dsh-web-mobile(MIT)。
|
|
6
|
+
//
|
|
7
|
+
// 识别方式:不依赖 dsh-web 的 hash 类名(每次构建都变),只认「文本像文件路径的
|
|
8
|
+
// <button>/<a>」——文件链接按钮的文案就是路径(如 lib/proxy.mjs / /Users/.../x.ts)。
|
|
9
|
+
|
|
10
|
+
/** 手机上点击文件时弹出的提示。 */
|
|
11
|
+
const GUARD_MSG = '手机上无法直接打开电脑上的文件'
|
|
12
|
+
/** 「添加工作区」入口的文案(随语言变化),两种都覆盖。 */
|
|
13
|
+
const WS_LABELS = ['添加工作区', '添加工作区…', 'Add workspace', 'Add workspace…']
|
|
14
|
+
|
|
15
|
+
/** 文本是否像文件路径:绝对路径 / 相对路径 / 带扩展名的目录路径。 */
|
|
16
|
+
function looksLikeFilePath(text: string | null): boolean {
|
|
17
|
+
const t = (text ?? '').trim()
|
|
18
|
+
if (t.length < 3 || t.length > 320) return false
|
|
19
|
+
if (/^(\/|~\/|\.\.?\/|[A-Za-z]:\\)/.test(t)) return true
|
|
20
|
+
if (/\/[\w.\-]+\.\w{1,12}$/.test(t)) return true
|
|
21
|
+
if (/[\w.\-]+\/[\w.\-]+\.\w{1,12}/.test(t)) return true
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 节点是否落在 dsh-pocket 自身面板内(不拦截面板内的交互)。 */
|
|
26
|
+
function isInsidePocket(el: Element | null): boolean {
|
|
27
|
+
return el !== null && el.closest('[data-mobile-nav="frame"]') !== null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function startFileGuard(): () => void {
|
|
31
|
+
// 轻量 toast:自包含,不依赖 dsh-pocket 面板的 React 状态。
|
|
32
|
+
let toastEl: HTMLElement | null = null
|
|
33
|
+
let toastTimer: number | null = null
|
|
34
|
+
const showToast = (text: string): void => {
|
|
35
|
+
if (toastEl === null) {
|
|
36
|
+
toastEl = document.createElement('div')
|
|
37
|
+
toastEl.setAttribute('data-mobile-nav', 'file-guard-toast')
|
|
38
|
+
Object.assign(toastEl.style, {
|
|
39
|
+
position: 'fixed',
|
|
40
|
+
left: '50%',
|
|
41
|
+
bottom: '64px',
|
|
42
|
+
transform: 'translateX(-50%)',
|
|
43
|
+
maxWidth: '84vw',
|
|
44
|
+
zIndex: '9999',
|
|
45
|
+
padding: '10px 14px',
|
|
46
|
+
borderRadius: '10px',
|
|
47
|
+
background: 'rgba(20,22,28,.92)',
|
|
48
|
+
color: '#fff',
|
|
49
|
+
fontSize: '13px',
|
|
50
|
+
lineHeight: '1.4',
|
|
51
|
+
textAlign: 'center',
|
|
52
|
+
fontFamily: 'inherit',
|
|
53
|
+
boxShadow: '0 4px 16px rgba(0,0,0,.28)',
|
|
54
|
+
pointerEvents: 'none',
|
|
55
|
+
opacity: '0',
|
|
56
|
+
transition: 'opacity .18s ease',
|
|
57
|
+
} as CSSStyleDeclaration)
|
|
58
|
+
document.body.appendChild(toastEl)
|
|
59
|
+
}
|
|
60
|
+
toastEl.textContent = text
|
|
61
|
+
requestAnimationFrame(() => {
|
|
62
|
+
if (toastEl !== null) toastEl.style.opacity = '1'
|
|
63
|
+
})
|
|
64
|
+
if (toastTimer !== null) window.clearTimeout(toastTimer)
|
|
65
|
+
toastTimer = window.setTimeout(() => {
|
|
66
|
+
if (toastEl !== null) toastEl.style.opacity = '0'
|
|
67
|
+
}, 2600)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 捕获阶段拦截文件链接的激活。按钮的键盘激活(Enter/Space)会派发 click,
|
|
71
|
+
// 因此只拦 click 即可同时覆盖鼠标与键盘,避免重复处理。
|
|
72
|
+
const onClick = (event: MouseEvent): void => {
|
|
73
|
+
const target = event.target as HTMLElement | null
|
|
74
|
+
if (target === null || isInsidePocket(target)) return
|
|
75
|
+
const el = target.closest('button, a') as HTMLElement | null
|
|
76
|
+
if (el === null) return
|
|
77
|
+
if (!looksLikeFilePath(el.textContent)) return
|
|
78
|
+
event.preventDefault()
|
|
79
|
+
event.stopImmediatePropagation()
|
|
80
|
+
showToast(GUARD_MSG)
|
|
81
|
+
}
|
|
82
|
+
document.addEventListener('click', onClick, true)
|
|
83
|
+
|
|
84
|
+
// 隐藏「添加工作区」入口:图标按钮由 mobile.css.ts 按 aria-label 隐藏;
|
|
85
|
+
// 下拉菜单里的文本项 CSS 选不到,这里按文案兜底(只在新增节点时检查,省开销)。
|
|
86
|
+
const hideWsEntries = (): (() => void) => {
|
|
87
|
+
const checkOne = (node: Node): void => {
|
|
88
|
+
if (node.nodeType !== 1) return
|
|
89
|
+
const el = node as HTMLElement
|
|
90
|
+
const txt = (el.getAttribute('aria-label') ?? el.textContent ?? '').trim()
|
|
91
|
+
if (WS_LABELS.includes(txt)) {
|
|
92
|
+
el.style.display = 'none'
|
|
93
|
+
el.setAttribute('data-mobile-nav-hide', 'add-workspace')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const sel = '[role="menuitem"],[role="option"],li,button,a'
|
|
97
|
+
document.querySelectorAll(sel).forEach(checkOne)
|
|
98
|
+
const observer = new MutationObserver((mutations) => {
|
|
99
|
+
for (const m of mutations) {
|
|
100
|
+
m.addedNodes.forEach((n) => {
|
|
101
|
+
if (n.nodeType !== 1) return
|
|
102
|
+
checkOne(n)
|
|
103
|
+
;(n as HTMLElement).querySelectorAll?.(sel).forEach(checkOne)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
108
|
+
return () => observer.disconnect()
|
|
109
|
+
}
|
|
110
|
+
const disconnectWs = hideWsEntries()
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
document.removeEventListener('click', onClick, true)
|
|
114
|
+
disconnectWs()
|
|
115
|
+
if (toastTimer !== null) window.clearTimeout(toastTimer)
|
|
116
|
+
toastEl?.remove()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -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 { startFileGuard } from './fileGuard.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,13 +263,14 @@ export function mobileApply(ctx): void {
|
|
|
263
263
|
}
|
|
264
264
|
}, 'dsh-mobile-nav: sheet rise animation replay')
|
|
265
265
|
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
266
|
+
// 移动端文件守卫(issue #17 修正):手机上点 dsh-web 渲染的文件链接会触发桌面
|
|
267
|
+
// 端 workspaces.openPath(open ...) —— 既打不开(路径在电脑上),又会抛
|
|
268
|
+
// "path open failed"。这里在捕获阶段拦截这类点击 / 键盘激活,改为弹一个提示,
|
|
269
|
+
// 并隐藏「添加工作区」入口(手机上配工作区无意义)。只挂窄屏。
|
|
269
270
|
ctx.effect(() => {
|
|
270
271
|
if (!narrow.matches) return () => {}
|
|
271
|
-
return
|
|
272
|
-
}, 'dsh-mobile-nav: file
|
|
272
|
+
return startFileGuard()
|
|
273
|
+
}, 'dsh-mobile-nav: file open guard + hide add-workspace (issue #17)')
|
|
273
274
|
|
|
274
275
|
ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
|
|
275
276
|
name: 'conversation.session.header.actions',
|
|
@@ -873,40 +873,15 @@ export const MOBILE_CSS = `
|
|
|
873
873
|
gap: 0 !important;
|
|
874
874
|
}
|
|
875
875
|
|
|
876
|
-
/* ----------
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
[
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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;
|
|
876
|
+
/* ---------- 隐藏「添加工作区」入口(手机上配工作区无意义,issue #17 修正) ----------
|
|
877
|
+
图标按钮的 aria-label 随语言变化(zh「添加工作区」/ en「Add workspace」),
|
|
878
|
+
两种都覆盖;下拉菜单里的「添加工作区…」项由 fileGuard.ts 的 MutationObserver
|
|
879
|
+
按文案兜底隐藏(CSS 选不到纯文本节点)。只在窄屏生效——桌面端照常保留。 */
|
|
880
|
+
button[aria-label="添加工作区"],
|
|
881
|
+
button[aria-label="添加工作区…"],
|
|
882
|
+
button[aria-label="Add workspace"],
|
|
883
|
+
button[aria-label="Add workspace…"] {
|
|
884
|
+
display: none !important;
|
|
910
885
|
}
|
|
911
886
|
}
|
|
912
887
|
|
package/package.json
CHANGED
|
@@ -1,115 +0,0 @@
|
|
|
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
|
-
}
|