dsh-remote-bridge 2.2.4 → 2.3.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/README.en.md +30 -13
- package/README.md +34 -22
- package/docs/api/asyncapi.yaml +1 -1
- package/docs/api/openapi.yaml +2 -2
- package/kotlin-sdk/build.gradle.kts +1 -1
- package/lib/client.cjs +960 -280
- package/lib/index.d.ts +133 -2
- package/lib/index.js +498 -45
- package/package.json +1 -1
package/lib/client.cjs
CHANGED
|
@@ -164,6 +164,22 @@ window.__ModuleLoader__.load({
|
|
|
164
164
|
async audit() {
|
|
165
165
|
return this.manageJson("/audit?limit=200");
|
|
166
166
|
}
|
|
167
|
+
/** 识图路径:当前值 + 出厂默认(只读)+ 兜底目录(只读)。 */
|
|
168
|
+
async imageSettings() {
|
|
169
|
+
return this.manageJson("/image");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 改「识图路径」的当前值。
|
|
173
|
+
*
|
|
174
|
+
* 改动立即生效:宿主的交接写盘与只读图片路由都通过 getter 现读这份设置,
|
|
175
|
+
* 不需要重启 dsh,也不需要重装插件。
|
|
176
|
+
*/
|
|
177
|
+
async saveImageSubdir(subdir) {
|
|
178
|
+
return this.manageJson("/image", {
|
|
179
|
+
method: "PUT",
|
|
180
|
+
body: JSON.stringify({ subdir })
|
|
181
|
+
});
|
|
182
|
+
}
|
|
167
183
|
async json(path, init = {}) {
|
|
168
184
|
const response = await this.fetch(path, init);
|
|
169
185
|
await ensureOk(response);
|
|
@@ -205,6 +221,299 @@ window.__ModuleLoader__.load({
|
|
|
205
221
|
throw new WorkspaceApiError(response.status, code, message);
|
|
206
222
|
}
|
|
207
223
|
//#endregion
|
|
224
|
+
//#region src/client/handoff.tsx
|
|
225
|
+
/**
|
|
226
|
+
* 浏览器半边:把**图片交接消息**里的那个地址渲染成图片本身。
|
|
227
|
+
*
|
|
228
|
+
* 这半边是从 `dsh-image-paths` 0.3.1 并过来的,接的是同一个契约:纯文本模型收到图片时,
|
|
229
|
+
* remote-bridge 会在用户消息里追加两行 ——
|
|
230
|
+
*
|
|
231
|
+
* 🖼️ 图片 1「shot.png」(当前模型看不到图,已存到 .dsh-images/<sha>.png;要看图,请用识图工具打开下面那行的路径)
|
|
232
|
+
* [Image: source: /absolute/path/.dsh-images/<sha>.png]
|
|
233
|
+
*
|
|
234
|
+
* 本模块只读第一行里那个地址,把它变成一张真的图。第二行是给模型读的机器契约,同一张图
|
|
235
|
+
* 不再重复出第二张;而且只扫**用户消息**,上下文注入、助手正文、工具输出里引用的路径
|
|
236
|
+
* 一律不碰 —— 否则对话里到处都是 `.dsh-images/…` 的例子,每个都会被渲染成图。
|
|
237
|
+
*
|
|
238
|
+
* 取图走宿主那条只读路由 `/plugin/dsh-remote-bridge/raw?sessionId=…&name=…`:
|
|
239
|
+
* 请求里**只有会话与裸文件名**,目录由宿主按会话工作目录推出,客户端连「读哪个目录」
|
|
240
|
+
* 都递不进去。这条路是给 `<img>` 用的 —— 浏览器不会给图片请求加 Authorization 头,
|
|
241
|
+
* 所以它独立于版本化 REST,按回环 + 同源 + 文件名白名单 + 字节自证守门。
|
|
242
|
+
*
|
|
243
|
+
* 座位选的是 Turn 尾部(`conversation.chat.turnTail`):Chat 节点座位会把这个 kind 收进
|
|
244
|
+
* 「过程分组」,standard 模式下折叠起来 —— DOM 里有、肉眼看不着。Turn 尾在独立成行的
|
|
245
|
+
* 白名单内,任何工作详情模式下都看得见。
|
|
246
|
+
*/
|
|
247
|
+
/** 本插件在 Chat store 里占的节点 kind。 */
|
|
248
|
+
const KIND = "dsh-remote-bridge-handoff-gallery";
|
|
249
|
+
/** 宿主的只读图片路由。 */
|
|
250
|
+
const ROUTE = "/plugin/dsh-remote-bridge/raw";
|
|
251
|
+
/** 每轮最多显示几张。 */
|
|
252
|
+
const MAX_IMAGES = 8;
|
|
253
|
+
/** 长过这个的路径不值得去解析。 */
|
|
254
|
+
const MAX_PATH_CHARS = 4096;
|
|
255
|
+
/** 图片扩展名:与宿主交接写盘的白名单一致。 */
|
|
256
|
+
const IMAGE_EXTENSION = /\.(?:png|jpe?g|webp|gif|heic|heif|bmp|avif)$/i;
|
|
257
|
+
/**
|
|
258
|
+
* 交接行的行首标记:中英两种文案都以它开头。
|
|
259
|
+
*
|
|
260
|
+
* 判据选「行首标记」而不是「句子里有 已存到」——后者会把用户自己写的句子也算进来,
|
|
261
|
+
* 而 dsh-companion 摘除这一行用的也是同一个标记,两边口径一致。
|
|
262
|
+
*/
|
|
263
|
+
const HANDOFF_LINE = /^🖼️\s*(?:图片|Image)\s+\d+/i;
|
|
264
|
+
/**
|
|
265
|
+
* 从交接行里抠出图片地址。
|
|
266
|
+
*
|
|
267
|
+
* 允许「已存到 / 已保存到 / saved to」后面隔一段散文(历史文案里有「工作目录」这种词),
|
|
268
|
+
* 所以是「点心号 + 非贪婪」而不是紧邻匹配。取出的是消息里**原样**的那条路径,
|
|
269
|
+
* 相对(`.dsh-images/x.png`)与绝对(兜底目录那版)都认。
|
|
270
|
+
*/
|
|
271
|
+
const HANDOFF_PATH = /(?:已(?:保存|存)到|saved to)[^;;\n]*?([A-Za-z0-9_./@~+-][^\s`"'()[\]{}<>|,;:?,。;:、!?]*?\.(?:png|jpe?g|webp|gif|heic|heif|bmp|avif))/i;
|
|
272
|
+
/** 路径里的文件名(路由只收裸文件名)。 */
|
|
273
|
+
function basenameOf(path) {
|
|
274
|
+
const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
275
|
+
return cut === -1 ? path : path.slice(cut + 1);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 读出一段文本里的交接图片。
|
|
279
|
+
* @param text - 消息的文本内容(已按块拼接)。
|
|
280
|
+
* @returns 单个 `{ path, label }`,不是交接消息就是 null。
|
|
281
|
+
*/
|
|
282
|
+
function handoffMention(text) {
|
|
283
|
+
if (typeof text !== "string" || text === "") return null;
|
|
284
|
+
for (const line of text.split("\n")) {
|
|
285
|
+
if (!HANDOFF_LINE.test(line.trim())) continue;
|
|
286
|
+
const match = HANDOFF_PATH.exec(line);
|
|
287
|
+
if (match === null) continue;
|
|
288
|
+
const path = match[1] ?? "";
|
|
289
|
+
if (path.length === 0 || path.length > MAX_PATH_CHARS) continue;
|
|
290
|
+
if (!IMAGE_EXTENSION.test(path)) continue;
|
|
291
|
+
return {
|
|
292
|
+
path,
|
|
293
|
+
label: basenameOf(path)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
/** 把一条消息的内容块拼成文本。 */
|
|
299
|
+
function contentText(content) {
|
|
300
|
+
if (!Array.isArray(content)) return "";
|
|
301
|
+
const parts = [];
|
|
302
|
+
for (const block of content) {
|
|
303
|
+
if (block === null || typeof block !== "object") continue;
|
|
304
|
+
const candidate = block;
|
|
305
|
+
if (candidate.type === "text" && typeof candidate.text === "string") parts.push(candidate.text);
|
|
306
|
+
}
|
|
307
|
+
return parts.join("\n");
|
|
308
|
+
}
|
|
309
|
+
/** 一条消息交接出去的那张图,没有就是空数组。 */
|
|
310
|
+
function itemsOfMessage(payload) {
|
|
311
|
+
if (payload === null || typeof payload !== "object") return [];
|
|
312
|
+
const item = handoffMention(contentText(payload.content));
|
|
313
|
+
return item === null ? [] : [item];
|
|
314
|
+
}
|
|
315
|
+
const WRAP_STYLE = {
|
|
316
|
+
display: "flex",
|
|
317
|
+
flexWrap: "wrap",
|
|
318
|
+
alignItems: "flex-start",
|
|
319
|
+
gap: 8,
|
|
320
|
+
margin: "8px 0 4px"
|
|
321
|
+
};
|
|
322
|
+
const ITEM_STYLE = {
|
|
323
|
+
display: "flex",
|
|
324
|
+
flexDirection: "column",
|
|
325
|
+
gap: 2,
|
|
326
|
+
maxWidth: "100%"
|
|
327
|
+
};
|
|
328
|
+
const THUMB_STYLE = {
|
|
329
|
+
display: "block",
|
|
330
|
+
maxHeight: 180,
|
|
331
|
+
maxWidth: "100%",
|
|
332
|
+
borderRadius: 8,
|
|
333
|
+
border: "1px solid rgba(127,127,127,0.28)",
|
|
334
|
+
background: "rgba(127,127,127,0.06)",
|
|
335
|
+
cursor: "zoom-in"
|
|
336
|
+
};
|
|
337
|
+
const CAPTION_STYLE = {
|
|
338
|
+
display: "block",
|
|
339
|
+
maxWidth: 180,
|
|
340
|
+
fontFamily: "inherit",
|
|
341
|
+
fontSize: 11,
|
|
342
|
+
lineHeight: 1.4,
|
|
343
|
+
color: "rgba(127,127,127,0.95)",
|
|
344
|
+
overflow: "hidden",
|
|
345
|
+
textOverflow: "ellipsis",
|
|
346
|
+
whiteSpace: "nowrap"
|
|
347
|
+
};
|
|
348
|
+
const OVERLAY_STYLE = {
|
|
349
|
+
position: "fixed",
|
|
350
|
+
inset: 0,
|
|
351
|
+
zIndex: 1e3,
|
|
352
|
+
display: "flex",
|
|
353
|
+
alignItems: "center",
|
|
354
|
+
justifyContent: "center",
|
|
355
|
+
background: "rgba(0,0,0,0.72)",
|
|
356
|
+
cursor: "zoom-out"
|
|
357
|
+
};
|
|
358
|
+
const FULL_STYLE = {
|
|
359
|
+
maxWidth: "92vw",
|
|
360
|
+
maxHeight: "92vh",
|
|
361
|
+
borderRadius: 6,
|
|
362
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.5)"
|
|
363
|
+
};
|
|
364
|
+
/** 某条地址在这条路由上的 URL。 */
|
|
365
|
+
function imageUrl(path, sessionId) {
|
|
366
|
+
return `${ROUTE}?sessionId=${encodeURIComponent(sessionId)}&name=${encodeURIComponent(basenameOf(path))}`;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* 一张图。取不到的(文件已删、会话读不出、格式不符)自己消失,而不是在轮末留一个碎图标。
|
|
370
|
+
*/
|
|
371
|
+
function HandoffImage(props) {
|
|
372
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
373
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
374
|
+
(0, react.useEffect)(() => {
|
|
375
|
+
if (!open) return void 0;
|
|
376
|
+
const onKey = (event) => {
|
|
377
|
+
if (event.key === "Escape") setOpen(false);
|
|
378
|
+
};
|
|
379
|
+
document.addEventListener("keydown", onKey);
|
|
380
|
+
return () => {
|
|
381
|
+
document.removeEventListener("keydown", onKey);
|
|
382
|
+
};
|
|
383
|
+
}, [open]);
|
|
384
|
+
if (failed) return null;
|
|
385
|
+
const url = imageUrl(props.item.path, props.sessionId);
|
|
386
|
+
const thumb = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
387
|
+
src: url,
|
|
388
|
+
alt: props.item.label,
|
|
389
|
+
title: props.item.path,
|
|
390
|
+
loading: "lazy",
|
|
391
|
+
decoding: "async",
|
|
392
|
+
referrerPolicy: "no-referrer",
|
|
393
|
+
style: THUMB_STYLE,
|
|
394
|
+
onError: () => setFailed(true),
|
|
395
|
+
onClick: () => setOpen(true)
|
|
396
|
+
});
|
|
397
|
+
const framed = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
398
|
+
className: "daw-handoff-item",
|
|
399
|
+
style: ITEM_STYLE,
|
|
400
|
+
children: [thumb, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
401
|
+
className: "daw-handoff-caption",
|
|
402
|
+
title: props.item.path,
|
|
403
|
+
style: CAPTION_STYLE,
|
|
404
|
+
children: props.item.path
|
|
405
|
+
})]
|
|
406
|
+
});
|
|
407
|
+
if (!open) return framed;
|
|
408
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [framed, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
409
|
+
style: OVERLAY_STYLE,
|
|
410
|
+
role: "dialog",
|
|
411
|
+
"aria-label": props.item.path,
|
|
412
|
+
onClick: () => setOpen(false),
|
|
413
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
414
|
+
src: url,
|
|
415
|
+
alt: props.item.label,
|
|
416
|
+
decoding: "async",
|
|
417
|
+
referrerPolicy: "no-referrer",
|
|
418
|
+
style: FULL_STYLE
|
|
419
|
+
})
|
|
420
|
+
})] });
|
|
421
|
+
}
|
|
422
|
+
/** 把本轮各节点里交接出去的图收成一串(按出现顺序去重)。 */
|
|
423
|
+
function collectTurnItems(nodes, locations, turn) {
|
|
424
|
+
if (nodes === void 0 || locations === void 0 || turn === void 0 || turn === null) return [];
|
|
425
|
+
let keys;
|
|
426
|
+
try {
|
|
427
|
+
keys = locations.getTurn(turn.turn);
|
|
428
|
+
} catch {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
if (!Array.isArray(keys)) return [];
|
|
432
|
+
const seen = /* @__PURE__ */ new Set();
|
|
433
|
+
const items = [];
|
|
434
|
+
for (const key of keys) {
|
|
435
|
+
const node = nodes.get(key);
|
|
436
|
+
if (node === void 0 || node === null || node.kind !== KIND) continue;
|
|
437
|
+
const list = node.data?.items;
|
|
438
|
+
if (!Array.isArray(list)) continue;
|
|
439
|
+
for (const item of list) {
|
|
440
|
+
if (item === null || typeof item !== "object" || typeof item.path !== "string") continue;
|
|
441
|
+
if (seen.has(item.path)) continue;
|
|
442
|
+
seen.add(item.path);
|
|
443
|
+
items.push({
|
|
444
|
+
path: item.path,
|
|
445
|
+
label: typeof item.label === "string" ? item.label : basenameOf(item.path)
|
|
446
|
+
});
|
|
447
|
+
if (items.length >= MAX_IMAGES) return items;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return items;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Turn 尾部的图集:本轮每条交接消息各自那张图。
|
|
454
|
+
*
|
|
455
|
+
* 没有会话 id 就什么都不显示 —— 路由需要它来定位目录,猜一个只会 404。
|
|
456
|
+
*/
|
|
457
|
+
function HandoffTurnTail(props) {
|
|
458
|
+
const items = collectTurnItems(props.useChat((snapshot) => snapshot.nodes), props.useChat((snapshot) => snapshot.locations), props.turn);
|
|
459
|
+
const sessionId = props.sessionId;
|
|
460
|
+
if (items.length === 0) return null;
|
|
461
|
+
if (typeof sessionId !== "string" || sessionId === "") return null;
|
|
462
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
463
|
+
className: "daw-handoff-gallery",
|
|
464
|
+
style: WRAP_STYLE,
|
|
465
|
+
children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HandoffImage, {
|
|
466
|
+
item,
|
|
467
|
+
sessionId
|
|
468
|
+
}, item.path))
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* 一条「交接消息 → 一个图集节点」的 Definition。
|
|
473
|
+
*
|
|
474
|
+
* 只匹配 `user/message`:交接说明属于读者自己发的那条消息。助手正文、上下文注入、
|
|
475
|
+
* 工具输出里到处都是引用 `.dsh-images/…` 的文本,读它们会把别人的话切成碎图。
|
|
476
|
+
*
|
|
477
|
+
* 节点自身**不注册渲染器**(Turn 尾负责显示),所以过程分组里不会多出一行、
|
|
478
|
+
* verbose 模式下也不会重复一次。
|
|
479
|
+
*/
|
|
480
|
+
const handoffDefinition = {
|
|
481
|
+
kind: KIND,
|
|
482
|
+
target: "chat",
|
|
483
|
+
match(event) {
|
|
484
|
+
if (event === null || typeof event !== "object" || event.surfaceOp !== "append") return null;
|
|
485
|
+
if (event.type !== "user/message") return null;
|
|
486
|
+
return {
|
|
487
|
+
id: `m:${String(event.seq)}`,
|
|
488
|
+
role: "start"
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
start(_context, match) {
|
|
492
|
+
return {
|
|
493
|
+
items: itemsOfMessage(match.event.data),
|
|
494
|
+
anchorSeq: match.event.seq
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
update(context) {
|
|
498
|
+
return context.state;
|
|
499
|
+
},
|
|
500
|
+
buildViewNode(context) {
|
|
501
|
+
const state = context.state;
|
|
502
|
+
if (state === void 0 || !Array.isArray(state.items) || state.items.length === 0) return null;
|
|
503
|
+
const start = context.start;
|
|
504
|
+
return {
|
|
505
|
+
key: context.key,
|
|
506
|
+
kind: KIND,
|
|
507
|
+
id: context.id,
|
|
508
|
+
target: "chat",
|
|
509
|
+
anchorSeq: state.anchorSeq,
|
|
510
|
+
location: start !== void 0 && start !== null ? start.location : { kind: "unresolved" },
|
|
511
|
+
visibility: "visible",
|
|
512
|
+
data: { items: state.items }
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
//#endregion
|
|
208
517
|
//#region node_modules/.pnpm/lucide-react@0.468.0_react@18.3.1/node_modules/lucide-react/dist/cjs/lucide-react.js
|
|
209
518
|
/**
|
|
210
519
|
* @license lucide-react v0.468.0 - ISC
|
|
@@ -15024,7 +15333,7 @@ window.__ModuleLoader__.load({
|
|
|
15024
15333
|
key: "1wpmix"
|
|
15025
15334
|
}]
|
|
15026
15335
|
]);
|
|
15027
|
-
createLucideIcon("Image", [
|
|
15336
|
+
const Image = createLucideIcon("Image", [
|
|
15028
15337
|
["rect", {
|
|
15029
15338
|
width: "18",
|
|
15030
15339
|
height: "18",
|
|
@@ -29728,6 +30037,7 @@ window.__ModuleLoader__.load({
|
|
|
29728
30037
|
exports.Download = Download;
|
|
29729
30038
|
exports.File = File;
|
|
29730
30039
|
exports.HardDrive = HardDrive;
|
|
30040
|
+
exports.Image = Image;
|
|
29731
30041
|
exports.Smartphone = Smartphone;
|
|
29732
30042
|
exports.Trash2 = Trash2;
|
|
29733
30043
|
exports.Upload = Upload;
|
|
@@ -29794,11 +30104,45 @@ window.__ModuleLoader__.load({
|
|
|
29794
30104
|
devicesTab: "设备",
|
|
29795
30105
|
trashTab: "回收站",
|
|
29796
30106
|
auditTab: "审计",
|
|
30107
|
+
imageTab: "识图",
|
|
30108
|
+
rootsTabDesc: "授权手机访问这台机器上的哪些目录。新增、命名、移除都在这里。",
|
|
30109
|
+
remoteTabDesc: "绑定 IP 与端口、开关远程访问、生成配对码与配置文本。",
|
|
30110
|
+
devicesTabDesc: "查看已配对的手机,修改它们的权限范围与可访问目录,或吊销设备。",
|
|
30111
|
+
imageTabDesc: "设置图片交给识图工具的落盘目录名。",
|
|
30112
|
+
trashTabDesc: "经手机端删除的文件留在这里,可恢复或彻底清理。",
|
|
30113
|
+
auditTabDesc: "最近的操作记录:谁、在什么时间、动了哪个路径。",
|
|
30114
|
+
appDownloadTabDesc: "手机客户端的下载二维码与地址。",
|
|
30115
|
+
imageTitle: "识图路径",
|
|
30116
|
+
imageHint: "纯文本模型收到图片时,图片会落在这个目录里,消息里只留路径给识图工具读。这里改的就是那个目录。",
|
|
30117
|
+
imageSubdir: "识图路径(目录名)",
|
|
30118
|
+
imageSubdirHint: "相对于每个会话自己的工作目录。只填单段目录名:字母、数字、点、下划线、连字符(1-64 个字符),不能用 “.” 或 “..”。",
|
|
30119
|
+
imageDefault: "当前默认(不能改)",
|
|
30120
|
+
imageDefaultHint: "出厂默认值,只读展示:历史交接消息与识图工具都按它写过路径,所以它不跟着上面的设置走。",
|
|
30121
|
+
imageFallback: "兜底目录(只读)",
|
|
30122
|
+
imageFallbackHint: "解析不出会话工作目录时(冷会话,或会话目录不在任何授权根下)才会用到的落点。",
|
|
30123
|
+
imageExample: "实际落点:<会话工作目录>/{subdir}/<内容哈希>.<扩展名>",
|
|
30124
|
+
imageSaved: "识图路径已保存",
|
|
29797
30125
|
authorizedRoots: "授权根目录",
|
|
29798
30126
|
absolutePath: "绝对目录路径",
|
|
29799
30127
|
displayLabel: "显示名称",
|
|
29800
30128
|
addRoot: "添加根目录",
|
|
29801
30129
|
removeRoot: "移除根目录",
|
|
30130
|
+
builtinRootBadge: "默认",
|
|
30131
|
+
builtinRootHint: "默认目录 · 不可删除",
|
|
30132
|
+
builtinRootNotice: "新加的根目录默认不发给已配对的手机。需要的话,到「设备」里给对应手机勾上这个目录的访问权。",
|
|
30133
|
+
errorRootBuiltin: "这是 DSH 当前的工作目录,不能删除。你仍然可以在上面添加别的根目录。",
|
|
30134
|
+
scopeChatRead: "读聊天",
|
|
30135
|
+
scopeChatWrite: "发聊天",
|
|
30136
|
+
scopeFilesRead: "读文件",
|
|
30137
|
+
scopeFilesWrite: "写文件",
|
|
30138
|
+
scopeFilesDelete: "删文件",
|
|
30139
|
+
scopeSettingsRead: "读设置",
|
|
30140
|
+
scopeSettingsWrite: "改设置",
|
|
30141
|
+
noScopes: "没有任何权限",
|
|
30142
|
+
scopeGrantNotice: "勾选要授予的权限,然后点「启用并创建配对」。APP 输入配对码后,这些权限会跟着设备一起生效。",
|
|
30143
|
+
scopeGrantNoticeEmpty: "还没选任何权限。至少勾一项,否则生成的访问码不会带上任何权限。",
|
|
30144
|
+
scopeGrantNoticeExisting: "改这里只影响**新生成**的配对码。已在用的设备要改权限,请到「设备」里点对应手机的「权限」。",
|
|
30145
|
+
configTextScopeNote: "配置文本固定使用最小权限(不含删除文件、修改设置),不跟随上面勾选 —— 它是发给别人扫的。要完整权限请改用配对码。",
|
|
29802
30146
|
remoteAccess: "远程访问",
|
|
29803
30147
|
bindIp: "绑定 IP",
|
|
29804
30148
|
bindPort: "端口",
|
|
@@ -29921,11 +30265,45 @@ window.__ModuleLoader__.load({
|
|
|
29921
30265
|
devicesTab: "Devices",
|
|
29922
30266
|
trashTab: "Trash",
|
|
29923
30267
|
auditTab: "Audit",
|
|
30268
|
+
imageTab: "Image reading",
|
|
30269
|
+
rootsTabDesc: "Which directories on this machine phones may reach. Add, rename and remove them here.",
|
|
30270
|
+
remoteTabDesc: "Bind address and port, switch remote access on or off, mint pairing codes and config texts.",
|
|
30271
|
+
devicesTabDesc: "Paired phones, their permission scopes and reachable roots — or revoke them outright.",
|
|
30272
|
+
imageTabDesc: "Choose the directory name where images land for the image-reading tool.",
|
|
30273
|
+
trashTabDesc: "Files deleted from a phone land here. Restore them, or purge them for good.",
|
|
30274
|
+
auditTabDesc: "Recent activity: who, when, and which path was touched.",
|
|
30275
|
+
appDownloadTabDesc: "QR codes and addresses for the phone app download.",
|
|
30276
|
+
imageTitle: "Image-reading path",
|
|
30277
|
+
imageHint: "When a text-only model receives a picture, the file lands in this directory and the message keeps only its path for the image-reading tool. This setting is that directory.",
|
|
30278
|
+
imageSubdir: "Image path (directory name)",
|
|
30279
|
+
imageSubdirHint: "Relative to each session’s own working directory. A single directory name only: letters, digits, dots, underscores, hyphens (1-64 characters); \".\" and \"..\" are refused.",
|
|
30280
|
+
imageDefault: "Current default (read-only)",
|
|
30281
|
+
imageDefaultHint: "The factory default, shown read-only: past handoff messages and image-reading tools wrote their paths against it, so it does not follow the setting above.",
|
|
30282
|
+
imageFallback: "Fallback directory (read-only)",
|
|
30283
|
+
imageFallbackHint: "Used only when the session working directory cannot be resolved (a cold session, or a directory outside every authorized root).",
|
|
30284
|
+
imageExample: "Where files actually land: <session working directory>/{subdir}/<content hash>.<extension>",
|
|
30285
|
+
imageSaved: "Image path saved",
|
|
29924
30286
|
authorizedRoots: "Authorized roots",
|
|
29925
30287
|
absolutePath: "Absolute directory path",
|
|
29926
30288
|
displayLabel: "Display label",
|
|
29927
30289
|
addRoot: "Add root",
|
|
29928
30290
|
removeRoot: "Remove root",
|
|
30291
|
+
builtinRootBadge: "Default",
|
|
30292
|
+
builtinRootHint: "Default directory · cannot be removed",
|
|
30293
|
+
builtinRootNotice: "New roots are not granted to already paired phones by default. To share one, open 「设备」 and tick that directory for the phone in question.",
|
|
30294
|
+
errorRootBuiltin: "This is the current DSH working directory, so it cannot be removed. You can still add other roots above.",
|
|
30295
|
+
scopeChatRead: "Read chat",
|
|
30296
|
+
scopeChatWrite: "Send chat",
|
|
30297
|
+
scopeFilesRead: "Read files",
|
|
30298
|
+
scopeFilesWrite: "Write files",
|
|
30299
|
+
scopeFilesDelete: "Delete files",
|
|
30300
|
+
scopeSettingsRead: "Read settings",
|
|
30301
|
+
scopeSettingsWrite: "Change settings",
|
|
30302
|
+
noScopes: "No permissions",
|
|
30303
|
+
scopeGrantNotice: "Tick the permissions to grant, then press \"Enable and pair\". Once the app enters the pairing code, these scopes take effect with the device.",
|
|
30304
|
+
scopeGrantNoticeEmpty: "No permission selected yet. Tick at least one, otherwise the pairing code will carry no scopes at all.",
|
|
30305
|
+
scopeGrantNoticeExisting: "Changing this only affects new pairing codes. To change an already paired device, open Devices and use its Permissions button.",
|
|
30306
|
+
configTextScopeNote: "Config texts always carry a minimal set of scopes (no file deletion, no settings write) and ignore the ticks above — they are meant to be shared. Use a pairing code when you want the full set.",
|
|
29929
30307
|
remoteAccess: "Remote access",
|
|
29930
30308
|
bindIp: "Bind IP",
|
|
29931
30309
|
bindPort: "Port",
|
|
@@ -30008,8 +30386,48 @@ window.__ModuleLoader__.load({
|
|
|
30008
30386
|
listeners.add(listener);
|
|
30009
30387
|
return () => listeners.delete(listener);
|
|
30010
30388
|
}
|
|
30389
|
+
/**
|
|
30390
|
+
* 取一条文案并填占位符。
|
|
30391
|
+
*
|
|
30392
|
+
* **缺键不抛异常**。原来这里是 `dictionaries[locale][key].replace(...)`:任何一个不存在的键
|
|
30393
|
+
* 都会抛 TypeError,而它在 React 渲染路径上 —— 结果不是那一行显示不出原文,而是**整个设置
|
|
30394
|
+
* 面板白屏**。缺一个描述行就把七张卡里的全部设置都搭进去,代价和收益完全不成比例。
|
|
30395
|
+
*
|
|
30396
|
+
* 降级成「回退到另一种语言 → 再回退成键名本身」:最坏情况是某一行露出 `rootsTabDesc`
|
|
30397
|
+
* 这样的字面量,一眼能看出该补哪条,而不是面对一个空白面板。
|
|
30398
|
+
*/
|
|
30011
30399
|
function translate(locale, key, values = {}) {
|
|
30012
|
-
|
|
30400
|
+
const fill = (template) => template?.replace(/\{(\w+)\}/g, (_, name) => String(values[name] ?? `{${name}}`));
|
|
30401
|
+
const primary = fill(dictionaries[locale][key]);
|
|
30402
|
+
if (primary !== void 0) return primary;
|
|
30403
|
+
const text = fill(dictionaries["zh-CN"][key]) ?? key;
|
|
30404
|
+
console.warn(`[dsh-remote-bridge] 缺少文案键 ${String(key)}(locale=${locale}),已降级显示键名`);
|
|
30405
|
+
return text;
|
|
30406
|
+
}
|
|
30407
|
+
/**
|
|
30408
|
+
* 权限码 → 人话。
|
|
30409
|
+
*
|
|
30410
|
+
* 用户要求「中文状态下显示中文,英文显示英文」。文案刻意**用人话而不是协议码**:
|
|
30411
|
+
* `读文件` / `写文件` / `删文件` 自己就看得懂,而 `files.read` 要对照文档才知道指什么 ——
|
|
30412
|
+
* 出问题时用户把屏幕上的字念出来,比报一串码有用得多。
|
|
30413
|
+
*
|
|
30414
|
+
* 键名 `scope${码}`(`chat.read` → `scopeChatRead`):与既有 `rootsTab` 一致的拼法。
|
|
30415
|
+
* 用**显式查表**而不是模板拼 —— 拼错会查不到键,而键查不到时 `translate` 会静默降级成
|
|
30416
|
+
* 露出键名(见上面),界面上会突然冒出一行 `scopeChatread`。白屏那次就是这么来的。
|
|
30417
|
+
*/
|
|
30418
|
+
const SCOPE_LABEL_KEYS = {
|
|
30419
|
+
"chat.read": "scopeChatRead",
|
|
30420
|
+
"chat.write": "scopeChatWrite",
|
|
30421
|
+
"files.read": "scopeFilesRead",
|
|
30422
|
+
"files.write": "scopeFilesWrite",
|
|
30423
|
+
"files.delete": "scopeFilesDelete",
|
|
30424
|
+
"settings.read": "scopeSettingsRead",
|
|
30425
|
+
"settings.write": "scopeSettingsWrite"
|
|
30426
|
+
};
|
|
30427
|
+
function scopeLabel(locale, scope) {
|
|
30428
|
+
const key = SCOPE_LABEL_KEYS[scope];
|
|
30429
|
+
if (key === void 0) return scope;
|
|
30430
|
+
return translate(locale, key);
|
|
30013
30431
|
}
|
|
30014
30432
|
function useWorkspaceI18n() {
|
|
30015
30433
|
const locale = (0, react.useSyncExternalStore)(subscribeWorkspaceLocale, getWorkspaceLocale, () => "zh-CN");
|
|
@@ -30042,15 +30460,30 @@ window.__ModuleLoader__.load({
|
|
|
30042
30460
|
*/
|
|
30043
30461
|
const WORKSPACE_CSS = `
|
|
30044
30462
|
.daw-version{font:600 11px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-muted);white-space:nowrap}
|
|
30045
|
-
.daw-root,.daw-settings-section{--daw-bg
|
|
30046
|
-
.daw-root *{box-sizing:border-box;letter-spacing:0}
|
|
30047
|
-
.daw-
|
|
30048
|
-
.daw-icon
|
|
30049
|
-
.daw-
|
|
30050
|
-
.daw-
|
|
30051
|
-
.daw-
|
|
30052
|
-
.daw-
|
|
30053
|
-
|
|
30463
|
+
.daw-root,.daw-settings-section{--daw-bg:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-2,#f7f8fa));--daw-panel:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));--daw-ink:var(--dsw-alias-label-primary,#18202a);--daw-muted:var(--dsw-alias-label-tertiary,#687381);--daw-line:var(--dsw-alias-border-l2,#dfe4e8);--daw-accent:var(--dsw-alias-brand-primary,#087f8c);--daw-accent-soft:var(--dsw-alias-bg-module-platform,#e5f5f6);--daw-good:var(--dsw-alias-state-success-primary,#2f7d4a);--daw-danger:var(--dsw-alias-state-error-primary,#b33a3a);color:var(--daw-ink);background:transparent;font:13px/1.45 Inter,ui-sans-serif,system-ui,sans-serif;letter-spacing:0;min-width:0}
|
|
30464
|
+
.daw-root *{box-sizing:border-box;letter-spacing:0}
|
|
30465
|
+
.daw-input:focus-visible{border-color:var(--dsw-alias-brand-primary,var(--daw-accent));outline:none}.daw-icon{width:30px;padding:0}
|
|
30466
|
+
.daw-icon:hover{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}
|
|
30467
|
+
.daw-icon.danger{color:var(--daw-danger);border-color:transparent;background:transparent}
|
|
30468
|
+
.daw-icon:disabled{opacity:.45;cursor:not-allowed}
|
|
30469
|
+
.daw-command{padding:0 10px;border-color:#cbd2d8;background:#fff}.daw-command.primary{color:var(--dsw-alias-label-primary,var(--daw-ink));background:var(--dsw-alias-bg-module-platform,var(--daw-panel));border-color:var(--dsw-alias-border-l2,var(--daw-line));font-weight:500}
|
|
30470
|
+
.daw-command.primary:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}.daw-command.danger{color:var(--daw-danger)}.daw-filebar{display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--daw-line);padding:4px 8px;min-width:0}.daw-admin{overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}.daw-admin h3{font-size:14px;margin:0 0 12px}
|
|
30471
|
+
.daw-cards{display:grid;gap:10px}
|
|
30472
|
+
.daw-card{border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
|
|
30473
|
+
.daw-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
30474
|
+
.daw-card-open{background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
30475
|
+
.daw-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
|
|
30476
|
+
.daw-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:-2px}
|
|
30477
|
+
.daw-card-icon{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;display:flex;align-items:center}
|
|
30478
|
+
.daw-card-headtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
|
30479
|
+
.daw-card-name{color:var(--dsw-alias-label-primary,var(--daw-ink));font-size:15px;font-weight:600;line-height:1.4}
|
|
30480
|
+
.daw-card-desc{color:var(--dsw-alias-label-tertiary,var(--daw-muted));font-size:13px;line-height:1.5}
|
|
30481
|
+
.daw-card-chevron{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;transition:transform .16s;font-size:12px;line-height:1}
|
|
30482
|
+
.daw-card-chevron-open{transform:rotate(180deg)}
|
|
30483
|
+
.daw-card-body{border-top:1px solid var(--dsw-alias-border-l2,var(--daw-line));margin:0 16px;padding-bottom:8px}.daw-settings-section{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;min-width:0;min-height:0}.daw-settings-section>.daw-list-meta{margin:0}.daw-settings-section .daw-admin{padding:14px 2px}.daw-qr-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:12px}.daw-qr-card{margin:0;display:grid;justify-items:center;gap:8px;padding:16px;border:1px solid var(--daw-line);border-radius:6px;background:var(--daw-panel)}.daw-qr-image{width:180px;height:180px;background:var(--dsw-alias-bg-layer-3,#fff);border-radius:4px}.daw-qr-site{font-weight:600}.daw-qr-url{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-accent);text-decoration:none;overflow-wrap:anywhere;text-align:center}.daw-qr-url:hover{text-decoration:underline}.daw-form-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,.45fr) auto;gap:8px;margin-bottom:14px}.daw-listener-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px auto;gap:8px;align-items:end}.daw-field{display:grid;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:12px}.daw-listener-status{display:flex;flex-wrap:wrap;gap:5px 18px;padding:9px 0;color:var(--daw-muted);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.daw-listener-hint{margin:0 0 8px}.daw-list{border-top:1px solid var(--daw-line)}.daw-list-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;min-height:48px;border-bottom:1px solid var(--daw-line);padding:7px 2px}.daw-list-title{font-weight:600;overflow-wrap:anywhere}
|
|
30484
|
+
.daw-list-title .daw-pill{margin-left:8px;vertical-align:middle}
|
|
30485
|
+
.daw-builtin-hint{text-align:right;align-self:center}
|
|
30486
|
+
.daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-inline-command{margin-left:8px}@mediaa(max-width:720px){.daw-form-row,.daw-listener-form{grid-template-columns:1fr}.daw-admin{padding:12px}.daw-qr-grid{grid-template-columns:1fr}.daw-qr-image{width:150px;height:150px}.daw-inline-command{margin:8px 0 0}}
|
|
30054
30487
|
`;
|
|
30055
30488
|
function installWorkspaceStyles() {
|
|
30056
30489
|
if (document.getElementById(STYLE_ID) !== null) return;
|
|
@@ -30088,14 +30521,6 @@ window.__ModuleLoader__.load({
|
|
|
30088
30521
|
//#endregion
|
|
30089
30522
|
//#region src/client/workspace.tsx
|
|
30090
30523
|
var import_lucide_react = require_lucide_react();
|
|
30091
|
-
const ADMIN_TABS = [
|
|
30092
|
-
"roots",
|
|
30093
|
-
"remote",
|
|
30094
|
-
"devices",
|
|
30095
|
-
"trash",
|
|
30096
|
-
"audit",
|
|
30097
|
-
"app-download"
|
|
30098
|
-
];
|
|
30099
30524
|
/** What the settings-page launcher opens: authorized roots, the first section. */
|
|
30100
30525
|
const DEFAULT_PANEL_SECTION = "roots";
|
|
30101
30526
|
/**
|
|
@@ -30109,6 +30534,29 @@ window.__ModuleLoader__.load({
|
|
|
30109
30534
|
return tab === "app-download" ? "appDownloadTab" : `${tab}Tab`;
|
|
30110
30535
|
}
|
|
30111
30536
|
/**
|
|
30537
|
+
* 卡片头部的描述行。与 `tabLabelKey` 同理:唯一含连字符的分区显式映射。
|
|
30538
|
+
*
|
|
30539
|
+
* 键名是 `${tab}TabDesc`(**带 Tab**),不是 `${tab}Desc`。这个中间段不是冗余:它让
|
|
30540
|
+
* 描述键与标题键 `rootsTab` / `rootsTabDesc` 成对,读的人一眼能看出这两个属于同一张卡。
|
|
30541
|
+
*
|
|
30542
|
+
* 踩过的坑:早先这里写成 `${tab}Desc`,`rootsDesc` 之类根本不在字典里 —— `translate` 直接
|
|
30543
|
+
* `dictionaries[locale][key].replace(...)` 抛 TypeError,整个设置面板白屏。`as MessageKey`
|
|
30544
|
+
* 把类型检查骗过去了,所以单测全绿也照样白屏。**键名靠模板拼出来时,类型系统是最后一道
|
|
30545
|
+
* 防线,这里偏偏被断言关掉了。** 现在改成显式查表,见下。
|
|
30546
|
+
*/
|
|
30547
|
+
const TAB_DESC_KEYS = {
|
|
30548
|
+
roots: "rootsTabDesc",
|
|
30549
|
+
remote: "remoteTabDesc",
|
|
30550
|
+
devices: "devicesTabDesc",
|
|
30551
|
+
image: "imageTabDesc",
|
|
30552
|
+
trash: "trashTabDesc",
|
|
30553
|
+
audit: "auditTabDesc",
|
|
30554
|
+
"app-download": "appDownloadTabDesc"
|
|
30555
|
+
};
|
|
30556
|
+
function tabDescKey(tab) {
|
|
30557
|
+
return TAB_DESC_KEYS[tab];
|
|
30558
|
+
}
|
|
30559
|
+
/**
|
|
30112
30560
|
* The single desktop dialog: a vertical section rail on the left, one content pane on the right.
|
|
30113
30561
|
*
|
|
30114
30562
|
* Settings only — the five admin sections. The file tree used to live here as a sixth row; it does
|
|
@@ -30124,38 +30572,88 @@ window.__ModuleLoader__.load({
|
|
|
30124
30572
|
* 这就是把弹窗嵌进设置页的代价:入口看起来是空的,因为内容在下一层。
|
|
30125
30573
|
* 设置页本身就是一层容器,没有任何理由再套一层。
|
|
30126
30574
|
*/
|
|
30575
|
+
/**
|
|
30576
|
+
* 设置面板主体:**七张卡片平铺,各自独立开合,没有导航栏**。
|
|
30577
|
+
*
|
|
30578
|
+
* 上一版留了个分区导航栏、一次只显示一张卡。对照 Vision Router 的实际长相才发现差得远:
|
|
30579
|
+
* 它是全部卡片一起列出来、想开哪张开哪张,标题栏自己就是入口。那句提示词写得很直白 ——
|
|
30580
|
+
* 「多张卡可以同时保持展开」。所以导航栏这一层是多余的,用户还得先点导航、再点卡片头,
|
|
30581
|
+
* 两次点击才走一步。
|
|
30582
|
+
*
|
|
30583
|
+
* 抽出来是为了让 DSH 设置页能把它**内联**渲染。
|
|
30584
|
+
*
|
|
30585
|
+
* 早先的版本做不到这一点,于是设置页里只放了一句说明加一个「打开手机设置面板」按钮,
|
|
30586
|
+
* 真正的设置藏在按钮后面的模态框里。用户的反应很直接:「其他的可以设置的东西那去了」——
|
|
30587
|
+
* 这就是把弹窗嵌进设置页的代价:入口看起来是空的,因为内容在下一层。
|
|
30588
|
+
* 设置页本身就是一层容器,没有任何理由再套一层。
|
|
30589
|
+
*
|
|
30590
|
+
* `section` / `onSection` 保留是为了兼容旧调用方,但现在**不再决定显示哪一张卡**,
|
|
30591
|
+
* 只用来决定默认展开哪一张。
|
|
30592
|
+
*/
|
|
30127
30593
|
function PhoneSettingsBody(props) {
|
|
30594
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminPanel, {
|
|
30595
|
+
api: props.api,
|
|
30596
|
+
initialOpen: props.section
|
|
30597
|
+
});
|
|
30598
|
+
}
|
|
30599
|
+
/**
|
|
30600
|
+
* 一个设置分区 = 一张 Vision Router 式折叠卡片。
|
|
30601
|
+
*
|
|
30602
|
+
* 交互模型照搬 dsh-vision-router 的 `vr-card`:外壳是圆角 12px 卡片,展开时加
|
|
30603
|
+
* `daw-card-open`;头部是一个 `aria-expanded` 的按钮,装着图标 + 标题 + 描述 + 旋转箭头;
|
|
30604
|
+
* 展开体是 `daw-card-body`(顶部一条分隔线)。
|
|
30605
|
+
*
|
|
30606
|
+
* 七张卡同时挂在页面上,各张独立开合 —— 导航栏已经删掉了,卡片头自己就是入口。
|
|
30607
|
+
*/
|
|
30608
|
+
function SettingsCard(props) {
|
|
30128
30609
|
const { t } = useWorkspaceI18n();
|
|
30129
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
30130
|
-
className: "daw-
|
|
30131
|
-
|
|
30132
|
-
|
|
30133
|
-
className:
|
|
30134
|
-
|
|
30610
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
30611
|
+
className: `daw-card${props.open ? " daw-card-open" : ""}`,
|
|
30612
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
30613
|
+
type: "button",
|
|
30614
|
+
className: "daw-card-header",
|
|
30615
|
+
"aria-expanded": props.open,
|
|
30616
|
+
onClick: props.onToggle,
|
|
30135
30617
|
children: [
|
|
30136
|
-
|
|
30137
|
-
|
|
30138
|
-
|
|
30618
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30619
|
+
className: "daw-card-icon",
|
|
30620
|
+
"aria-hidden": "true",
|
|
30621
|
+
children: tabIcon(props.tab)
|
|
30622
|
+
}),
|
|
30623
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
30624
|
+
className: "daw-card-headtext",
|
|
30625
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30626
|
+
className: "daw-card-name",
|
|
30627
|
+
children: t(props.nameKey)
|
|
30628
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30629
|
+
className: "daw-card-desc",
|
|
30630
|
+
children: t(props.descKey)
|
|
30631
|
+
})]
|
|
30632
|
+
}),
|
|
30633
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30634
|
+
className: `daw-card-chevron${props.open ? " daw-card-chevron-open" : ""}`,
|
|
30635
|
+
"aria-hidden": "true",
|
|
30636
|
+
children: "▾"
|
|
30637
|
+
})
|
|
30139
30638
|
]
|
|
30140
|
-
},
|
|
30141
|
-
|
|
30142
|
-
|
|
30143
|
-
|
|
30144
|
-
|
|
30145
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminPanel, {
|
|
30146
|
-
api: props.api,
|
|
30147
|
-
section: props.section
|
|
30148
|
-
})
|
|
30149
|
-
})
|
|
30150
|
-
})] });
|
|
30639
|
+
}), props.open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30640
|
+
className: "daw-card-body",
|
|
30641
|
+
children: props.children
|
|
30642
|
+
})]
|
|
30643
|
+
});
|
|
30151
30644
|
}
|
|
30152
30645
|
/**
|
|
30153
30646
|
* The settings content of one admin section, extracted from `AdminOverlay` so the unified panel and
|
|
30154
30647
|
* the standalone `/dsh-workspace` page render exactly the same body.
|
|
30155
30648
|
*/
|
|
30649
|
+
/**
|
|
30650
|
+
* 设置面板:七张卡片平铺,各自独立开合。
|
|
30651
|
+
*
|
|
30652
|
+
* `initialOpen` 决定进页面时先展开哪一张(默认第一张),之后完全由用户点卡片头控制。
|
|
30653
|
+
* 展开状态存 Set —— 这样「同时展开两张」是自然结果,不需要额外的互斥逻辑。
|
|
30654
|
+
*/
|
|
30156
30655
|
function AdminPanel(props) {
|
|
30157
30656
|
const { locale, t } = useWorkspaceI18n();
|
|
30158
|
-
const tab = props.section;
|
|
30159
30657
|
const [status, setStatus] = (0, react.useState)();
|
|
30160
30658
|
const [trash, setTrash] = (0, react.useState)([]);
|
|
30161
30659
|
const [audit, setAudit] = (0, react.useState)([]);
|
|
@@ -30167,18 +30665,37 @@ window.__ModuleLoader__.load({
|
|
|
30167
30665
|
const [configText, setConfigText] = (0, react.useState)();
|
|
30168
30666
|
const [configEndpoint, setConfigEndpoint] = (0, react.useState)("");
|
|
30169
30667
|
const [configTextBusy, setConfigTextBusy] = (0, react.useState)(false);
|
|
30668
|
+
const [image, setImage] = (0, react.useState)();
|
|
30669
|
+
const [imageDraft, setImageDraft] = (0, react.useState)("");
|
|
30670
|
+
const [imageBusy, setImageBusy] = (0, react.useState)(false);
|
|
30671
|
+
const [openCards, setOpenCards] = (0, react.useState)(() => /* @__PURE__ */ new Set([props.initialOpen]));
|
|
30672
|
+
const [imageSaved, setImageSaved] = (0, react.useState)(false);
|
|
30170
30673
|
const [error, setError] = (0, react.useState)();
|
|
30674
|
+
const isOpen = (0, react.useCallback)((card) => openCards.has(card), [openCards]);
|
|
30675
|
+
const toggleCard = (0, react.useCallback)((card) => {
|
|
30676
|
+
setOpenCards((previous) => {
|
|
30677
|
+
const next = new Set(previous);
|
|
30678
|
+
if (!next.delete(card)) next.add(card);
|
|
30679
|
+
return next;
|
|
30680
|
+
});
|
|
30681
|
+
}, []);
|
|
30171
30682
|
const refresh = (0, react.useCallback)(async () => {
|
|
30172
30683
|
try {
|
|
30173
30684
|
const next = await props.api.status();
|
|
30174
30685
|
setStatus(next);
|
|
30175
|
-
if (
|
|
30176
|
-
if (
|
|
30686
|
+
if (openCards.has("trash")) setTrash((await props.api.trashItems()).items);
|
|
30687
|
+
if (openCards.has("audit")) setAudit((await props.api.audit()).items);
|
|
30688
|
+
if (openCards.has("image")) {
|
|
30689
|
+
const settings = await props.api.imageSettings();
|
|
30690
|
+
setImage(settings);
|
|
30691
|
+
setImageDraft(settings.subdir);
|
|
30692
|
+
setImageSaved(false);
|
|
30693
|
+
}
|
|
30177
30694
|
setError(void 0);
|
|
30178
30695
|
} catch (cause) {
|
|
30179
30696
|
setError(cause);
|
|
30180
30697
|
}
|
|
30181
|
-
}, [props.api,
|
|
30698
|
+
}, [props.api, openCards]);
|
|
30182
30699
|
(0, react.useEffect)(() => {
|
|
30183
30700
|
refresh();
|
|
30184
30701
|
}, [refresh]);
|
|
@@ -30188,6 +30705,11 @@ window.__ModuleLoader__.load({
|
|
|
30188
30705
|
setListenerPort(String(status.configuredPort));
|
|
30189
30706
|
}, [status?.configuredHost, status?.configuredPort]);
|
|
30190
30707
|
const roots = status?.roots ?? [];
|
|
30708
|
+
const [grantedScopes, setGrantedScopes] = (0, react.useState)([...DEVICE_SCOPES]);
|
|
30709
|
+
const toggleScope = (0, react.useCallback)((scope) => {
|
|
30710
|
+
setGrantedScopes((current) => current.includes(scope) ? current.filter((item) => item !== scope) : [...current, scope]);
|
|
30711
|
+
}, []);
|
|
30712
|
+
const grantNotice = grantedScopes.length === 0 ? t("scopeGrantNoticeEmpty") : (status?.devices?.length ?? 0) === 0 ? t("scopeGrantNotice") : t("scopeGrantNoticeExisting");
|
|
30191
30713
|
const parsedListenerPort = Number(listenerPort);
|
|
30192
30714
|
const listenerPortValid = Number.isInteger(parsedListenerPort) && parsedListenerPort >= 1 && parsedListenerPort <= 65535;
|
|
30193
30715
|
const listenerDirty = status !== void 0 && (listenerHost.trim() !== status.configuredHost || parsedListenerPort !== status.configuredPort);
|
|
@@ -30215,226 +30737,361 @@ window.__ModuleLoader__.load({
|
|
|
30215
30737
|
className: "daw-warning",
|
|
30216
30738
|
children: messageOf(error, t)
|
|
30217
30739
|
}),
|
|
30218
|
-
|
|
30219
|
-
|
|
30220
|
-
|
|
30221
|
-
|
|
30222
|
-
|
|
30223
|
-
|
|
30224
|
-
|
|
30225
|
-
|
|
30226
|
-
|
|
30227
|
-
|
|
30228
|
-
|
|
30229
|
-
|
|
30230
|
-
|
|
30231
|
-
|
|
30232
|
-
|
|
30233
|
-
|
|
30740
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
30741
|
+
tab: "roots",
|
|
30742
|
+
nameKey: tabLabelKey("roots"),
|
|
30743
|
+
descKey: tabDescKey("roots"),
|
|
30744
|
+
open: isOpen("roots"),
|
|
30745
|
+
onToggle: () => toggleCard("roots"),
|
|
30746
|
+
children: [
|
|
30747
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30748
|
+
className: "daw-form-row",
|
|
30749
|
+
children: [
|
|
30750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30751
|
+
className: "daw-input",
|
|
30752
|
+
placeholder: t("absolutePath"),
|
|
30753
|
+
value: path,
|
|
30754
|
+
onChange: (event) => setPath(event.target.value)
|
|
30755
|
+
}),
|
|
30756
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30757
|
+
className: "daw-input",
|
|
30758
|
+
placeholder: t("displayLabel"),
|
|
30759
|
+
value: label,
|
|
30760
|
+
onChange: (event) => setLabel(event.target.value)
|
|
30761
|
+
}),
|
|
30762
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30763
|
+
className: "daw-command primary",
|
|
30764
|
+
onClick: () => {
|
|
30765
|
+
props.api.addRoot(path, label || void 0).then(() => {
|
|
30766
|
+
setPath("");
|
|
30767
|
+
setLabel("");
|
|
30768
|
+
return refresh();
|
|
30769
|
+
}).catch(setError);
|
|
30770
|
+
},
|
|
30771
|
+
children: t("addRoot")
|
|
30772
|
+
})
|
|
30773
|
+
]
|
|
30774
|
+
}),
|
|
30775
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30776
|
+
className: "daw-list",
|
|
30777
|
+
children: roots.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30778
|
+
className: "daw-list-row",
|
|
30779
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30780
|
+
className: "daw-list-title",
|
|
30781
|
+
children: [root.label, root.builtin === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30782
|
+
className: "daw-pill on",
|
|
30783
|
+
children: t("builtinRootBadge")
|
|
30784
|
+
})]
|
|
30785
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30786
|
+
className: "daw-list-meta",
|
|
30787
|
+
children: root.realPath
|
|
30788
|
+
})] }), root.builtin === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30789
|
+
className: "daw-list-meta daw-builtin-hint",
|
|
30790
|
+
children: t("builtinRootHint")
|
|
30791
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30792
|
+
className: "daw-icon danger",
|
|
30793
|
+
title: t("removeRoot"),
|
|
30794
|
+
onClick: () => {
|
|
30795
|
+
props.api.removeRoot(root.id).then(refresh).catch(setError);
|
|
30796
|
+
},
|
|
30797
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
30798
|
+
})]
|
|
30799
|
+
}, root.id))
|
|
30800
|
+
}),
|
|
30801
|
+
roots.some((root) => root.builtin === true) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30802
|
+
className: "daw-list-meta daw-builtin-notice",
|
|
30803
|
+
children: t("builtinRootNotice")
|
|
30804
|
+
})
|
|
30805
|
+
]
|
|
30806
|
+
}, "roots"),
|
|
30807
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
30808
|
+
tab: "remote",
|
|
30809
|
+
nameKey: tabLabelKey("remote"),
|
|
30810
|
+
descKey: tabDescKey("remote"),
|
|
30811
|
+
open: isOpen("remote"),
|
|
30812
|
+
onToggle: () => toggleCard("remote"),
|
|
30813
|
+
children: [
|
|
30814
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30815
|
+
className: "daw-warning",
|
|
30816
|
+
children: t("httpWarning")
|
|
30817
|
+
}),
|
|
30818
|
+
roots.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30819
|
+
className: "daw-warning",
|
|
30820
|
+
children: t("addRootBeforeRemote")
|
|
30821
|
+
}),
|
|
30822
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30823
|
+
className: "daw-listener-form",
|
|
30824
|
+
children: [
|
|
30825
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
30826
|
+
className: "daw-field",
|
|
30827
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("bindIp") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30828
|
+
className: "daw-input",
|
|
30829
|
+
value: listenerHost,
|
|
30830
|
+
disabled: status?.remoteEnabled,
|
|
30831
|
+
spellCheck: false,
|
|
30832
|
+
placeholder: "0.0.0.0",
|
|
30833
|
+
onChange: (event) => setListenerHost(event.target.value)
|
|
30834
|
+
})]
|
|
30835
|
+
}),
|
|
30836
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
30837
|
+
className: "daw-field",
|
|
30838
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("bindPort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30839
|
+
className: "daw-input",
|
|
30840
|
+
type: "number",
|
|
30841
|
+
min: 1,
|
|
30842
|
+
max: 65535,
|
|
30843
|
+
inputMode: "numeric",
|
|
30844
|
+
value: listenerPort,
|
|
30845
|
+
disabled: status?.remoteEnabled,
|
|
30846
|
+
onChange: (event) => setListenerPort(event.target.value)
|
|
30847
|
+
})]
|
|
30848
|
+
}),
|
|
30849
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30850
|
+
className: "daw-command",
|
|
30851
|
+
disabled: status?.remoteEnabled || listenerHost.trim() === "" || !listenerPortValid || !listenerDirty,
|
|
30852
|
+
onClick: () => {
|
|
30853
|
+
props.api.configureListener(listenerHost.trim(), parsedListenerPort).then((result) => poll(result.id)).then(refresh).catch(setError);
|
|
30854
|
+
},
|
|
30855
|
+
children: t("saveListener")
|
|
30856
|
+
})
|
|
30857
|
+
]
|
|
30858
|
+
}),
|
|
30859
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30860
|
+
className: "daw-listener-status",
|
|
30861
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
30862
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [t("configuredListener"), ":"] }),
|
|
30863
|
+
" ",
|
|
30864
|
+
formatEndpoint(status?.configuredHost ?? listenerHost, status?.configuredPort ?? parsedListenerPort)
|
|
30865
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
30866
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [t("actualListener"), ":"] }),
|
|
30867
|
+
" ",
|
|
30868
|
+
formatEndpoint(status?.host ?? "127.0.0.1", status?.port ?? parsedListenerPort)
|
|
30869
|
+
] })]
|
|
30870
|
+
}),
|
|
30871
|
+
status?.remoteEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30872
|
+
className: "daw-list-meta daw-listener-hint",
|
|
30873
|
+
children: t("disableToEditListener")
|
|
30874
|
+
}),
|
|
30875
|
+
!status?.remoteEnabled && listenerDirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30876
|
+
className: "daw-list-meta daw-listener-hint",
|
|
30877
|
+
children: t("saveBeforeEnable")
|
|
30878
|
+
}),
|
|
30879
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30880
|
+
className: `daw-pill${status?.remoteEnabled ? " on" : ""}`,
|
|
30881
|
+
children: status?.remoteEnabled ? t("remoteEnabled") : t("loopbackOnly")
|
|
30882
|
+
}) }),
|
|
30883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30884
|
+
className: "daw-scope-grid",
|
|
30885
|
+
children: DEVICE_SCOPES.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
30886
|
+
className: "daw-check",
|
|
30887
|
+
children: [
|
|
30888
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30889
|
+
type: "checkbox",
|
|
30890
|
+
checked: grantedScopes.includes(scope),
|
|
30891
|
+
onChange: () => toggleScope(scope)
|
|
30892
|
+
}),
|
|
30893
|
+
" ",
|
|
30894
|
+
scopeLabel(locale, scope)
|
|
30895
|
+
]
|
|
30896
|
+
}, scope))
|
|
30897
|
+
}),
|
|
30898
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30899
|
+
className: "daw-list-meta",
|
|
30900
|
+
children: grantNotice
|
|
30901
|
+
}),
|
|
30902
|
+
status?.remoteEnabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30903
|
+
className: "daw-command danger",
|
|
30904
|
+
onClick: () => {
|
|
30905
|
+
props.api.disableRemote().then((result) => poll(result.id)).then(refresh).catch(setError);
|
|
30906
|
+
},
|
|
30907
|
+
children: t("disableRemote")
|
|
30908
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30909
|
+
className: "daw-command primary",
|
|
30910
|
+
disabled: roots.length === 0 || listenerDirty || !listenerPortValid || listenerHost.trim() === "" || grantedScopes.length === 0,
|
|
30911
|
+
onClick: () => {
|
|
30912
|
+
props.api.enableRemote(roots.map((root) => root.id), grantedScopes).then((result) => poll(result.id)).catch(setError);
|
|
30913
|
+
},
|
|
30914
|
+
children: t("enablePairing")
|
|
30915
|
+
}),
|
|
30916
|
+
status?.remoteEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
30917
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30918
|
+
className: "daw-command daw-inline-command",
|
|
30919
|
+
disabled: grantedScopes.length === 0,
|
|
30920
|
+
onClick: () => {
|
|
30921
|
+
props.api.createPairing(roots.map((root) => root.id), grantedScopes).then((value) => setPairing(value)).catch(setError);
|
|
30922
|
+
},
|
|
30923
|
+
children: t("newPairingCode")
|
|
30234
30924
|
}),
|
|
30235
30925
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30236
|
-
className: "daw-command
|
|
30926
|
+
className: "daw-command daw-inline-command",
|
|
30927
|
+
disabled: configTextBusy,
|
|
30237
30928
|
onClick: () => {
|
|
30238
|
-
|
|
30239
|
-
|
|
30240
|
-
|
|
30929
|
+
const manual = configEndpoint.trim();
|
|
30930
|
+
setConfigTextBusy(true);
|
|
30931
|
+
setError(void 0);
|
|
30932
|
+
props.api.createConfigText({
|
|
30933
|
+
...roots.length === 0 ? {} : { rootIds: roots.map((root) => root.id) },
|
|
30934
|
+
...manual === "" ? {} : { endpoints: [{
|
|
30935
|
+
label: t("configTextManualLabel"),
|
|
30936
|
+
baseUrl: manual
|
|
30937
|
+
}] }
|
|
30938
|
+
}).then((value) => {
|
|
30939
|
+
setConfigText(value);
|
|
30241
30940
|
return refresh();
|
|
30242
|
-
}).catch(setError);
|
|
30941
|
+
}).catch(setError).finally(() => setConfigTextBusy(false));
|
|
30243
30942
|
},
|
|
30244
|
-
children: t("
|
|
30943
|
+
children: t("newConfigText")
|
|
30944
|
+
}),
|
|
30945
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30946
|
+
className: "daw-list-meta daw-listener-hint",
|
|
30947
|
+
children: t("configTextScopeNote")
|
|
30245
30948
|
})
|
|
30246
|
-
]
|
|
30247
|
-
|
|
30248
|
-
|
|
30249
|
-
|
|
30250
|
-
|
|
30251
|
-
|
|
30252
|
-
|
|
30253
|
-
|
|
30254
|
-
|
|
30255
|
-
}
|
|
30256
|
-
|
|
30257
|
-
children: root.realPath
|
|
30258
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30259
|
-
className: "daw-icon danger",
|
|
30260
|
-
title: t("removeRoot"),
|
|
30261
|
-
onClick: () => {
|
|
30262
|
-
props.api.removeRoot(root.id).then(refresh).catch(setError);
|
|
30263
|
-
},
|
|
30264
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
30265
|
-
})]
|
|
30266
|
-
}, root.id))
|
|
30267
|
-
})
|
|
30268
|
-
] }),
|
|
30269
|
-
tab === "remote" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
30270
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("remoteAccess") }),
|
|
30271
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30272
|
-
className: "daw-warning",
|
|
30273
|
-
children: t("httpWarning")
|
|
30274
|
-
}),
|
|
30275
|
-
roots.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30276
|
-
className: "daw-warning",
|
|
30277
|
-
children: t("addRootBeforeRemote")
|
|
30278
|
-
}),
|
|
30279
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30280
|
-
className: "daw-listener-form",
|
|
30281
|
-
children: [
|
|
30282
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
30949
|
+
] }),
|
|
30950
|
+
pairing !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30951
|
+
className: "daw-code",
|
|
30952
|
+
children: pairing.code
|
|
30953
|
+
}),
|
|
30954
|
+
status?.remoteEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30955
|
+
style: {
|
|
30956
|
+
marginTop: 12,
|
|
30957
|
+
maxWidth: 560
|
|
30958
|
+
},
|
|
30959
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
30283
30960
|
className: "daw-field",
|
|
30284
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("
|
|
30961
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("configTextManual") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30285
30962
|
className: "daw-input",
|
|
30286
|
-
value:
|
|
30287
|
-
disabled: status?.remoteEnabled,
|
|
30963
|
+
value: configEndpoint,
|
|
30288
30964
|
spellCheck: false,
|
|
30289
|
-
placeholder: "
|
|
30290
|
-
onChange: (event) =>
|
|
30291
|
-
})]
|
|
30292
|
-
}),
|
|
30293
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
30294
|
-
className: "daw-field",
|
|
30295
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("bindPort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30296
|
-
className: "daw-input",
|
|
30297
|
-
type: "number",
|
|
30298
|
-
min: 1,
|
|
30299
|
-
max: 65535,
|
|
30300
|
-
inputMode: "numeric",
|
|
30301
|
-
value: listenerPort,
|
|
30302
|
-
disabled: status?.remoteEnabled,
|
|
30303
|
-
onChange: (event) => setListenerPort(event.target.value)
|
|
30965
|
+
placeholder: t("configTextManualPlaceholder"),
|
|
30966
|
+
onChange: (event) => setConfigEndpoint(event.target.value)
|
|
30304
30967
|
})]
|
|
30305
|
-
}),
|
|
30306
|
-
|
|
30307
|
-
|
|
30308
|
-
|
|
30309
|
-
|
|
30310
|
-
|
|
30311
|
-
|
|
30312
|
-
|
|
30313
|
-
|
|
30314
|
-
|
|
30315
|
-
|
|
30316
|
-
|
|
30317
|
-
|
|
30318
|
-
|
|
30319
|
-
|
|
30320
|
-
|
|
30321
|
-
|
|
30322
|
-
]
|
|
30323
|
-
|
|
30324
|
-
|
|
30325
|
-
|
|
30326
|
-
|
|
30327
|
-
|
|
30328
|
-
|
|
30329
|
-
|
|
30330
|
-
|
|
30331
|
-
|
|
30332
|
-
|
|
30333
|
-
|
|
30334
|
-
|
|
30335
|
-
|
|
30336
|
-
|
|
30337
|
-
|
|
30338
|
-
|
|
30339
|
-
|
|
30340
|
-
|
|
30341
|
-
|
|
30342
|
-
|
|
30343
|
-
className: "daw-check",
|
|
30344
|
-
children: [
|
|
30345
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30346
|
-
type: "checkbox",
|
|
30347
|
-
checked: true,
|
|
30348
|
-
readOnly: true
|
|
30349
|
-
}),
|
|
30350
|
-
" ",
|
|
30351
|
-
scope
|
|
30352
|
-
]
|
|
30353
|
-
}, scope))
|
|
30354
|
-
}),
|
|
30355
|
-
status?.remoteEnabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30356
|
-
className: "daw-command danger",
|
|
30357
|
-
onClick: () => {
|
|
30358
|
-
props.api.disableRemote().then((result) => poll(result.id)).then(refresh).catch(setError);
|
|
30359
|
-
},
|
|
30360
|
-
children: t("disableRemote")
|
|
30361
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30362
|
-
className: "daw-command primary",
|
|
30363
|
-
disabled: roots.length === 0 || listenerDirty || !listenerPortValid || listenerHost.trim() === "",
|
|
30364
|
-
onClick: () => {
|
|
30365
|
-
props.api.enableRemote(roots.map((root) => root.id), [...DEVICE_SCOPES]).then((result) => poll(result.id)).catch(setError);
|
|
30366
|
-
},
|
|
30367
|
-
children: t("enablePairing")
|
|
30368
|
-
}),
|
|
30369
|
-
status?.remoteEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30370
|
-
className: "daw-command daw-inline-command",
|
|
30371
|
-
onClick: () => {
|
|
30372
|
-
props.api.createPairing(roots.map((root) => root.id), [...DEVICE_SCOPES]).then((value) => setPairing(value)).catch(setError);
|
|
30373
|
-
},
|
|
30374
|
-
children: t("newPairingCode")
|
|
30375
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
30376
|
-
className: "daw-command daw-inline-command",
|
|
30377
|
-
disabled: configTextBusy,
|
|
30378
|
-
onClick: () => {
|
|
30379
|
-
const manual = configEndpoint.trim();
|
|
30380
|
-
setConfigTextBusy(true);
|
|
30381
|
-
setError(void 0);
|
|
30382
|
-
props.api.createConfigText({
|
|
30383
|
-
...roots.length === 0 ? {} : { rootIds: roots.map((root) => root.id) },
|
|
30384
|
-
...manual === "" ? {} : { endpoints: [{
|
|
30385
|
-
label: t("configTextManualLabel"),
|
|
30386
|
-
baseUrl: manual
|
|
30387
|
-
}] }
|
|
30388
|
-
}).then((value) => {
|
|
30389
|
-
setConfigText(value);
|
|
30390
|
-
return refresh();
|
|
30391
|
-
}).catch(setError).finally(() => setConfigTextBusy(false));
|
|
30392
|
-
},
|
|
30393
|
-
children: t("newConfigText")
|
|
30394
|
-
})] }),
|
|
30395
|
-
pairing !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30396
|
-
className: "daw-code",
|
|
30397
|
-
children: pairing.code
|
|
30398
|
-
}),
|
|
30399
|
-
status?.remoteEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30400
|
-
style: {
|
|
30401
|
-
marginTop: 12,
|
|
30402
|
-
maxWidth: 560
|
|
30403
|
-
},
|
|
30404
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
30968
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30969
|
+
className: "daw-list-meta",
|
|
30970
|
+
style: { marginTop: 5 },
|
|
30971
|
+
children: t("configTextManualHint")
|
|
30972
|
+
})]
|
|
30973
|
+
}),
|
|
30974
|
+
configText !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigTextPanel, { result: configText })
|
|
30975
|
+
]
|
|
30976
|
+
}, "remote"),
|
|
30977
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsCard, {
|
|
30978
|
+
tab: "devices",
|
|
30979
|
+
nameKey: tabLabelKey("devices"),
|
|
30980
|
+
descKey: tabDescKey("devices"),
|
|
30981
|
+
open: isOpen("devices"),
|
|
30982
|
+
onToggle: () => toggleCard("devices"),
|
|
30983
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30984
|
+
className: "daw-list",
|
|
30985
|
+
children: (status?.devices ?? []).map((device) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceRow, {
|
|
30986
|
+
api: props.api,
|
|
30987
|
+
device,
|
|
30988
|
+
roots,
|
|
30989
|
+
onChanged: refresh,
|
|
30990
|
+
onError: setError
|
|
30991
|
+
}, device.id))
|
|
30992
|
+
})
|
|
30993
|
+
}, "devices"),
|
|
30994
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
30995
|
+
tab: "image",
|
|
30996
|
+
nameKey: tabLabelKey("image"),
|
|
30997
|
+
descKey: tabDescKey("image"),
|
|
30998
|
+
open: isOpen("image"),
|
|
30999
|
+
onToggle: () => toggleCard("image"),
|
|
31000
|
+
children: [
|
|
31001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
31002
|
+
className: "daw-list-meta",
|
|
31003
|
+
children: t("imageHint")
|
|
31004
|
+
}),
|
|
31005
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
30405
31006
|
className: "daw-field",
|
|
30406
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("
|
|
31007
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageSubdir") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
30407
31008
|
className: "daw-input",
|
|
30408
|
-
value:
|
|
31009
|
+
value: imageDraft,
|
|
30409
31010
|
spellCheck: false,
|
|
30410
|
-
|
|
30411
|
-
onChange: (event) =>
|
|
31011
|
+
disabled: imageBusy || image === void 0,
|
|
31012
|
+
onChange: (event) => {
|
|
31013
|
+
setImageDraft(event.target.value);
|
|
31014
|
+
setImageSaved(false);
|
|
31015
|
+
}
|
|
30412
31016
|
})]
|
|
30413
|
-
}),
|
|
31017
|
+
}),
|
|
31018
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30414
31019
|
className: "daw-list-meta",
|
|
30415
31020
|
style: { marginTop: 5 },
|
|
30416
|
-
children: t("
|
|
30417
|
-
})
|
|
30418
|
-
|
|
30419
|
-
|
|
30420
|
-
|
|
30421
|
-
|
|
30422
|
-
|
|
30423
|
-
|
|
30424
|
-
|
|
30425
|
-
|
|
30426
|
-
|
|
30427
|
-
|
|
30428
|
-
|
|
30429
|
-
|
|
30430
|
-
|
|
30431
|
-
|
|
30432
|
-
|
|
30433
|
-
|
|
31021
|
+
children: t("imageSubdirHint")
|
|
31022
|
+
}),
|
|
31023
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31024
|
+
className: "daw-field",
|
|
31025
|
+
style: { marginTop: 14 },
|
|
31026
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageDefault") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31027
|
+
className: "daw-input",
|
|
31028
|
+
value: image?.defaultSubdir ?? "",
|
|
31029
|
+
readOnly: true,
|
|
31030
|
+
disabled: true
|
|
31031
|
+
})]
|
|
31032
|
+
}),
|
|
31033
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31034
|
+
className: "daw-list-meta",
|
|
31035
|
+
style: { marginTop: 5 },
|
|
31036
|
+
children: t("imageDefaultHint")
|
|
31037
|
+
}),
|
|
31038
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31039
|
+
className: "daw-field",
|
|
31040
|
+
style: { marginTop: 14 },
|
|
31041
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageFallback") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31042
|
+
className: "daw-input",
|
|
31043
|
+
value: image?.fallbackDir ?? "",
|
|
31044
|
+
readOnly: true,
|
|
31045
|
+
disabled: true
|
|
31046
|
+
})]
|
|
31047
|
+
}),
|
|
31048
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31049
|
+
className: "daw-list-meta",
|
|
31050
|
+
style: { marginTop: 5 },
|
|
31051
|
+
children: t("imageFallbackHint")
|
|
31052
|
+
}),
|
|
31053
|
+
image !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31054
|
+
className: "daw-list-meta",
|
|
31055
|
+
style: { marginTop: 12 },
|
|
31056
|
+
children: t("imageExample", { subdir: image.subdir })
|
|
31057
|
+
}),
|
|
31058
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31059
|
+
className: "daw-form-row",
|
|
31060
|
+
style: {
|
|
31061
|
+
marginTop: 12,
|
|
31062
|
+
gridTemplateColumns: "auto auto minmax(0,1fr)",
|
|
31063
|
+
alignItems: "center"
|
|
31064
|
+
},
|
|
31065
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31066
|
+
className: "daw-command primary",
|
|
31067
|
+
disabled: imageBusy || image === void 0 || imageDraft.trim() === "" || imageDraft.trim() === image.subdir,
|
|
31068
|
+
onClick: () => {
|
|
31069
|
+
setImageBusy(true);
|
|
31070
|
+
setError(void 0);
|
|
31071
|
+
props.api.saveImageSubdir(imageDraft.trim()).then((next) => {
|
|
31072
|
+
setImage(next);
|
|
31073
|
+
setImageDraft(next.subdir);
|
|
31074
|
+
setImageSaved(true);
|
|
31075
|
+
}).catch(setError).finally(() => setImageBusy(false));
|
|
31076
|
+
},
|
|
31077
|
+
children: t("save")
|
|
31078
|
+
}), imageSaved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31079
|
+
className: "daw-pill on",
|
|
31080
|
+
children: t("imageSaved")
|
|
31081
|
+
})]
|
|
31082
|
+
})
|
|
31083
|
+
]
|
|
31084
|
+
}, "image"),
|
|
31085
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31086
|
+
tab: "trash",
|
|
31087
|
+
nameKey: tabLabelKey("trash"),
|
|
31088
|
+
descKey: tabDescKey("trash"),
|
|
31089
|
+
open: isOpen("trash"),
|
|
31090
|
+
onToggle: () => toggleCard("trash"),
|
|
31091
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
30434
31092
|
className: "daw-list-meta",
|
|
30435
31093
|
children: t("pluginTrashHint")
|
|
30436
|
-
}),
|
|
30437
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31094
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30438
31095
|
className: "daw-list",
|
|
30439
31096
|
children: trash.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30440
31097
|
className: "daw-list-row",
|
|
@@ -30465,30 +31122,40 @@ window.__ModuleLoader__.load({
|
|
|
30465
31122
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
30466
31123
|
})] })]
|
|
30467
31124
|
}, item.id))
|
|
31125
|
+
})]
|
|
31126
|
+
}, "trash"),
|
|
31127
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsCard, {
|
|
31128
|
+
tab: "audit",
|
|
31129
|
+
nameKey: tabLabelKey("audit"),
|
|
31130
|
+
descKey: tabDescKey("audit"),
|
|
31131
|
+
open: isOpen("audit"),
|
|
31132
|
+
onToggle: () => toggleCard("audit"),
|
|
31133
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31134
|
+
className: "daw-list",
|
|
31135
|
+
children: audit.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31136
|
+
className: "daw-list-row",
|
|
31137
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31138
|
+
className: "daw-list-title",
|
|
31139
|
+
children: String(item.action ?? t("event"))
|
|
31140
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31141
|
+
className: "daw-list-meta",
|
|
31142
|
+
children: [
|
|
31143
|
+
new Date(Number(item.time ?? 0)).toLocaleString(locale),
|
|
31144
|
+
" · ",
|
|
31145
|
+
String(item.actorType ?? ""),
|
|
31146
|
+
":",
|
|
31147
|
+
String(item.actorId ?? ""),
|
|
31148
|
+
" · ",
|
|
31149
|
+
String(item.relativePath ?? "")
|
|
31150
|
+
]
|
|
31151
|
+
})] })
|
|
31152
|
+
}, String(item.id ?? index)))
|
|
30468
31153
|
})
|
|
30469
|
-
|
|
30470
|
-
|
|
30471
|
-
|
|
30472
|
-
|
|
30473
|
-
|
|
30474
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30475
|
-
className: "daw-list-title",
|
|
30476
|
-
children: String(item.action ?? t("event"))
|
|
30477
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30478
|
-
className: "daw-list-meta",
|
|
30479
|
-
children: [
|
|
30480
|
-
new Date(Number(item.time ?? 0)).toLocaleString(locale),
|
|
30481
|
-
" · ",
|
|
30482
|
-
String(item.actorType ?? ""),
|
|
30483
|
-
":",
|
|
30484
|
-
String(item.actorId ?? ""),
|
|
30485
|
-
" · ",
|
|
30486
|
-
String(item.relativePath ?? "")
|
|
30487
|
-
]
|
|
30488
|
-
})] })
|
|
30489
|
-
}, String(item.id ?? index)))
|
|
30490
|
-
})] }),
|
|
30491
|
-
tab === "app-download" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppDownloadSection, {})
|
|
31154
|
+
}, "audit"),
|
|
31155
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppDownloadSection, {
|
|
31156
|
+
open: isOpen("app-download"),
|
|
31157
|
+
onToggle: () => toggleCard("app-download")
|
|
31158
|
+
})
|
|
30492
31159
|
]
|
|
30493
31160
|
});
|
|
30494
31161
|
}
|
|
@@ -30503,15 +31170,18 @@ window.__ModuleLoader__.load({
|
|
|
30503
31170
|
* screen, so this page is mostly used by the desktop admin to send a code to someone else. Without
|
|
30504
31171
|
* the text there would be no way to type the address in by hand when a camera is unavailable.
|
|
30505
31172
|
*/
|
|
30506
|
-
function AppDownloadSection() {
|
|
31173
|
+
function AppDownloadSection(props) {
|
|
30507
31174
|
const { t } = useWorkspaceI18n();
|
|
30508
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
30509
|
-
|
|
30510
|
-
|
|
31175
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31176
|
+
tab: "app-download",
|
|
31177
|
+
nameKey: tabLabelKey("app-download"),
|
|
31178
|
+
descKey: tabDescKey("app-download"),
|
|
31179
|
+
open: props.open,
|
|
31180
|
+
onToggle: props.onToggle,
|
|
31181
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
30511
31182
|
className: "daw-list-meta",
|
|
30512
31183
|
children: t("appDownloadHint")
|
|
30513
|
-
}),
|
|
30514
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31184
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30515
31185
|
className: "daw-qr-grid",
|
|
30516
31186
|
children: APP_QR_CODES.map((code) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
|
|
30517
31187
|
className: "daw-qr-card",
|
|
@@ -30536,8 +31206,8 @@ window.__ModuleLoader__.load({
|
|
|
30536
31206
|
})
|
|
30537
31207
|
]
|
|
30538
31208
|
}, code.key))
|
|
30539
|
-
})
|
|
30540
|
-
|
|
31209
|
+
})]
|
|
31210
|
+
});
|
|
30541
31211
|
}
|
|
30542
31212
|
/**
|
|
30543
31213
|
* Standalone settings overlay kept for the `/dsh-workspace` page (mobile WebView / direct link).
|
|
@@ -30672,7 +31342,7 @@ window.__ModuleLoader__.load({
|
|
|
30672
31342
|
});
|
|
30673
31343
|
}
|
|
30674
31344
|
function DeviceRow(props) {
|
|
30675
|
-
const { t } = useWorkspaceI18n();
|
|
31345
|
+
const { locale, t } = useWorkspaceI18n();
|
|
30676
31346
|
const [editing, setEditing] = (0, react.useState)(false);
|
|
30677
31347
|
const [scopes, setScopes] = (0, react.useState)(props.device.scopes);
|
|
30678
31348
|
const [rootIds, setRootIds] = (0, react.useState)(props.device.rootIds);
|
|
@@ -30696,7 +31366,7 @@ window.__ModuleLoader__.load({
|
|
|
30696
31366
|
children: [
|
|
30697
31367
|
props.device.id,
|
|
30698
31368
|
" · ",
|
|
30699
|
-
props.device.scopes.
|
|
31369
|
+
props.device.scopes.length === 0 ? t("noScopes") : props.device.scopes.map((scope) => scopeLabel(locale, scope)).join(" · ")
|
|
30700
31370
|
]
|
|
30701
31371
|
}),
|
|
30702
31372
|
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -30707,7 +31377,7 @@ window.__ModuleLoader__.load({
|
|
|
30707
31377
|
type: "checkbox",
|
|
30708
31378
|
checked: scopes.includes(scope),
|
|
30709
31379
|
onChange: () => setScopes((current) => toggle(current, scope))
|
|
30710
|
-
}), scope]
|
|
31380
|
+
}), scopeLabel(locale, scope)]
|
|
30711
31381
|
}, scope))
|
|
30712
31382
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30713
31383
|
className: "daw-scope-grid",
|
|
@@ -30747,6 +31417,7 @@ window.__ModuleLoader__.load({
|
|
|
30747
31417
|
function tabIcon(tab) {
|
|
30748
31418
|
if (tab === "roots") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.HardDrive, { size: 15 });
|
|
30749
31419
|
if (tab === "devices") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Smartphone, { size: 15 });
|
|
31420
|
+
if (tab === "image") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Image, { size: 15 });
|
|
30750
31421
|
if (tab === "trash") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 });
|
|
30751
31422
|
if (tab === "remote") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Upload, { size: 15 });
|
|
30752
31423
|
if (tab === "app-download") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Download, { size: 15 });
|
|
@@ -30772,6 +31443,7 @@ window.__ModuleLoader__.load({
|
|
|
30772
31443
|
if (error.status === 401) return "errorUnauthorized";
|
|
30773
31444
|
if (error.status === 429) return "errorRateLimited";
|
|
30774
31445
|
if (error.code === "ROOT_NOT_GRANTED" || error.code === "ROOT_FORBIDDEN") return "errorRootDenied";
|
|
31446
|
+
if (error.code === "ROOT_BUILTIN") return "errorRootBuiltin";
|
|
30775
31447
|
if (error.code === "ETAG_MISMATCH") return "errorEtag";
|
|
30776
31448
|
if (error.code === "LISTENER_HOST_INVALID") return "listenerHostInvalid";
|
|
30777
31449
|
if (error.code === "LISTENER_PORT_INVALID") return "listenerPortInvalid";
|
|
@@ -30794,9 +31466,17 @@ window.__ModuleLoader__.load({
|
|
|
30794
31466
|
//#region src/client/index.tsx
|
|
30795
31467
|
const DEFAULT_SECTION = DEFAULT_PANEL_SECTION;
|
|
30796
31468
|
const api = new WorkspaceApi("/dsh-workspace-api/api/v1", "/dsh-workspace-api/manage", void 0, true);
|
|
30797
|
-
const inject = ["slots"];
|
|
31469
|
+
const inject = ["slots", "uiConversation"];
|
|
30798
31470
|
function apply(ctx) {
|
|
30799
31471
|
installWorkspaceStyles();
|
|
31472
|
+
ctx.effect(() => {
|
|
31473
|
+
ctx.uiConversation.events.register(handoffDefinition);
|
|
31474
|
+
return ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
|
|
31475
|
+
name: "conversation.chat.turnTail",
|
|
31476
|
+
id: "dsh-remote-bridge-handoff",
|
|
31477
|
+
order: 40
|
|
31478
|
+
}, HandoffTurnTail));
|
|
31479
|
+
}, "dsh remote bridge handoff gallery");
|
|
30800
31480
|
ctx.effect(() => localizedSlot(ctx, "settings.section", {
|
|
30801
31481
|
name: "settings.section",
|
|
30802
31482
|
id: "dsh-remote-bridge",
|