dsh-mobile 0.6.15 → 0.6.17
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/CHANGELOG.md +25 -0
- package/CONTRIBUTORS.md +24 -2
- package/README.en.md +8 -6
- package/README.md +8 -6
- package/SECURITY.md +1 -0
- package/apps/mobile/README.md +29 -5
- package/apps/mobile/README.zh-CN.md +29 -5
- package/apps/mobile/release.json +3 -3
- package/cordis.patch.yml +10 -17
- package/docs/ATTACH_EXISTING_FRPS.en.md +8 -0
- package/docs/ATTACH_EXISTING_FRPS.md +8 -0
- package/docs/CLIENT_MODULES.en.md +10 -2
- package/docs/CLIENT_MODULES.md +10 -2
- package/docs/SLOW_CONNECTIONS.en.md +11 -1
- package/docs/SLOW_CONNECTIONS.md +11 -1
- package/lib/client.js +1771 -89
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +54 -1
- package/lib/index.mjs +621 -44
- package/lib/index.mjs.map +1 -1
- package/package.json +8 -2
- package/packages/question-fixes/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -6,6 +6,44 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
8
|
//#region src/client-messages.ts
|
|
9
|
+
const frpFormMessages = {
|
|
10
|
+
en: {
|
|
11
|
+
frpServerAddressInvalid: "Enter the frps IP address or full hostname only, without https://, a port, or a path.",
|
|
12
|
+
frpServerPortInvalid: "Enter the frps control port as a whole number from 1 to 65535.",
|
|
13
|
+
frpTokenInvalid: "Enter a shared token of 16–512 characters without spaces or line breaks. Re-enter it when changing saved connection settings.",
|
|
14
|
+
frpPublicOriginInvalid: "Enter an HTTPS address such as https://phone.example.com or https://1.2.3.4, without a path, credentials, or a non-standard port. An IPv4 address must be publicly routable.",
|
|
15
|
+
frpPublicOriginHint: "Include https://. Do not include a path or non-standard port. For self-signed ingress, enter https://PUBLIC_IP here and set its port separately below.",
|
|
16
|
+
frpModeInvalid: "Choose either managed deployment or an existing frps.",
|
|
17
|
+
frpVhostPortInvalid: "Enter the real frps vhostHTTPPort as a whole number from 1 to 65535.",
|
|
18
|
+
frpPublicPortInvalid: "Enter the public ingress port as a whole number from 1 to 65535, excluding 3080, 3443, and 3444. Keep this port out of the HTTPS address above.",
|
|
19
|
+
frpActionBusy: "Another remote operation is in progress. Wait for it to finish before saving.",
|
|
20
|
+
frpInstallBeforeConnect: "Install the local frpc component in step 3 before saving and verifying the connection."
|
|
21
|
+
},
|
|
22
|
+
it: {
|
|
23
|
+
frpServerAddressInvalid: "Inserisci solo l’indirizzo IP o il nome host completo di frps, senza https://, porta o percorso.",
|
|
24
|
+
frpServerPortInvalid: "Inserisci la porta di controllo frps come numero intero da 1 a 65535.",
|
|
25
|
+
frpTokenInvalid: "Inserisci un token condiviso di 16–512 caratteri senza spazi o ritorni a capo. Reinseriscilo quando modifichi una connessione salvata.",
|
|
26
|
+
frpPublicOriginInvalid: "Inserisci un indirizzo HTTPS come https://phone.example.com o https://1.2.3.4, senza percorso, credenziali o porta non standard. Un indirizzo IPv4 deve essere pubblico e instradabile.",
|
|
27
|
+
frpPublicOriginHint: "Includi https://, senza percorso o porta non standard. Per l’ingresso autofirmato usa https://IP_PUBBLICO e indica la porta separatamente qui sotto.",
|
|
28
|
+
frpModeInvalid: "Scegli la distribuzione gestita oppure un frps esistente.",
|
|
29
|
+
frpVhostPortInvalid: "Inserisci il vero vhostHTTPPort di frps come numero intero da 1 a 65535.",
|
|
30
|
+
frpPublicPortInvalid: "Inserisci una porta pubblica intera da 1 a 65535, esclusi 3080, 3443 e 3444. Non aggiungerla all’indirizzo HTTPS sopra.",
|
|
31
|
+
frpActionBusy: "È in corso un’altra operazione remota. Attendi prima di salvare.",
|
|
32
|
+
frpInstallBeforeConnect: "Installa il componente frpc locale al punto 3 prima di salvare e verificare la connessione."
|
|
33
|
+
},
|
|
34
|
+
zh: {
|
|
35
|
+
frpServerAddressInvalid: "请只填写 frps 的 IP 或完整域名,不要包含 https://、端口或路径。",
|
|
36
|
+
frpServerPortInvalid: "请填写 1–65535 的整数,作为 frps 控制端口。",
|
|
37
|
+
frpTokenInvalid: "共享 Token 必须为 16–512 个字符,不能包含空格或换行。修改已保存的连接信息时,请重新输入 Token。",
|
|
38
|
+
frpPublicOriginInvalid: "请填写 https://phone.example.com 或 https://1.2.3.4 形式的 HTTPS 地址,不含路径、账号密码或非标准端口;IPv4 必须为可路由的公网地址。",
|
|
39
|
+
frpPublicOriginHint: "必须包含 https://,不要带路径或非标准端口。自签入口在此填 https://公网IP,端口在下方单独填写。",
|
|
40
|
+
frpModeInvalid: "请选择“由插件部署 frps”或“接入我已有的 frps”。",
|
|
41
|
+
frpVhostPortInvalid: "请填写你 frps 真实的 vhostHTTPPort,必须为 1–65535 的整数。",
|
|
42
|
+
frpPublicPortInvalid: "公网入口端口必须为 1–65535 的整数,不能使用 3080、3443、3444;不要把它加到上方 HTTPS 地址中。",
|
|
43
|
+
frpActionBusy: "另一项远程操作正在进行,请等待完成后再保存。",
|
|
44
|
+
frpInstallBeforeConnect: "请先在第 3 步安装本机 frpc 组件,再保存并验证连接。"
|
|
45
|
+
}
|
|
46
|
+
};
|
|
9
47
|
const storageMessages = {
|
|
10
48
|
en: {
|
|
11
49
|
funnelStateInvalid: "The private Tailscale state folder cannot be accessed safely. Check its permissions or directory links; reinstalling the component is not required.",
|
|
@@ -61,6 +99,7 @@ window.__ModuleLoader__.load({
|
|
|
61
99
|
};
|
|
62
100
|
const MOBILE_CONTROL_MESSAGES = {
|
|
63
101
|
en: {
|
|
102
|
+
...frpFormMessages.en,
|
|
64
103
|
mobileAccess: "Mobile access",
|
|
65
104
|
collapseMobileAccess: "Collapse Mobile access",
|
|
66
105
|
downloadAndroid: "Download latest Android app",
|
|
@@ -229,6 +268,7 @@ window.__ModuleLoader__.load({
|
|
|
229
268
|
wsPathsAdvancedHint: "Manual entries are rarely necessary: use the buttons above. Only fill this in when you know the exact path."
|
|
230
269
|
},
|
|
231
270
|
it: {
|
|
271
|
+
...frpFormMessages.it,
|
|
232
272
|
mobileAccess: "Accesso mobile",
|
|
233
273
|
collapseMobileAccess: "Riduci Accesso mobile",
|
|
234
274
|
downloadAndroid: "Scarica l’ultima app Android",
|
|
@@ -396,7 +436,7 @@ window.__ModuleLoader__.load({
|
|
|
396
436
|
wsPathsAnnounce: "{n} connessioni di plugin in attesa di approvazione",
|
|
397
437
|
wsPathsAdvancedHint: "Le voci manuali servono raramente: usa i pulsanti qui sopra. Compila solo se conosci il percorso esatto."
|
|
398
438
|
},
|
|
399
|
-
zh: {}
|
|
439
|
+
zh: { ...frpFormMessages.zh }
|
|
400
440
|
};
|
|
401
441
|
for (const locale of [
|
|
402
442
|
"en",
|
|
@@ -2494,6 +2534,85 @@ window.__ModuleLoader__.load({
|
|
|
2494
2534
|
return createFrpAttachTemplateParts(settings, options).text;
|
|
2495
2535
|
}
|
|
2496
2536
|
//#endregion
|
|
2537
|
+
//#region src/frp-form-validation.ts
|
|
2538
|
+
/** Field ownership for errors returned by the Host parser. */
|
|
2539
|
+
const FRP_FORM_ERRORS = {
|
|
2540
|
+
frp_server_address_invalid: {
|
|
2541
|
+
field: "serverAddress",
|
|
2542
|
+
code: "frp_server_address_invalid",
|
|
2543
|
+
messageKey: "frpServerAddressInvalid"
|
|
2544
|
+
},
|
|
2545
|
+
frp_server_port_invalid: {
|
|
2546
|
+
field: "serverPort",
|
|
2547
|
+
code: "frp_server_port_invalid",
|
|
2548
|
+
messageKey: "frpServerPortInvalid"
|
|
2549
|
+
},
|
|
2550
|
+
frp_token_invalid: {
|
|
2551
|
+
field: "token",
|
|
2552
|
+
code: "frp_token_invalid",
|
|
2553
|
+
messageKey: "frpTokenInvalid"
|
|
2554
|
+
},
|
|
2555
|
+
frp_public_origin_invalid: {
|
|
2556
|
+
field: "publicOrigin",
|
|
2557
|
+
code: "frp_public_origin_invalid",
|
|
2558
|
+
messageKey: "frpPublicOriginInvalid"
|
|
2559
|
+
},
|
|
2560
|
+
frp_attach_mode_requires_vhost_port: {
|
|
2561
|
+
field: "vhostHttpPort",
|
|
2562
|
+
code: "frp_attach_mode_requires_vhost_port",
|
|
2563
|
+
messageKey: "frpAttachModeRequiresVhostPort"
|
|
2564
|
+
},
|
|
2565
|
+
frp_entry_tls_invalid: {
|
|
2566
|
+
field: "entryTls",
|
|
2567
|
+
code: "frp_entry_tls_invalid",
|
|
2568
|
+
messageKey: "frpEntryTlsInvalid"
|
|
2569
|
+
},
|
|
2570
|
+
frp_self_signed_requires_public_ipv4: {
|
|
2571
|
+
field: "publicOrigin",
|
|
2572
|
+
code: "frp_self_signed_requires_public_ipv4",
|
|
2573
|
+
messageKey: "frpSelfSignedRequiresPublicIpv4"
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
/** Collect field errors using the Host validators; token omission is permitted only for saved configuration. */
|
|
2577
|
+
function validateFrpForm(form, allowSavedToken = false) {
|
|
2578
|
+
const issues = [];
|
|
2579
|
+
const check = (field, messageKey, validate) => {
|
|
2580
|
+
try {
|
|
2581
|
+
validate();
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
issues.push({
|
|
2584
|
+
field,
|
|
2585
|
+
messageKey,
|
|
2586
|
+
code: error instanceof Error ? error.message : "frp_settings_invalid"
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
};
|
|
2590
|
+
check("serverAddress", "frpServerAddressInvalid", () => validateFrpServerAddress(form.serverAddress));
|
|
2591
|
+
check("serverPort", "frpServerPortInvalid", () => validateFrpServerPort(form.serverPort));
|
|
2592
|
+
if (form.token !== "" || !allowSavedToken) check("token", "frpTokenInvalid", () => validateFrpToken(form.token));
|
|
2593
|
+
check("publicOrigin", "frpPublicOriginInvalid", () => validateFrpPublicOrigin(form.publicOrigin));
|
|
2594
|
+
check("mode", "frpModeInvalid", () => validateFrpMode(form.mode));
|
|
2595
|
+
check("entryTls", "frpEntryTlsInvalid", () => validateFrpEntryTls(form.entryTls));
|
|
2596
|
+
if (form.entryTls === "self-signed" && form.mode !== "attach") issues.push(FRP_FORM_ERRORS.frp_entry_tls_invalid);
|
|
2597
|
+
if (form.entryTls !== "self-signed" && form.vhostHttpPort !== void 0) check("vhostHttpPort", "frpVhostPortInvalid", () => validateFrpVhostHttpPort(form.vhostHttpPort));
|
|
2598
|
+
else if (form.entryTls !== "self-signed" && form.mode === "attach") issues.push(FRP_FORM_ERRORS.frp_attach_mode_requires_vhost_port);
|
|
2599
|
+
if (form.publicPort !== void 0 || form.entryTls === "self-signed") check("publicPort", "frpPublicPortInvalid", () => validateFrpPublicPort(form.publicPort));
|
|
2600
|
+
if (issues.length === 0) try {
|
|
2601
|
+
parseFrpSettings({
|
|
2602
|
+
...form,
|
|
2603
|
+
token: form.token === "" && allowSavedToken ? "0123456789abcdef" : form.token
|
|
2604
|
+
});
|
|
2605
|
+
} catch (error) {
|
|
2606
|
+
const code = error instanceof Error ? error.message : "frp_settings_invalid";
|
|
2607
|
+
issues.push(FRP_FORM_ERRORS[code] ?? {
|
|
2608
|
+
field: "mode",
|
|
2609
|
+
code,
|
|
2610
|
+
messageKey: "frpInputInvalid"
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
return issues;
|
|
2614
|
+
}
|
|
2615
|
+
//#endregion
|
|
2497
2616
|
//#region node_modules/@noble/hashes/esm/utils.js
|
|
2498
2617
|
/*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
2499
2618
|
/** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */
|
|
@@ -4362,14 +4481,27 @@ window.__ModuleLoader__.load({
|
|
|
4362
4481
|
icon.append(body, lens);
|
|
4363
4482
|
return icon;
|
|
4364
4483
|
};
|
|
4365
|
-
const createMediaAction = (text) => {
|
|
4484
|
+
const createMediaAction = (text, kind = "camera") => {
|
|
4366
4485
|
const button = document.createElement("button");
|
|
4367
4486
|
button.type = "button";
|
|
4368
4487
|
button.className = "dsh-mobile-media-action";
|
|
4369
4488
|
button.lang = language;
|
|
4370
|
-
button.dataset.dshMobileMediaAction =
|
|
4489
|
+
button.dataset.dshMobileMediaAction = kind;
|
|
4490
|
+
button.setAttribute("role", "option");
|
|
4491
|
+
button.setAttribute("aria-selected", "false");
|
|
4492
|
+
button.setAttribute("aria-label", text);
|
|
4371
4493
|
const icon = document.createElement("span");
|
|
4372
|
-
|
|
4494
|
+
const graphic = mediaIcon();
|
|
4495
|
+
if (kind === "gallery") {
|
|
4496
|
+
graphic.replaceChildren();
|
|
4497
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
4498
|
+
path.setAttribute("d", "M2 2h12v12H2z M2 11l4-4 3 3 2-2 3 3 M10 5h.01");
|
|
4499
|
+
path.setAttribute("stroke", "currentColor");
|
|
4500
|
+
path.setAttribute("stroke-width", "1.3");
|
|
4501
|
+
path.setAttribute("stroke-linejoin", "round");
|
|
4502
|
+
graphic.append(path);
|
|
4503
|
+
}
|
|
4504
|
+
icon.append(graphic);
|
|
4373
4505
|
button.append(icon);
|
|
4374
4506
|
const caption = document.createElement("span");
|
|
4375
4507
|
caption.textContent = text;
|
|
@@ -4462,6 +4594,9 @@ window.__ModuleLoader__.load({
|
|
|
4462
4594
|
document.body.append(mediaToast);
|
|
4463
4595
|
const cameraButton = createMediaAction(label("Scatta foto", "Take photo", "拍照"));
|
|
4464
4596
|
cameraButton.disabled = true;
|
|
4597
|
+
const galleryButton = createMediaAction(label("Galleria", "Gallery", "相册"), "gallery");
|
|
4598
|
+
galleryButton.disabled = true;
|
|
4599
|
+
let galleryBusy = false;
|
|
4465
4600
|
let branchToastTimer = 0;
|
|
4466
4601
|
let mediaToastTimer = 0;
|
|
4467
4602
|
const showMediaToast = (message) => {
|
|
@@ -4549,40 +4684,48 @@ window.__ModuleLoader__.load({
|
|
|
4549
4684
|
const sectionTitle = Array.from(commandMenu.querySelectorAll("[role=\"listbox\"] [role=\"presentation\"]")).find((candidate) => !candidate.hasAttribute("data-source") && candidate.textContent?.trim() === addGroupLabel);
|
|
4550
4685
|
if (sectionTitle === void 0) {
|
|
4551
4686
|
cameraButton.remove();
|
|
4687
|
+
galleryButton.remove();
|
|
4552
4688
|
return false;
|
|
4553
4689
|
}
|
|
4554
|
-
cameraButton.className = "dsh-mobile-media-action";
|
|
4555
4690
|
const nativeItem = (() => {
|
|
4556
4691
|
let sibling = sectionTitle.nextElementSibling;
|
|
4557
4692
|
while (sibling instanceof HTMLElement && sibling.getAttribute("role") !== "presentation") {
|
|
4558
|
-
if (sibling.matches("button[role=\"option\"]:not([aria-selected=\"true\"])")) return sibling;
|
|
4693
|
+
if (!sibling.hasAttribute("data-dsh-mobile-media-action") && sibling.matches("button[role=\"option\"]:not([aria-selected=\"true\"])")) return sibling;
|
|
4559
4694
|
sibling = sibling.nextElementSibling;
|
|
4560
4695
|
}
|
|
4561
4696
|
})();
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
const
|
|
4566
|
-
const
|
|
4567
|
-
const
|
|
4568
|
-
|
|
4569
|
-
if (
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
cameraButton.setAttribute("aria-selected", "false");
|
|
4573
|
-
cameraButton.setAttribute("aria-label", label("Scatta foto", "Take photo", "拍照"));
|
|
4697
|
+
for (const button of [cameraButton, galleryButton]) {
|
|
4698
|
+
const className = nativeItem?.className ?? "dsh-mobile-media-action";
|
|
4699
|
+
if (button.className !== className) button.className = className;
|
|
4700
|
+
const icon = button.firstElementChild;
|
|
4701
|
+
const caption = button.lastElementChild;
|
|
4702
|
+
const nativeIcon = nativeItem === void 0 ? void 0 : firstByClassSuffix(nativeItem, "_itemIcon");
|
|
4703
|
+
const nativeLabel = nativeItem === void 0 ? void 0 : firstByClassSuffix(nativeItem, "_itemName");
|
|
4704
|
+
if (icon instanceof HTMLElement && nativeIcon !== void 0 && icon.className !== nativeIcon.className) icon.className = nativeIcon.className;
|
|
4705
|
+
if (caption instanceof HTMLElement && nativeLabel !== void 0 && caption.className !== nativeLabel.className) caption.className = nativeLabel.className;
|
|
4706
|
+
}
|
|
4574
4707
|
let lastItem = sectionTitle;
|
|
4575
4708
|
let sibling = sectionTitle.nextElementSibling;
|
|
4576
4709
|
while (sibling instanceof HTMLElement && sibling.getAttribute("role") !== "presentation") {
|
|
4577
|
-
if (sibling !== cameraButton) lastItem = sibling;
|
|
4710
|
+
if (sibling !== cameraButton && sibling !== galleryButton) lastItem = sibling;
|
|
4578
4711
|
sibling = sibling.nextElementSibling;
|
|
4579
4712
|
}
|
|
4580
|
-
if (
|
|
4713
|
+
if (boundComposer?.closest(".dshm-main") !== null && boundComposer !== null) {
|
|
4714
|
+
if (galleryButton.parentElement !== sectionTitle.parentElement || galleryButton.previousElementSibling !== sectionTitle) sectionTitle.after(galleryButton);
|
|
4715
|
+
if (cameraButton.parentElement !== galleryButton.parentElement || cameraButton.previousElementSibling !== galleryButton) galleryButton.after(cameraButton);
|
|
4716
|
+
} else {
|
|
4717
|
+
galleryButton.remove();
|
|
4718
|
+
if (cameraButton.parentElement !== sectionTitle.parentElement || cameraButton.previousElementSibling !== lastItem) lastItem.after(cameraButton);
|
|
4719
|
+
}
|
|
4581
4720
|
return true;
|
|
4582
4721
|
};
|
|
4583
4722
|
const setCameraActionDisabled = (disabled, title) => {
|
|
4584
4723
|
if (cameraButton.disabled !== disabled) cameraButton.disabled = disabled;
|
|
4585
4724
|
if (cameraButton.title !== title) cameraButton.title = title;
|
|
4725
|
+
const galleryDisabled = disabled || galleryBusy;
|
|
4726
|
+
if (galleryButton.disabled !== galleryDisabled) galleryButton.disabled = galleryDisabled;
|
|
4727
|
+
const galleryTitle = disabled ? title : label("Scegli immagini da allegare", "Choose images to attach", "选择图片并附加到当前消息");
|
|
4728
|
+
if (galleryButton.title !== galleryTitle) galleryButton.title = galleryTitle;
|
|
4586
4729
|
};
|
|
4587
4730
|
const quietMediaPointer = (event) => {
|
|
4588
4731
|
event.preventDefault();
|
|
@@ -4614,6 +4757,34 @@ window.__ModuleLoader__.load({
|
|
|
4614
4757
|
showMediaToast(code === "payload_too_large" ? label("La foto supera il limite di 8 MiB", "The photo exceeds the 8 MiB limit", "照片超过 8 MiB 限制") : label("Impossibile aggiungere la foto", "Could not attach the photo", "无法附加照片"));
|
|
4615
4758
|
});
|
|
4616
4759
|
};
|
|
4760
|
+
const chooseGallery = () => {
|
|
4761
|
+
if (disposed || galleryBusy || galleryButton.disabled || boundComposer?.closest(".dshm-main") == null) return;
|
|
4762
|
+
if (!canAcceptComposerDrop()) {
|
|
4763
|
+
setCameraActionDisabled(true, label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
|
|
4764
|
+
dismissCommandMenu();
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4767
|
+
const context = mediaRequestContext();
|
|
4768
|
+
galleryBusy = true;
|
|
4769
|
+
galleryButton.disabled = true;
|
|
4770
|
+
galleryButton.setAttribute("aria-busy", "true");
|
|
4771
|
+
dismissCommandMenu();
|
|
4772
|
+
pickBrowserFiles({
|
|
4773
|
+
accept: "image/*",
|
|
4774
|
+
multiple: true,
|
|
4775
|
+
signal: mediaPickerAbortController.signal
|
|
4776
|
+
}).then((files) => {
|
|
4777
|
+
if (files !== void 0) deliverImages(files, context);
|
|
4778
|
+
}).catch((_error) => {
|
|
4779
|
+
if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aggiungere le immagini", "Could not attach the images", "无法附加图片"));
|
|
4780
|
+
}).finally(() => {
|
|
4781
|
+
galleryBusy = false;
|
|
4782
|
+
galleryButton.removeAttribute("aria-busy");
|
|
4783
|
+
if (!disposed) schedule();
|
|
4784
|
+
});
|
|
4785
|
+
};
|
|
4786
|
+
galleryButton.addEventListener("pointerdown", quietMediaPointer);
|
|
4787
|
+
galleryButton.addEventListener("click", chooseGallery);
|
|
4617
4788
|
cameraButton.addEventListener("pointerdown", quietMediaPointer);
|
|
4618
4789
|
cameraButton.addEventListener("click", takePhoto);
|
|
4619
4790
|
const showBranchToast = () => {
|
|
@@ -4878,6 +5049,7 @@ window.__ModuleLoader__.load({
|
|
|
4878
5049
|
syncMediaBinding(null);
|
|
4879
5050
|
setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
|
|
4880
5051
|
cameraButton.remove();
|
|
5052
|
+
galleryButton.remove();
|
|
4881
5053
|
return;
|
|
4882
5054
|
}
|
|
4883
5055
|
if (center !== void 0) {
|
|
@@ -4917,6 +5089,7 @@ window.__ModuleLoader__.load({
|
|
|
4917
5089
|
syncMediaBinding(null);
|
|
4918
5090
|
setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
|
|
4919
5091
|
cameraButton.remove();
|
|
5092
|
+
galleryButton.remove();
|
|
4920
5093
|
}
|
|
4921
5094
|
if (composerRow instanceof HTMLElement) {
|
|
4922
5095
|
composerRow.dataset.dshMobileComposerRow = "true";
|
|
@@ -4936,7 +5109,10 @@ window.__ModuleLoader__.load({
|
|
|
4936
5109
|
setCameraActionDisabled(mediaDisabled, mediaTitle);
|
|
4937
5110
|
const commandMenu = composerCard?.querySelector("[data-trigger-menu]") ?? null;
|
|
4938
5111
|
if (commandMenu !== null) placeCameraAction(commandMenu);
|
|
4939
|
-
else
|
|
5112
|
+
else {
|
|
5113
|
+
cameraButton.remove();
|
|
5114
|
+
galleryButton.remove();
|
|
5115
|
+
}
|
|
4940
5116
|
} else syncComposerEnterNewline(null);
|
|
4941
5117
|
if (composerTrailing !== void 0 && composerTrailing !== composerTools) {
|
|
4942
5118
|
composerTrailing.dataset.dshMobileComposerTrailing = "true";
|
|
@@ -5052,6 +5228,9 @@ window.__ModuleLoader__.load({
|
|
|
5052
5228
|
applyStrip(0);
|
|
5053
5229
|
cameraButton.removeEventListener("pointerdown", quietMediaPointer);
|
|
5054
5230
|
cameraButton.removeEventListener("click", takePhoto);
|
|
5231
|
+
galleryButton.removeEventListener("pointerdown", quietMediaPointer);
|
|
5232
|
+
galleryButton.removeEventListener("click", chooseGallery);
|
|
5233
|
+
galleryButton.remove();
|
|
5055
5234
|
if (branchToastTimer !== 0) window.clearTimeout(branchToastTimer);
|
|
5056
5235
|
if (mediaToastTimer !== 0) window.clearTimeout(mediaToastTimer);
|
|
5057
5236
|
branchToast.remove();
|
|
@@ -5184,6 +5363,363 @@ window.__ModuleLoader__.load({
|
|
|
5184
5363
|
release();
|
|
5185
5364
|
};
|
|
5186
5365
|
}
|
|
5366
|
+
const LEASE_MS = 3e4;
|
|
5367
|
+
function transferableText(text) {
|
|
5368
|
+
return !/[\uE100-\uE11D\uFFFC]/u.test(text) && new TextEncoder().encode(text).length <= 262144;
|
|
5369
|
+
}
|
|
5370
|
+
function snapshotShape(value) {
|
|
5371
|
+
if (value === null || typeof value !== "object") return false;
|
|
5372
|
+
const s = value;
|
|
5373
|
+
return s.version === 1 && typeof s.token === "string" && /^[a-f0-9]{48}$/u.test(s.token) && typeof s.href === "string" && s.href.length <= 8192 && typeof s.sessionId === "string" && s.sessionId.length > 0 && s.sessionId.length <= 1024 && typeof s.text === "string" && transferableText(s.text) && Number.isSafeInteger(s.revision) && (s.revision ?? -1) >= 0;
|
|
5374
|
+
}
|
|
5375
|
+
function plain(s) {
|
|
5376
|
+
return typeof s.draft === "string" && Number.isSafeInteger(s.draftRev) && s.draftRev >= 0 && s.phase === "plain" && s.claim === void 0 && Array.isArray(s.attachmentIds) && s.attachmentIds.length === 0 && Array.isArray(s.occurrences) && s.occurrences.length === 0 && Array.isArray(s.queue) && s.queue.length === 0;
|
|
5377
|
+
}
|
|
5378
|
+
/** Public state subscriptions are kept for the lifetime of each observed composer,
|
|
5379
|
+
* not just the lease: attachment/submit uncertainty stays sticky until reload. */
|
|
5380
|
+
function createDraftTransfer(view, page) {
|
|
5381
|
+
const observed = /* @__PURE__ */ new Map();
|
|
5382
|
+
const subscriptions = /* @__PURE__ */ new Set();
|
|
5383
|
+
let epoch = 0, risky = false, disposed = false, composing = false;
|
|
5384
|
+
let lease;
|
|
5385
|
+
let timer;
|
|
5386
|
+
let imported;
|
|
5387
|
+
let importTimer;
|
|
5388
|
+
let importClosed = false;
|
|
5389
|
+
const clearImport = () => {
|
|
5390
|
+
if (imported !== void 0) importClosed = true;
|
|
5391
|
+
imported = void 0;
|
|
5392
|
+
if (importTimer !== void 0) view.clearTimeout(importTimer);
|
|
5393
|
+
importTimer = void 0;
|
|
5394
|
+
};
|
|
5395
|
+
const invalidate = () => {
|
|
5396
|
+
epoch += 1;
|
|
5397
|
+
};
|
|
5398
|
+
const clearLease = () => {
|
|
5399
|
+
lease = void 0;
|
|
5400
|
+
if (timer !== void 0) view.clearTimeout(timer);
|
|
5401
|
+
timer = void 0;
|
|
5402
|
+
};
|
|
5403
|
+
const risk = () => {
|
|
5404
|
+
risky = true;
|
|
5405
|
+
invalidate();
|
|
5406
|
+
};
|
|
5407
|
+
const onActivity = (event) => {
|
|
5408
|
+
const inComposer = Boolean(event.target?.closest?.("[data-composer-card]"));
|
|
5409
|
+
if ([
|
|
5410
|
+
"input",
|
|
5411
|
+
"beforeinput",
|
|
5412
|
+
"paste",
|
|
5413
|
+
"compositionstart",
|
|
5414
|
+
"compositionend"
|
|
5415
|
+
].includes(event.type) && !inComposer) {
|
|
5416
|
+
risk();
|
|
5417
|
+
return;
|
|
5418
|
+
}
|
|
5419
|
+
if (event.type === "compositionstart" || event.type === "compositionend") {
|
|
5420
|
+
composing = event.type === "compositionstart";
|
|
5421
|
+
invalidate();
|
|
5422
|
+
return;
|
|
5423
|
+
}
|
|
5424
|
+
if (event.type === "change" && event.target?.type !== "file") return;
|
|
5425
|
+
if (event.type === "paste") {
|
|
5426
|
+
const clipboard = event.clipboardData;
|
|
5427
|
+
if (clipboard !== null && clipboard !== void 0 && clipboard.files.length === 0 && clipboard.types.length === 1 && clipboard.types[0] === "text/plain" && Array.from(clipboard.items).every((item) => item.kind === "string" && item.type === "text/plain")) {
|
|
5428
|
+
invalidate();
|
|
5429
|
+
return;
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
if (event.type === "input" || event.type === "beforeinput") invalidate();
|
|
5433
|
+
else risk();
|
|
5434
|
+
};
|
|
5435
|
+
const onClick = (event) => {
|
|
5436
|
+
if (event.target?.closest?.("[data-composer-card] button, [data-dsh-mobile-media-action], [data-voice-activity]")) risk();
|
|
5437
|
+
};
|
|
5438
|
+
const checkVoice = () => {
|
|
5439
|
+
if (page.querySelector("[data-voice-activity=\"requesting\"], [data-voice-activity=\"recording\"], [data-voice-activity=\"transcribing\"]") !== null) risk();
|
|
5440
|
+
};
|
|
5441
|
+
const voiceObserver = new MutationObserver(checkVoice);
|
|
5442
|
+
voiceObserver.observe(page.documentElement, {
|
|
5443
|
+
subtree: true,
|
|
5444
|
+
childList: true,
|
|
5445
|
+
attributes: true,
|
|
5446
|
+
attributeFilter: ["data-voice-activity"]
|
|
5447
|
+
});
|
|
5448
|
+
checkVoice();
|
|
5449
|
+
page.addEventListener("click", onClick, true);
|
|
5450
|
+
const events = [
|
|
5451
|
+
"compositionstart",
|
|
5452
|
+
"compositionend",
|
|
5453
|
+
"drop",
|
|
5454
|
+
"paste",
|
|
5455
|
+
"change",
|
|
5456
|
+
"submit",
|
|
5457
|
+
"input",
|
|
5458
|
+
"beforeinput",
|
|
5459
|
+
"dsh-mobile:voice-start",
|
|
5460
|
+
"dsh-mobile:voice-stop"
|
|
5461
|
+
];
|
|
5462
|
+
for (const name of events) page.addEventListener(name, onActivity, true);
|
|
5463
|
+
const current = () => {
|
|
5464
|
+
if (disposed || risky || composing || observed.size !== 1) return void 0;
|
|
5465
|
+
for (const modal of page.querySelectorAll("[role=\"dialog\"][aria-modal=\"true\"]")) if (!modal.hidden && !modal.closest("[hidden], [inert], [aria-hidden=\"true\"]") && modal.getClientRects().length > 0) {
|
|
5466
|
+
const style = view.getComputedStyle(modal);
|
|
5467
|
+
if (style.display !== "none" && style.visibility !== "hidden") return void 0;
|
|
5468
|
+
}
|
|
5469
|
+
const entry = observed.values().next().value;
|
|
5470
|
+
if (entry === void 0 || !plain(entry.input.state.getSnapshot())) return void 0;
|
|
5471
|
+
return entry;
|
|
5472
|
+
};
|
|
5473
|
+
const api = {
|
|
5474
|
+
capture() {
|
|
5475
|
+
if (lease !== void 0) return null;
|
|
5476
|
+
const entry = current();
|
|
5477
|
+
if (entry === void 0) return null;
|
|
5478
|
+
const state = entry.input.state.getSnapshot();
|
|
5479
|
+
if (!transferableText(state.draft)) return null;
|
|
5480
|
+
const bytes = /* @__PURE__ */ new Uint8Array(24);
|
|
5481
|
+
try {
|
|
5482
|
+
view.crypto.getRandomValues(bytes);
|
|
5483
|
+
} catch {
|
|
5484
|
+
return null;
|
|
5485
|
+
}
|
|
5486
|
+
lease = {
|
|
5487
|
+
version: 1,
|
|
5488
|
+
token: Array.from(bytes, (n) => n.toString(16).padStart(2, "0")).join(""),
|
|
5489
|
+
href: view.location.href,
|
|
5490
|
+
sessionId: entry.sessionId,
|
|
5491
|
+
text: state.draft,
|
|
5492
|
+
revision: epoch
|
|
5493
|
+
};
|
|
5494
|
+
timer = view.setTimeout(clearLease, LEASE_MS);
|
|
5495
|
+
return { ...lease };
|
|
5496
|
+
},
|
|
5497
|
+
validate(value) {
|
|
5498
|
+
if (!snapshotShape(value) || lease === void 0) return false;
|
|
5499
|
+
const entry = current();
|
|
5500
|
+
return entry !== void 0 && value.token === lease.token && value.href === lease.href && value.href === view.location.href && value.sessionId === lease.sessionId && value.sessionId === entry.sessionId && value.revision === lease.revision && value.revision === epoch && value.text === lease.text && value.text === entry.input.state.getSnapshot().draft;
|
|
5501
|
+
},
|
|
5502
|
+
restore(value) {
|
|
5503
|
+
if (!snapshotShape(value)) return false;
|
|
5504
|
+
if (imported !== void 0) return api.verify(value);
|
|
5505
|
+
if (importClosed) return false;
|
|
5506
|
+
const entry = current();
|
|
5507
|
+
if (entry === void 0 || entry.sessionId !== value.sessionId) return false;
|
|
5508
|
+
const state = entry.input.state.getSnapshot();
|
|
5509
|
+
if (state.draft !== "" && state.draft !== value.text) return false;
|
|
5510
|
+
const wrote = state.draft !== value.text;
|
|
5511
|
+
imported = {
|
|
5512
|
+
snapshot: { ...value },
|
|
5513
|
+
entry,
|
|
5514
|
+
epoch,
|
|
5515
|
+
href: view.location.href,
|
|
5516
|
+
wrote,
|
|
5517
|
+
actualText: state.draft,
|
|
5518
|
+
successful: false
|
|
5519
|
+
};
|
|
5520
|
+
importTimer = view.setTimeout(clearImport, LEASE_MS);
|
|
5521
|
+
let writeFailed = false;
|
|
5522
|
+
if (wrote) try {
|
|
5523
|
+
entry.input.setDraft(value.text);
|
|
5524
|
+
} catch {
|
|
5525
|
+
writeFailed = true;
|
|
5526
|
+
}
|
|
5527
|
+
const actual = entry.input.state.getSnapshot();
|
|
5528
|
+
if (current() !== entry || !plain(actual)) return false;
|
|
5529
|
+
imported.epoch = epoch;
|
|
5530
|
+
imported.actualText = actual.draft;
|
|
5531
|
+
imported.successful = !writeFailed && actual.draft === value.text;
|
|
5532
|
+
return imported.successful;
|
|
5533
|
+
},
|
|
5534
|
+
verify(value) {
|
|
5535
|
+
if (!snapshotShape(value) || imported === void 0) return false;
|
|
5536
|
+
const saved = imported.snapshot;
|
|
5537
|
+
return imported.successful && value.token === saved.token && value.href === saved.href && value.sessionId === saved.sessionId && value.text === saved.text && value.revision === saved.revision && current() === imported.entry && epoch === imported.epoch && view.location.href === imported.href && imported.entry.input.state.getSnapshot().draft === saved.text;
|
|
5538
|
+
},
|
|
5539
|
+
rollback(value) {
|
|
5540
|
+
if (!snapshotShape(value) || imported === void 0) return false;
|
|
5541
|
+
const record = imported;
|
|
5542
|
+
const saved = record.snapshot;
|
|
5543
|
+
if (value.token !== saved.token || value.href !== saved.href || value.sessionId !== saved.sessionId || value.text !== saved.text || value.revision !== saved.revision || current() !== record.entry || epoch !== record.epoch || view.location.href !== record.href || record.entry.input.state.getSnapshot().draft !== record.actualText) return false;
|
|
5544
|
+
if (record.wrote) record.entry.input.setDraft("");
|
|
5545
|
+
const restored = !record.wrote || record.entry.input.state.getSnapshot().draft === "";
|
|
5546
|
+
clearImport();
|
|
5547
|
+
return restored;
|
|
5548
|
+
},
|
|
5549
|
+
release(value) {
|
|
5550
|
+
if (!snapshotShape(value)) return;
|
|
5551
|
+
if (value.token === lease?.token) clearLease();
|
|
5552
|
+
if (value.token === imported?.snapshot.token) clearImport();
|
|
5553
|
+
}
|
|
5554
|
+
};
|
|
5555
|
+
return {
|
|
5556
|
+
api: {
|
|
5557
|
+
capture() {
|
|
5558
|
+
try {
|
|
5559
|
+
return api.capture();
|
|
5560
|
+
} catch {
|
|
5561
|
+
risk();
|
|
5562
|
+
clearLease();
|
|
5563
|
+
return null;
|
|
5564
|
+
}
|
|
5565
|
+
},
|
|
5566
|
+
validate(value) {
|
|
5567
|
+
try {
|
|
5568
|
+
return api.validate(value);
|
|
5569
|
+
} catch {
|
|
5570
|
+
risk();
|
|
5571
|
+
return false;
|
|
5572
|
+
}
|
|
5573
|
+
},
|
|
5574
|
+
restore(value) {
|
|
5575
|
+
try {
|
|
5576
|
+
return api.restore(value);
|
|
5577
|
+
} catch {
|
|
5578
|
+
risk();
|
|
5579
|
+
return false;
|
|
5580
|
+
}
|
|
5581
|
+
},
|
|
5582
|
+
verify(value) {
|
|
5583
|
+
try {
|
|
5584
|
+
return api.verify(value);
|
|
5585
|
+
} catch {
|
|
5586
|
+
risk();
|
|
5587
|
+
return false;
|
|
5588
|
+
}
|
|
5589
|
+
},
|
|
5590
|
+
rollback(value) {
|
|
5591
|
+
try {
|
|
5592
|
+
return api.rollback(value);
|
|
5593
|
+
} catch {
|
|
5594
|
+
risk();
|
|
5595
|
+
return false;
|
|
5596
|
+
}
|
|
5597
|
+
},
|
|
5598
|
+
release(value) {
|
|
5599
|
+
try {
|
|
5600
|
+
api.release(value);
|
|
5601
|
+
} catch {}
|
|
5602
|
+
}
|
|
5603
|
+
},
|
|
5604
|
+
observe(sessionId, input) {
|
|
5605
|
+
if (disposed) return () => void 0;
|
|
5606
|
+
const key = {}, entry = {
|
|
5607
|
+
sessionId,
|
|
5608
|
+
input
|
|
5609
|
+
};
|
|
5610
|
+
observed.set(key, entry);
|
|
5611
|
+
invalidate();
|
|
5612
|
+
const changed = () => {
|
|
5613
|
+
invalidate();
|
|
5614
|
+
try {
|
|
5615
|
+
if (!plain(input.state.getSnapshot())) risky = true;
|
|
5616
|
+
} catch {
|
|
5617
|
+
risky = true;
|
|
5618
|
+
}
|
|
5619
|
+
};
|
|
5620
|
+
changed();
|
|
5621
|
+
const unsubscribe = input.state.subscribe(changed);
|
|
5622
|
+
const remove = () => {
|
|
5623
|
+
if (!subscriptions.delete(remove)) return;
|
|
5624
|
+
unsubscribe();
|
|
5625
|
+
observed.delete(key);
|
|
5626
|
+
invalidate();
|
|
5627
|
+
};
|
|
5628
|
+
subscriptions.add(remove);
|
|
5629
|
+
return remove;
|
|
5630
|
+
},
|
|
5631
|
+
dispose() {
|
|
5632
|
+
if (disposed) return;
|
|
5633
|
+
disposed = true;
|
|
5634
|
+
voiceObserver.disconnect();
|
|
5635
|
+
page.removeEventListener("click", onClick, true);
|
|
5636
|
+
clearLease();
|
|
5637
|
+
clearImport();
|
|
5638
|
+
for (const remove of subscriptions) remove();
|
|
5639
|
+
subscriptions.clear();
|
|
5640
|
+
observed.clear();
|
|
5641
|
+
for (const name of events) page.removeEventListener(name, onActivity, true);
|
|
5642
|
+
}
|
|
5643
|
+
};
|
|
5644
|
+
}
|
|
5645
|
+
/** Install only on the authenticated Mobile frontend; identity comes from the
|
|
5646
|
+
* real public session-scoped composer slot, state/writes from the public facade. */
|
|
5647
|
+
function installDraftTransfer(ctx, view, page) {
|
|
5648
|
+
const controller = createDraftTransfer(view, page);
|
|
5649
|
+
const previous = Object.getOwnPropertyDescriptor(view, "__dshMobileDraftTransfer");
|
|
5650
|
+
const mounted = /* @__PURE__ */ new Map();
|
|
5651
|
+
let active = true;
|
|
5652
|
+
let requestedSession;
|
|
5653
|
+
let importedToken;
|
|
5654
|
+
const api = {
|
|
5655
|
+
...controller.api,
|
|
5656
|
+
restore(value) {
|
|
5657
|
+
if (!active || !snapshotShape(value)) return false;
|
|
5658
|
+
if (importedToken !== void 0) return controller.api.restore(value);
|
|
5659
|
+
if (mounted.size === 0 || mounted.size === 1 && ![...mounted.values()].includes(value.sessionId)) {
|
|
5660
|
+
if (ctx.get("sessions")?.list?.getSnapshot?.().byId[value.sessionId] === void 0) return false;
|
|
5661
|
+
if (requestedSession !== value.sessionId) {
|
|
5662
|
+
const navigation = ctx.get("uiWorkspace");
|
|
5663
|
+
if (typeof navigation?.openSession !== "function") return false;
|
|
5664
|
+
try {
|
|
5665
|
+
navigation.openSession(value.sessionId);
|
|
5666
|
+
} catch {
|
|
5667
|
+
return false;
|
|
5668
|
+
}
|
|
5669
|
+
requestedSession = value.sessionId;
|
|
5670
|
+
}
|
|
5671
|
+
return false;
|
|
5672
|
+
}
|
|
5673
|
+
const restored = controller.api.restore(value);
|
|
5674
|
+
if (restored) importedToken = value.token;
|
|
5675
|
+
return restored;
|
|
5676
|
+
},
|
|
5677
|
+
release(value) {
|
|
5678
|
+
controller.api.release(value);
|
|
5679
|
+
if (snapshotShape(value) && value.token === importedToken) importedToken = void 0;
|
|
5680
|
+
}
|
|
5681
|
+
};
|
|
5682
|
+
Object.defineProperty(view, "__dshMobileDraftTransfer", {
|
|
5683
|
+
configurable: true,
|
|
5684
|
+
value: api
|
|
5685
|
+
});
|
|
5686
|
+
function Observer({ sessionId }) {
|
|
5687
|
+
(0, react.useEffect)(() => {
|
|
5688
|
+
if (!active) return;
|
|
5689
|
+
const sessions = ctx.get("sessions");
|
|
5690
|
+
const conversation = ctx.get("conversation");
|
|
5691
|
+
if (typeof sessions?.scope !== "function" || typeof conversation?.input?.for !== "function") return;
|
|
5692
|
+
const scope = sessions.scope(sessionId);
|
|
5693
|
+
if (scope === void 0) return;
|
|
5694
|
+
const key = {};
|
|
5695
|
+
mounted.set(key, sessionId);
|
|
5696
|
+
const remove = controller.observe(sessionId, conversation.input.for(scope));
|
|
5697
|
+
return () => {
|
|
5698
|
+
mounted.delete(key);
|
|
5699
|
+
remove();
|
|
5700
|
+
};
|
|
5701
|
+
}, [sessionId]);
|
|
5702
|
+
return (0, react.createElement)("span", {
|
|
5703
|
+
hidden: true,
|
|
5704
|
+
"data-dsh-mobile-draft-observer": sessionId
|
|
5705
|
+
});
|
|
5706
|
+
}
|
|
5707
|
+
const removeSlot = ctx.slots.inject("conversation.input.overlay", () => ctx.slots.register({
|
|
5708
|
+
name: "conversation.input.overlay",
|
|
5709
|
+
id: "dsh-mobile-draft-transfer"
|
|
5710
|
+
}, Observer));
|
|
5711
|
+
return () => {
|
|
5712
|
+
if (!active) return;
|
|
5713
|
+
active = false;
|
|
5714
|
+
removeSlot();
|
|
5715
|
+
mounted.clear();
|
|
5716
|
+
controller.dispose();
|
|
5717
|
+
if (view.__dshMobileDraftTransfer === api) {
|
|
5718
|
+
if (previous === void 0) delete view.__dshMobileDraftTransfer;
|
|
5719
|
+
else Object.defineProperty(view, "__dshMobileDraftTransfer", previous);
|
|
5720
|
+
}
|
|
5721
|
+
};
|
|
5722
|
+
}
|
|
5187
5723
|
//#endregion
|
|
5188
5724
|
//#region src/mobile-font-capabilities.ts
|
|
5189
5725
|
const NO_ADVANCED_MOBILE_TYPOGRAPHY = Object.freeze({
|
|
@@ -6094,18 +6630,67 @@ window.__ModuleLoader__.load({
|
|
|
6094
6630
|
const omitted = !entry.required && excluded.has(entry.id);
|
|
6095
6631
|
return matchesQuery(entry.id, query) && (filter === "all" || (filter === "excluded" ? omitted : !omitted));
|
|
6096
6632
|
}
|
|
6633
|
+
/** Format a module's measured transfer size for the settings panel.
|
|
6634
|
+
* @param entry - Catalog row with optional measured sizes.
|
|
6635
|
+
* @returns A short size label, or `undefined` when nothing was measured.
|
|
6636
|
+
*/
|
|
6637
|
+
function formatClientModuleSize(entry) {
|
|
6638
|
+
const value = entry.gzipBytes ?? entry.bytes;
|
|
6639
|
+
if (value === void 0 || !Number.isFinite(value) || value < 0) return void 0;
|
|
6640
|
+
if (value === 0) return "0 KB";
|
|
6641
|
+
const kilobytes = value / 1024;
|
|
6642
|
+
return kilobytes >= 1024 ? `${(kilobytes / 1024).toFixed(1)} MB` : `${String(Math.max(1, Math.round(kilobytes)))} KB`;
|
|
6643
|
+
}
|
|
6644
|
+
/** Sort measured modules largest first, keeping unknown sizes last and ties stable by ID. */
|
|
6645
|
+
function sortClientModulesBySize(entries) {
|
|
6646
|
+
return [...entries].sort((left, right) => {
|
|
6647
|
+
const leftBytes = left.gzipBytes ?? left.bytes;
|
|
6648
|
+
const rightBytes = right.gzipBytes ?? right.bytes;
|
|
6649
|
+
if (leftBytes === void 0 && rightBytes !== void 0) return 1;
|
|
6650
|
+
if (rightBytes === void 0 && leftBytes !== void 0) return -1;
|
|
6651
|
+
return (rightBytes ?? 0) - (leftBytes ?? 0) || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
6652
|
+
});
|
|
6653
|
+
}
|
|
6654
|
+
/**
|
|
6655
|
+
* Choose the smallest loadable set: required entries plus everything they depend on.
|
|
6656
|
+
*
|
|
6657
|
+
* Excluding every optional entry naively would keep a required module pointing at
|
|
6658
|
+
* an excluded dependency, which the Gateway rejects on save. The closure keeps
|
|
6659
|
+
* those dependencies loaded instead.
|
|
6660
|
+
* @param entries - Full catalog with declared dependencies.
|
|
6661
|
+
* @returns IDs to exclude, in catalog order.
|
|
6662
|
+
*/
|
|
6663
|
+
function clientModuleMinimalExclusion(entries) {
|
|
6664
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
6665
|
+
const keep = new Set(entries.filter((entry) => entry.required).map((entry) => entry.id));
|
|
6666
|
+
const pending = [...keep];
|
|
6667
|
+
while (pending.length > 0) {
|
|
6668
|
+
const entry = byId.get(pending.pop());
|
|
6669
|
+
if (entry === void 0) continue;
|
|
6670
|
+
for (const dependency of entry.dependencies) {
|
|
6671
|
+
if (keep.has(dependency)) continue;
|
|
6672
|
+
keep.add(dependency);
|
|
6673
|
+
pending.push(dependency);
|
|
6674
|
+
}
|
|
6675
|
+
}
|
|
6676
|
+
return entries.filter((entry) => !keep.has(entry.id)).map((entry) => entry.id);
|
|
6677
|
+
}
|
|
6097
6678
|
/** Editable selections retain unknown server exclusions and never cascade dependencies. */
|
|
6098
6679
|
var ClientModuleDraft = class {
|
|
6099
|
-
entries;
|
|
6100
6680
|
initial;
|
|
6101
6681
|
excluded;
|
|
6102
6682
|
entriesById;
|
|
6683
|
+
catalogEntries;
|
|
6103
6684
|
constructor(entries, excludedIds) {
|
|
6104
|
-
this.
|
|
6685
|
+
this.catalogEntries = entries;
|
|
6105
6686
|
this.initial = new Set(excludedIds);
|
|
6106
6687
|
this.excluded = new Set(excludedIds);
|
|
6107
6688
|
this.entriesById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
6108
6689
|
}
|
|
6690
|
+
/** @returns The original catalog with its most recently measured metadata. */
|
|
6691
|
+
get entries() {
|
|
6692
|
+
return this.catalogEntries;
|
|
6693
|
+
}
|
|
6109
6694
|
/** @returns Number of changed loading choices, including hidden filtered rows. */
|
|
6110
6695
|
get changeCount() {
|
|
6111
6696
|
let count = 0;
|
|
@@ -6153,13 +6738,60 @@ window.__ModuleLoader__.load({
|
|
|
6153
6738
|
if (loaded) this.excluded.delete(id);
|
|
6154
6739
|
else this.excluded.add(id);
|
|
6155
6740
|
}
|
|
6741
|
+
/** @returns Bytes the current selection still downloads, when sizes were measured. */
|
|
6742
|
+
loadedTransferBytes() {
|
|
6743
|
+
let total = 0;
|
|
6744
|
+
for (const entry of this.entries) {
|
|
6745
|
+
if (!this.isLoaded(entry.id)) continue;
|
|
6746
|
+
total += entry.gzipBytes ?? entry.bytes ?? 0;
|
|
6747
|
+
}
|
|
6748
|
+
return total;
|
|
6749
|
+
}
|
|
6750
|
+
/** @returns Number of selected modules whose transfer size is still unknown. */
|
|
6751
|
+
loadedUnknownSizeCount() {
|
|
6752
|
+
return this.entries.filter((entry) => this.isLoaded(entry.id) && entry.gzipBytes === void 0 && entry.bytes === void 0).length;
|
|
6753
|
+
}
|
|
6754
|
+
/** Refresh metadata without replacing saved or unsaved loading choices. */
|
|
6755
|
+
updateMeasurements(entries) {
|
|
6756
|
+
const measured = new Map(entries.map((entry) => [entry.id, entry]));
|
|
6757
|
+
for (const entry of this.entries) {
|
|
6758
|
+
const next = measured.get(entry.id);
|
|
6759
|
+
if (next === void 0) continue;
|
|
6760
|
+
const refreshed = { ...entry };
|
|
6761
|
+
delete refreshed.bytes;
|
|
6762
|
+
delete refreshed.gzipBytes;
|
|
6763
|
+
delete refreshed.description;
|
|
6764
|
+
this.entriesById.set(entry.id, {
|
|
6765
|
+
...refreshed,
|
|
6766
|
+
...next.bytes === void 0 ? {} : { bytes: next.bytes },
|
|
6767
|
+
...next.gzipBytes === void 0 ? {} : { gzipBytes: next.gzipBytes },
|
|
6768
|
+
...next.description === void 0 ? {} : { description: next.description }
|
|
6769
|
+
});
|
|
6770
|
+
}
|
|
6771
|
+
this.catalogEntries = this.entries.map((entry) => this.entriesById.get(entry.id));
|
|
6772
|
+
}
|
|
6773
|
+
/** Exclude every optional entry except dependencies a required entry still needs.
|
|
6774
|
+
* @returns Whether this draft changed; saving remains a separate action.
|
|
6775
|
+
*/
|
|
6776
|
+
applyMinimalSelection() {
|
|
6777
|
+
const target = new Set(clientModuleMinimalExclusion(this.entries));
|
|
6778
|
+
let changed = false;
|
|
6779
|
+
for (const entry of this.entries) {
|
|
6780
|
+
if (entry.required) continue;
|
|
6781
|
+
const shouldExclude = target.has(entry.id);
|
|
6782
|
+
if (shouldExclude !== this.excluded.has(entry.id)) changed = true;
|
|
6783
|
+
if (shouldExclude) this.excluded.add(entry.id);
|
|
6784
|
+
else this.excluded.delete(entry.id);
|
|
6785
|
+
}
|
|
6786
|
+
return changed;
|
|
6787
|
+
}
|
|
6156
6788
|
/** Filter a draft without changing hidden choices.
|
|
6157
6789
|
* @param query - Search input.
|
|
6158
6790
|
* @param filter - Requested loading selection.
|
|
6159
6791
|
* @returns Catalog entries in their original stable order.
|
|
6160
6792
|
*/
|
|
6161
6793
|
matchingEntries(query, filter) {
|
|
6162
|
-
return this.entries.filter((entry) => clientModuleMatches(entry, this.excluded, query, filter));
|
|
6794
|
+
return this.entries.map((entry) => this.entriesById.get(entry.id)).filter((entry) => clientModuleMatches(entry, this.excluded, query, filter));
|
|
6163
6795
|
}
|
|
6164
6796
|
/** Find invalid old exclusions by name or ID, independently from the protected catalog row.
|
|
6165
6797
|
* @param query - Search input.
|
|
@@ -6175,6 +6807,12 @@ window.__ModuleLoader__.load({
|
|
|
6175
6807
|
/** Device-local module selection and the computer's defaults share a read-only catalog. */
|
|
6176
6808
|
const CLIENT_MODULE_COPY = {
|
|
6177
6809
|
zh: {
|
|
6810
|
+
measure: "测量体积",
|
|
6811
|
+
measuring: "正在测量…",
|
|
6812
|
+
cancelMeasure: "取消测量",
|
|
6813
|
+
measured: "测量结束,未测到的项目保持未知。你的模块选择未变。",
|
|
6814
|
+
measureFailed: "测量未完成,已保留模块选择和已有体积。",
|
|
6815
|
+
totalPartial: "已测合计约 {size};另有 {count} 项未知",
|
|
6178
6816
|
title: "移动页面模块",
|
|
6179
6817
|
computer: "设置此电脑的移动访问默认项;电脑插件不变。",
|
|
6180
6818
|
device: "仅本设备,下次打开生效;电脑插件不变。",
|
|
@@ -6218,6 +6856,11 @@ window.__ModuleLoader__.load({
|
|
|
6218
6856
|
changes: "未保存:{count} 项改动",
|
|
6219
6857
|
unchanged: "没有未保存的改动",
|
|
6220
6858
|
results: "匹配 {visible} / {total} 项",
|
|
6859
|
+
sizeHint: "按传输体积从大到小排列;数字为 gzip 后的字节。说明取自插件包自身的描述字段,不代表上游认证。",
|
|
6860
|
+
total: "勾选合计约 {size}",
|
|
6861
|
+
totalUnknown: "勾选合计:部分模块未测到体积",
|
|
6862
|
+
minimal: "仅必需(最小加载)",
|
|
6863
|
+
minimalConfirm: "把除了启动必需项及其依赖之外全部设为不加载?保存后下次打开生效。",
|
|
6221
6864
|
retry: "重新读取",
|
|
6222
6865
|
saving: "正在保存…",
|
|
6223
6866
|
readTimeout: "读取超时。当前页面和已保存设置未变,请重试。",
|
|
@@ -6230,6 +6873,12 @@ window.__ModuleLoader__.load({
|
|
|
6230
6873
|
clearInvalid: "移除旧排除"
|
|
6231
6874
|
},
|
|
6232
6875
|
en: {
|
|
6876
|
+
measure: "Measure sizes",
|
|
6877
|
+
measuring: "Measuring…",
|
|
6878
|
+
cancelMeasure: "Cancel measurement",
|
|
6879
|
+
measured: "Measurement ended; unavailable sizes stay unknown. Your module choices are unchanged.",
|
|
6880
|
+
measureFailed: "Measurement did not complete. Your choices and known sizes are kept.",
|
|
6881
|
+
totalPartial: "Measured ≈ {size}; {count} sizes unknown",
|
|
6233
6882
|
title: "Mobile page modules",
|
|
6234
6883
|
computer: "Mobile defaults for this computer. Desktop plugins are unchanged.",
|
|
6235
6884
|
device: "For this device’s next open only. Computer plugins are unchanged.",
|
|
@@ -6273,6 +6922,11 @@ window.__ModuleLoader__.load({
|
|
|
6273
6922
|
changes: "Unsaved: {count} changes",
|
|
6274
6923
|
unchanged: "No unsaved changes",
|
|
6275
6924
|
results: "Matching {visible} / {total}",
|
|
6925
|
+
sizeHint: "Sorted by transfer size; numbers are gzipped bytes. Descriptions come from each plugin package; this is not an upstream endorsement.",
|
|
6926
|
+
total: "Selection ≈ {size}",
|
|
6927
|
+
totalUnknown: "Selection total: some sizes are unknown",
|
|
6928
|
+
minimal: "Required only (minimal load)",
|
|
6929
|
+
minimalConfirm: "Mark everything except required modules and their dependencies as not loaded? Applies on the next open after saving.",
|
|
6276
6930
|
retry: "Reread settings",
|
|
6277
6931
|
saving: "Saving…",
|
|
6278
6932
|
readTimeout: "Reading timed out. This page and saved settings are unchanged; try again.",
|
|
@@ -6285,6 +6939,12 @@ window.__ModuleLoader__.load({
|
|
|
6285
6939
|
clearInvalid: "Remove old exclusion"
|
|
6286
6940
|
},
|
|
6287
6941
|
it: {
|
|
6942
|
+
measure: "Misura dimensioni",
|
|
6943
|
+
measuring: "Misurazione…",
|
|
6944
|
+
cancelMeasure: "Annulla misurazione",
|
|
6945
|
+
measured: "Misurazione terminata; le dimensioni non disponibili restano sconosciute. Le scelte non cambiano.",
|
|
6946
|
+
measureFailed: "Misurazione incompleta. Scelte e dimensioni note conservate.",
|
|
6947
|
+
totalPartial: "Misurati ≈ {size}; {count} dimensioni sconosciute",
|
|
6288
6948
|
title: "Moduli della pagina mobile",
|
|
6289
6949
|
computer: "Predefiniti mobile del computer; i plugin non cambiano.",
|
|
6290
6950
|
device: "Solo alla prossima apertura su questo dispositivo; i plugin del computer non cambiano.",
|
|
@@ -6328,6 +6988,11 @@ window.__ModuleLoader__.load({
|
|
|
6328
6988
|
changes: "Non salvate: {count} modifiche",
|
|
6329
6989
|
unchanged: "Nessuna modifica non salvata",
|
|
6330
6990
|
results: "Corrispondenti {visible} / {total}",
|
|
6991
|
+
sizeHint: "Ordinati per dimensione trasferita; i numeri sono byte gzip. Le descrizioni provengono dal pacchetto del plugin; non sono un’approvazione upstream.",
|
|
6992
|
+
total: "Selezione ≈ {size}",
|
|
6993
|
+
totalUnknown: "Totale selezione: alcune dimensioni sono sconosciute",
|
|
6994
|
+
minimal: "Solo obbligatori (caricamento minimo)",
|
|
6995
|
+
minimalConfirm: "Impostare come non caricati tutti i moduli tranne gli obbligatori e le loro dipendenze? Vale alla prossima apertura dopo il salvataggio.",
|
|
6331
6996
|
retry: "Rileggi impostazioni",
|
|
6332
6997
|
saving: "Salvataggio…",
|
|
6333
6998
|
readTimeout: "Lettura scaduta. Pagina e impostazioni salvate non cambiano; riprova.",
|
|
@@ -6359,6 +7024,18 @@ window.__ModuleLoader__.load({
|
|
|
6359
7024
|
read: 15e3,
|
|
6360
7025
|
write: 2e4
|
|
6361
7026
|
};
|
|
7027
|
+
/** Describe measured bytes without presenting unmeasured modules as a complete total. */
|
|
7028
|
+
function clientModuleTotalLabel(draft, locale) {
|
|
7029
|
+
const copy = CLIENT_MODULE_COPY[locale];
|
|
7030
|
+
const unknownSizes = draft?.loadedUnknownSizeCount() ?? 0;
|
|
7031
|
+
const size = formatClientModuleSize({
|
|
7032
|
+
id: "",
|
|
7033
|
+
required: false,
|
|
7034
|
+
dependencies: [],
|
|
7035
|
+
gzipBytes: draft?.loadedTransferBytes() ?? 0
|
|
7036
|
+
}) ?? "0 KB";
|
|
7037
|
+
return unknownSizes > 0 ? copy.totalPartial.replace("{size}", size).replace("{count}", String(unknownSizes)) : copy.total.replace("{size}", size);
|
|
7038
|
+
}
|
|
6362
7039
|
/** Validate catalog response fields before creating interactive controls.
|
|
6363
7040
|
* @param value - Gateway or computer-admin JSON response.
|
|
6364
7041
|
* @returns The fields used by the module-selection dialog.
|
|
@@ -6372,10 +7049,23 @@ window.__ModuleLoader__.load({
|
|
|
6372
7049
|
return dependency;
|
|
6373
7050
|
});
|
|
6374
7051
|
if (entry.id.trim() === "") throw new Error("invalid_module_catalog");
|
|
7052
|
+
const sizes = {};
|
|
7053
|
+
for (const field of ["bytes", "gzipBytes"]) {
|
|
7054
|
+
if (!(field in entry)) continue;
|
|
7055
|
+
const measured = entry[field];
|
|
7056
|
+
if (typeof measured !== "number" || !Number.isSafeInteger(measured) || measured < 0) throw new Error("invalid_module_catalog");
|
|
7057
|
+
sizes[field] = measured;
|
|
7058
|
+
}
|
|
7059
|
+
if ("description" in entry) {
|
|
7060
|
+
const description = entry.description;
|
|
7061
|
+
if (typeof description !== "string" || description.trim() === "" || description.length > 240) throw new Error("invalid_module_catalog");
|
|
7062
|
+
sizes.description = description;
|
|
7063
|
+
}
|
|
6375
7064
|
return {
|
|
6376
7065
|
id: entry.id,
|
|
6377
7066
|
required: entry.required,
|
|
6378
|
-
dependencies
|
|
7067
|
+
dependencies,
|
|
7068
|
+
...sizes
|
|
6379
7069
|
};
|
|
6380
7070
|
});
|
|
6381
7071
|
if (new Set(entries.map((entry) => entry.id)).size !== entries.length) throw new Error("invalid_module_catalog");
|
|
@@ -6406,10 +7096,11 @@ window.__ModuleLoader__.load({
|
|
|
6406
7096
|
rejectCancellation(new DOMException("Dialog closed", "AbortError"));
|
|
6407
7097
|
};
|
|
6408
7098
|
signal.addEventListener("abort", abort, { once: true });
|
|
7099
|
+
const mutation = body !== void 0 && body.measure !== true;
|
|
6409
7100
|
const timer = setTimeout(() => {
|
|
6410
|
-
rejectCancellation(new ClientModuleRequestTimeout(
|
|
7101
|
+
rejectCancellation(new ClientModuleRequestTimeout(mutation));
|
|
6411
7102
|
controller.abort();
|
|
6412
|
-
},
|
|
7103
|
+
}, mutation ? CLIENT_MODULE_REQUEST_TIMEOUT_MS.write : CLIENT_MODULE_REQUEST_TIMEOUT_MS.read);
|
|
6413
7104
|
const operation = async () => {
|
|
6414
7105
|
const init = {
|
|
6415
7106
|
method: body === void 0 ? "GET" : "POST",
|
|
@@ -6453,6 +7144,8 @@ window.__ModuleLoader__.load({
|
|
|
6453
7144
|
.dsh-module-dialog[open]{display:flex;flex-direction:column}.dsh-module-dialog::backdrop{background:rgb(15 23 42 / 40%)}
|
|
6454
7145
|
.dsh-module-header{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
|
|
6455
7146
|
.dsh-module-header h2{flex:1;min-width:0;font-size:20px;line-height:1.4;margin:0;overflow-wrap:anywhere}.dsh-module-header .dsh-module-button{flex:none}
|
|
7147
|
+
.dsh-module-packageDescription{margin:2px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
7148
|
+
.dsh-module-description-full{margin:4px 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45}
|
|
6456
7149
|
.dsh-module-body{min-height:0;overflow:auto;overscroll-behavior:contain;padding:16px;scroll-padding:12px}.dsh-module-dialog p{margin:0 0 12px;overflow-wrap:anywhere}
|
|
6457
7150
|
.dsh-module-hint,.dsh-module-source,.dsh-module-choice small,.dsh-module-groupHint,.dsh-module-results{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
6458
7151
|
.dsh-module-tools{display:grid;grid-template-columns:minmax(0,1fr) minmax(100px,.45fr);gap:12px;margin:16px 0 8px}.dsh-module-field{display:flex;flex-direction:column;gap:4px;min-width:0;font-size:13px}
|
|
@@ -6470,7 +7163,7 @@ window.__ModuleLoader__.load({
|
|
|
6470
7163
|
.dsh-module-footer p{font-size:13px;line-height:20px;margin:0 0 8px}.dsh-module-actions{display:flex;flex-wrap:wrap;gap:8px}.dsh-module-actions .dsh-module-button{flex:1;min-width:0;white-space:normal;line-height:20px;padding:8px 12px}
|
|
6471
7164
|
.dsh-module-button:disabled{opacity:.5;cursor:default}.dsh-module-save{background:var(--dsw-alias-bg-module-platform);font-weight:600}
|
|
6472
7165
|
.dsh-module-notice:empty{display:none}.dsh-module-notice{max-height:min(96px,24dvh);overflow:auto;font-size:14px;line-height:22px;overscroll-behavior:contain}.dsh-module-empty{padding:16px 0}
|
|
6473
|
-
@media(max-width:420px){.dsh-module-tools{grid-template-columns:minmax(0,1fr)}.dsh-module-body{padding:12px}.dsh-module-header,.dsh-module-footer{padding-left:12px;padding-right:12px}}
|
|
7166
|
+
@media(max-width:420px){.dsh-module-tools{grid-template-columns:minmax(0,1fr)}.dsh-module-body{padding:12px}.dsh-module-header,.dsh-module-footer{padding-left:12px;padding-right:12px}.dsh-module-header{flex-wrap:wrap;gap:8px}.dsh-module-header h2{flex:1 1 100%}.dsh-module-header .dsh-module-button{flex:1 1 0;min-width:0;white-space:normal;overflow-wrap:anywhere;line-height:1.4;padding:8px 12px}}
|
|
6474
7167
|
.dsh-module-error{color:var(--dsw-alias-state-error-primary,#b42318)}
|
|
6475
7168
|
`;
|
|
6476
7169
|
const openDialogs = /* @__PURE__ */ new Set();
|
|
@@ -6517,7 +7210,9 @@ window.__ModuleLoader__.load({
|
|
|
6517
7210
|
const title = document.createElement("h2");
|
|
6518
7211
|
title.textContent = copy.title;
|
|
6519
7212
|
const close = button(copy.close);
|
|
6520
|
-
|
|
7213
|
+
const measure = button(copy.measure);
|
|
7214
|
+
measure.dataset.clientModuleMeasure = "true";
|
|
7215
|
+
header.append(title, measure, close);
|
|
6521
7216
|
const body = document.createElement("div");
|
|
6522
7217
|
body.className = "dsh-module-body";
|
|
6523
7218
|
const intro = paragraph(copy[scope]);
|
|
@@ -6527,6 +7222,7 @@ window.__ModuleLoader__.load({
|
|
|
6527
7222
|
const helpTitle = document.createElement("summary");
|
|
6528
7223
|
helpTitle.textContent = copy.help;
|
|
6529
7224
|
help.append(helpTitle, hint);
|
|
7225
|
+
help.append(paragraph(copy.sizeHint, "dsh-module-hint"));
|
|
6530
7226
|
const source = paragraph("", "dsh-module-source");
|
|
6531
7227
|
const status = paragraph("", "dsh-module-notice");
|
|
6532
7228
|
status.setAttribute("role", "status");
|
|
@@ -6573,14 +7269,16 @@ window.__ModuleLoader__.load({
|
|
|
6573
7269
|
actions.className = "dsh-module-actions";
|
|
6574
7270
|
const save = button(copy.save);
|
|
6575
7271
|
save.classList.add("dsh-module-save");
|
|
7272
|
+
const minimal = button(copy.minimal);
|
|
6576
7273
|
const reset = button(copy.reset);
|
|
6577
7274
|
const reread = button(copy.retry);
|
|
6578
7275
|
actions.append(save, reset, reread);
|
|
6579
7276
|
footer.append(changes, status, actions);
|
|
6580
|
-
body.append(intro, source, tools, results, list, help);
|
|
7277
|
+
body.append(intro, source, tools, results, minimal, list, help);
|
|
6581
7278
|
dialog.append(header, body, footer);
|
|
6582
7279
|
let disposed = false;
|
|
6583
7280
|
let busy;
|
|
7281
|
+
let measurementController;
|
|
6584
7282
|
let generation = 0;
|
|
6585
7283
|
let draft;
|
|
6586
7284
|
let unconfirmedWrite = false;
|
|
@@ -6596,11 +7294,15 @@ window.__ModuleLoader__.load({
|
|
|
6596
7294
|
const changed = draft?.changeCount ?? 0;
|
|
6597
7295
|
save.disabled = busy !== void 0 || changed === 0;
|
|
6598
7296
|
reset.disabled = busy !== void 0 || draft === void 0;
|
|
7297
|
+
minimal.disabled = busy !== void 0 || draft === void 0;
|
|
7298
|
+
measure.disabled = busy !== void 0 && busy !== "measure" || draft === void 0;
|
|
7299
|
+
measure.textContent = busy === "measure" ? copy.cancelMeasure : copy.measure;
|
|
6599
7300
|
reread.disabled = busy !== void 0;
|
|
6600
7301
|
search.disabled = busy !== void 0 || draft === void 0;
|
|
6601
7302
|
filter.disabled = search.disabled;
|
|
6602
7303
|
save.textContent = busy === "write" ? copy.saving : copy.save;
|
|
6603
|
-
|
|
7304
|
+
const total = clientModuleTotalLabel(draft, locale);
|
|
7305
|
+
changes.textContent = `${changed === 0 ? copy.unchanged : copy.changes.replace("{count}", String(changed))} · ${total} · ${copy.nextOpen}`;
|
|
6604
7306
|
list.setAttribute("aria-busy", String(busy !== void 0));
|
|
6605
7307
|
for (const input of choices.values()) input.disabled = busy !== void 0 || input.dataset.required === "true";
|
|
6606
7308
|
for (const action of invalidActions.values()) action.disabled = busy !== void 0;
|
|
@@ -6664,7 +7366,7 @@ window.__ModuleLoader__.load({
|
|
|
6664
7366
|
"advanced",
|
|
6665
7367
|
"required"
|
|
6666
7368
|
]) {
|
|
6667
|
-
const entries = visible.filter((entry) => clientModuleGroup(entry) === group);
|
|
7369
|
+
const entries = sortClientModulesBySize(visible.filter((entry) => clientModuleGroup(entry) === group));
|
|
6668
7370
|
if (entries.length === 0) continue;
|
|
6669
7371
|
const section = document.createElement(group === "extensions" ? "section" : "details");
|
|
6670
7372
|
section.className = "dsh-module-group";
|
|
@@ -6702,9 +7404,19 @@ window.__ModuleLoader__.load({
|
|
|
6702
7404
|
shortName.textContent = clientModuleDisplayName(entry.id);
|
|
6703
7405
|
const selected = document.createElement("small");
|
|
6704
7406
|
selected.textContent = entry.required ? copy.required : input.checked ? copy.loaded : copy.excluded;
|
|
6705
|
-
|
|
7407
|
+
const size = formatClientModuleSize(entry);
|
|
7408
|
+
const sizeLabel = document.createElement("small");
|
|
7409
|
+
sizeLabel.className = "dsh-module-size";
|
|
7410
|
+
sizeLabel.textContent = size ?? "";
|
|
7411
|
+
if (size !== void 0) sizeLabel.dataset.clientModuleSize = "true";
|
|
7412
|
+
name.append(shortName, selected, sizeLabel);
|
|
6706
7413
|
label.append(input, name);
|
|
6707
7414
|
row.append(label);
|
|
7415
|
+
if (entry.description !== void 0) {
|
|
7416
|
+
const summary = paragraph(entry.description, "dsh-module-packageDescription");
|
|
7417
|
+
summary.dataset.clientModuleDescription = "true";
|
|
7418
|
+
row.append(summary);
|
|
7419
|
+
}
|
|
6708
7420
|
const detail = document.createElement("details");
|
|
6709
7421
|
detail.className = "dsh-module-entryDetails";
|
|
6710
7422
|
const detailSummary = document.createElement("summary");
|
|
@@ -6713,7 +7425,9 @@ window.__ModuleLoader__.load({
|
|
|
6713
7425
|
const code = document.createElement("code");
|
|
6714
7426
|
code.textContent = entry.id;
|
|
6715
7427
|
id.append(code);
|
|
6716
|
-
detail.append(detailSummary, id
|
|
7428
|
+
detail.append(detailSummary, id);
|
|
7429
|
+
if (entry.description !== void 0) detail.append(paragraph(entry.description, "dsh-module-description-full"));
|
|
7430
|
+
detail.append(paragraph(entry.dependencies.length === 0 ? copy.noDependencies : `${copy.dependencies}${entry.dependencies.join(", ")}`));
|
|
6717
7431
|
row.append(detail);
|
|
6718
7432
|
input.addEventListener("change", () => {
|
|
6719
7433
|
if (busy !== void 0 || draft === void 0) return;
|
|
@@ -6748,23 +7462,36 @@ window.__ModuleLoader__.load({
|
|
|
6748
7462
|
};
|
|
6749
7463
|
const runRequest = (payload) => {
|
|
6750
7464
|
if (disposed || busy !== void 0 || payload !== void 0 && draft === void 0) return;
|
|
7465
|
+
const measuring = payload?.measure === true;
|
|
7466
|
+
const mutation = payload !== void 0 && !measuring;
|
|
6751
7467
|
const ownGeneration = ++generation;
|
|
6752
|
-
busy =
|
|
6753
|
-
if (
|
|
7468
|
+
busy = measuring ? "measure" : mutation ? "write" : "read";
|
|
7469
|
+
if (mutation) unconfirmedWrite = true;
|
|
7470
|
+
if (measuring) measurementController = new AbortController();
|
|
6754
7471
|
status.classList.remove("dsh-module-error");
|
|
6755
|
-
status.textContent =
|
|
7472
|
+
status.textContent = measuring ? copy.measuring : mutation ? copy.saving : copy.loading;
|
|
6756
7473
|
updateControls();
|
|
6757
|
-
requestClientModuleView(scope, controller.signal, payload).then((view) => {
|
|
7474
|
+
requestClientModuleView(scope, measuring ? measurementController.signal : controller.signal, payload).then((view) => {
|
|
6758
7475
|
if (disposed || ownGeneration !== generation) return;
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
7476
|
+
if (measuring && draft !== void 0) {
|
|
7477
|
+
draft.updateMeasurements(view.entries);
|
|
7478
|
+
renderList();
|
|
7479
|
+
} else {
|
|
7480
|
+
unconfirmedWrite = false;
|
|
7481
|
+
render(view);
|
|
7482
|
+
}
|
|
7483
|
+
status.textContent = measuring ? copy.measured : mutation ? copy.saved : "";
|
|
6762
7484
|
}, (error) => {
|
|
6763
7485
|
if (disposed || ownGeneration !== generation) return;
|
|
7486
|
+
if (measuring) {
|
|
7487
|
+
status.textContent = copy.measureFailed;
|
|
7488
|
+
return;
|
|
7489
|
+
}
|
|
6764
7490
|
if (error instanceof ClientModuleUiConflict) unconfirmedWrite = false;
|
|
6765
|
-
failed(error,
|
|
7491
|
+
failed(error, mutation);
|
|
6766
7492
|
}).finally(() => {
|
|
6767
7493
|
if (disposed || ownGeneration !== generation) return;
|
|
7494
|
+
measurementController = void 0;
|
|
6768
7495
|
busy = void 0;
|
|
6769
7496
|
updateControls();
|
|
6770
7497
|
});
|
|
@@ -6777,6 +7504,18 @@ window.__ModuleLoader__.load({
|
|
|
6777
7504
|
reset.addEventListener("click", () => {
|
|
6778
7505
|
if (busy === void 0 && draft !== void 0 && window.confirm(copy.resetConfirm)) runRequest({ reset: true });
|
|
6779
7506
|
});
|
|
7507
|
+
minimal.addEventListener("click", () => {
|
|
7508
|
+
if (busy !== void 0 || draft === void 0 || !window.confirm(copy.minimalConfirm)) return;
|
|
7509
|
+
if (draft.applyMinimalSelection()) {
|
|
7510
|
+
renderList();
|
|
7511
|
+
search.focus({ preventScroll: true });
|
|
7512
|
+
}
|
|
7513
|
+
updateControls();
|
|
7514
|
+
});
|
|
7515
|
+
measure.addEventListener("click", () => {
|
|
7516
|
+
if (busy === "measure") measurementController?.abort();
|
|
7517
|
+
else if (busy === void 0 && draft !== void 0) runRequest({ measure: true });
|
|
7518
|
+
});
|
|
6780
7519
|
reread.addEventListener("click", () => {
|
|
6781
7520
|
if (busy === void 0 && ((draft?.changeCount ?? 0) === 0 || window.confirm(copy.reloadConfirm))) runRequest();
|
|
6782
7521
|
});
|
|
@@ -6785,6 +7524,7 @@ window.__ModuleLoader__.load({
|
|
|
6785
7524
|
disposed = true;
|
|
6786
7525
|
++generation;
|
|
6787
7526
|
controller.abort();
|
|
7527
|
+
measurementController?.abort();
|
|
6788
7528
|
openDialogs.delete(openDialog);
|
|
6789
7529
|
window.removeEventListener("dsh-mobile:native-back", onBack, true);
|
|
6790
7530
|
window.removeEventListener("keydown", onEscape, true);
|
|
@@ -7199,6 +7939,851 @@ window.__ModuleLoader__.load({
|
|
|
7199
7939
|
assetDepth: 8
|
|
7200
7940
|
});
|
|
7201
7941
|
//#endregion
|
|
7942
|
+
//#region src/client-recovery.ts
|
|
7943
|
+
/** Only a visible, disconnected public transport needs an immediate resume nudge. */
|
|
7944
|
+
function shouldNudgeReconnect(visibility, connection) {
|
|
7945
|
+
return visibility === "visible" && connection !== "connected";
|
|
7946
|
+
}
|
|
7947
|
+
/** A persistent, known disconnection can request one native renewal. */
|
|
7948
|
+
function shouldRenewSession(connection, downForMs) {
|
|
7949
|
+
return isDisconnected(connection) && downForMs >= 5e3;
|
|
7950
|
+
}
|
|
7951
|
+
function isDisconnected(connection) {
|
|
7952
|
+
return connection === "connecting" || connection === "disconnected";
|
|
7953
|
+
}
|
|
7954
|
+
/**
|
|
7955
|
+
* Own resume nudges and one native renewal per continuous disconnection.
|
|
7956
|
+
* @param getConnection - Resolve the current service to reject a replaced owner.
|
|
7957
|
+
* @param page - The document's window and advertised native adapter.
|
|
7958
|
+
* @param document - The mounted document; page replacement withdraws its work.
|
|
7959
|
+
* @returns An awaited disposer, or nothing when no public connection is present.
|
|
7960
|
+
*/
|
|
7961
|
+
function installMobileConnectionRecovery(getConnection, page, document) {
|
|
7962
|
+
const connection = getConnection();
|
|
7963
|
+
const state = connection?.state;
|
|
7964
|
+
if (connection === void 0 || state === void 0 || typeof connection.reconnect !== "function") return;
|
|
7965
|
+
const origin = page.location.origin;
|
|
7966
|
+
let disposed = false;
|
|
7967
|
+
let generation = 0;
|
|
7968
|
+
let currentState = state.getSnapshot();
|
|
7969
|
+
let downSince = isDisconnected(currentState) ? Date.now() : void 0;
|
|
7970
|
+
let attempted = false;
|
|
7971
|
+
let bridge = page.__DSH_MOBILE_NATIVE__;
|
|
7972
|
+
let pending;
|
|
7973
|
+
let pendingAbort;
|
|
7974
|
+
let cleanup;
|
|
7975
|
+
const current = () => !disposed && page.document === document && page.location.origin === origin && getConnection() === connection && connection.state === state;
|
|
7976
|
+
const cancelPending = () => {
|
|
7977
|
+
generation += 1;
|
|
7978
|
+
pendingAbort?.abort(new DOMException("mobile recovery owner ended", "AbortError"));
|
|
7979
|
+
};
|
|
7980
|
+
const updateState = () => {
|
|
7981
|
+
const next = state.getSnapshot();
|
|
7982
|
+
if (!isDisconnected(next)) {
|
|
7983
|
+
downSince = void 0;
|
|
7984
|
+
attempted = false;
|
|
7985
|
+
if (currentState !== next) cancelPending();
|
|
7986
|
+
} else if (downSince === void 0) {
|
|
7987
|
+
downSince = Date.now();
|
|
7988
|
+
attempted = false;
|
|
7989
|
+
}
|
|
7990
|
+
currentState = next;
|
|
7991
|
+
};
|
|
7992
|
+
const updateBridge = () => {
|
|
7993
|
+
const next = page.__DSH_MOBILE_NATIVE__;
|
|
7994
|
+
if (bridge === next) return;
|
|
7995
|
+
cancelPending();
|
|
7996
|
+
bridge = next;
|
|
7997
|
+
attempted = false;
|
|
7998
|
+
};
|
|
7999
|
+
const renew = () => {
|
|
8000
|
+
if (!current()) {
|
|
8001
|
+
dispose();
|
|
8002
|
+
return;
|
|
8003
|
+
}
|
|
8004
|
+
updateState();
|
|
8005
|
+
updateBridge();
|
|
8006
|
+
const adapter = bridge;
|
|
8007
|
+
if (document.visibilityState !== "visible" || adapter === void 0 || pending !== void 0 || attempted || downSince === void 0 || !shouldRenewSession(currentState, Date.now() - downSince)) return;
|
|
8008
|
+
attempted = true;
|
|
8009
|
+
const owner = generation;
|
|
8010
|
+
const abort = new AbortController();
|
|
8011
|
+
pendingAbort = abort;
|
|
8012
|
+
const owns = () => current() && !abort.signal.aborted && generation === owner && page.__DSH_MOBILE_NATIVE__ === adapter && isDisconnected(state.getSnapshot());
|
|
8013
|
+
const task = Promise.resolve().then(async () => {
|
|
8014
|
+
if (!owns()) return;
|
|
8015
|
+
const capabilities = await adapter.capabilities();
|
|
8016
|
+
if (!owns() || !capabilities.includes("auth.renew")) return;
|
|
8017
|
+
const result = await adapter.invoke("auth.renew", {}, { signal: abort.signal });
|
|
8018
|
+
if (!owns() || typeof result !== "object" || result === null || !("renewed" in result) || result.renewed !== true) return;
|
|
8019
|
+
connection.reconnect?.();
|
|
8020
|
+
}).catch((_error) => {}).finally(() => {
|
|
8021
|
+
if (pending !== task) return;
|
|
8022
|
+
pending = void 0;
|
|
8023
|
+
pendingAbort = void 0;
|
|
8024
|
+
});
|
|
8025
|
+
pending = task;
|
|
8026
|
+
};
|
|
8027
|
+
const resume = () => {
|
|
8028
|
+
if (!current()) {
|
|
8029
|
+
dispose();
|
|
8030
|
+
return;
|
|
8031
|
+
}
|
|
8032
|
+
updateState();
|
|
8033
|
+
updateBridge();
|
|
8034
|
+
if (shouldNudgeReconnect(document.visibilityState, currentState)) connection.reconnect?.();
|
|
8035
|
+
renew();
|
|
8036
|
+
};
|
|
8037
|
+
const stopState = state.subscribe(updateState);
|
|
8038
|
+
const timer = setInterval(renew, 2e3);
|
|
8039
|
+
const pagehide = () => {
|
|
8040
|
+
dispose();
|
|
8041
|
+
};
|
|
8042
|
+
const dispose = () => {
|
|
8043
|
+
if (cleanup !== void 0) return cleanup;
|
|
8044
|
+
disposed = true;
|
|
8045
|
+
cancelPending();
|
|
8046
|
+
clearInterval(timer);
|
|
8047
|
+
stopState();
|
|
8048
|
+
document.removeEventListener("visibilitychange", resume);
|
|
8049
|
+
page.removeEventListener("dsh-mobile-native-ready", updateBridge);
|
|
8050
|
+
page.removeEventListener("pagehide", pagehide);
|
|
8051
|
+
cleanup = Promise.resolve(pending);
|
|
8052
|
+
return cleanup;
|
|
8053
|
+
};
|
|
8054
|
+
document.addEventListener("visibilitychange", resume);
|
|
8055
|
+
page.addEventListener("dsh-mobile-native-ready", updateBridge);
|
|
8056
|
+
page.addEventListener("pagehide", pagehide);
|
|
8057
|
+
return dispose;
|
|
8058
|
+
}
|
|
8059
|
+
//#endregion
|
|
8060
|
+
//#region src/browser-ui-locale.ts
|
|
8061
|
+
const UI_LOCALE_COOKIE = "dsh_ma_ui_locale";
|
|
8062
|
+
/** Host-only, non-secret preference, independent of the short authentication session. */
|
|
8063
|
+
function browserUiLocaleCookie(locale, secure) {
|
|
8064
|
+
return `${UI_LOCALE_COOKIE}=${locale}; Path=/mobile-access; Max-Age=31536000; SameSite=Lax${secure ? "; Secure" : ""}`;
|
|
8065
|
+
}
|
|
8066
|
+
//#endregion
|
|
8067
|
+
//#region src/mobile-ui-locale.ts
|
|
8068
|
+
/** Remember the DSH display language only on authenticated Mobile Gateway documents. */
|
|
8069
|
+
function installMobileUiLocalePersistence(page, view) {
|
|
8070
|
+
if (page.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") === null) return () => {};
|
|
8071
|
+
let disposed = false;
|
|
8072
|
+
let previous;
|
|
8073
|
+
const sync = () => {
|
|
8074
|
+
if (disposed) return;
|
|
8075
|
+
const language = page.documentElement.lang.trim().toLowerCase();
|
|
8076
|
+
if (language === "") return;
|
|
8077
|
+
const base = language.split(/[-_]/u)[0];
|
|
8078
|
+
const locale = base === "zh" || base === "it" ? base : "en";
|
|
8079
|
+
if (locale === previous) return;
|
|
8080
|
+
try {
|
|
8081
|
+
page.cookie = browserUiLocaleCookie(locale, view.location.protocol === "https:");
|
|
8082
|
+
previous = locale;
|
|
8083
|
+
} catch {}
|
|
8084
|
+
};
|
|
8085
|
+
const observer = new view.MutationObserver(sync);
|
|
8086
|
+
observer.observe(page.documentElement, {
|
|
8087
|
+
attributes: true,
|
|
8088
|
+
attributeFilter: ["lang"]
|
|
8089
|
+
});
|
|
8090
|
+
sync();
|
|
8091
|
+
return () => {
|
|
8092
|
+
disposed = true;
|
|
8093
|
+
observer.disconnect();
|
|
8094
|
+
};
|
|
8095
|
+
}
|
|
8096
|
+
//#endregion
|
|
8097
|
+
//#region src/mobile-directory-browser.ts
|
|
8098
|
+
/** Expected Gateway or response-validation failure for one read-only listing. */
|
|
8099
|
+
var MobileDirectoryReadError = class extends Error {
|
|
8100
|
+
code;
|
|
8101
|
+
constructor(code, options) {
|
|
8102
|
+
super(`Mobile directory listing failed: ${code}`, options);
|
|
8103
|
+
this.code = code;
|
|
8104
|
+
this.name = "MobileDirectoryReadError";
|
|
8105
|
+
}
|
|
8106
|
+
};
|
|
8107
|
+
/** Accept portable absolute spellings; the Host remains authoritative for its platform. */
|
|
8108
|
+
function isMobileDirectoryPath(path) {
|
|
8109
|
+
return path.length > 0 && path.length <= 8192 && !path.includes("\0") && (path.startsWith("/") || /^[a-z]:[\\/]/iu.test(path) || /^\\\\[^\\/]+[\\/][^\\/]+/u.test(path));
|
|
8110
|
+
}
|
|
8111
|
+
function object(value) {
|
|
8112
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
8113
|
+
}
|
|
8114
|
+
/** Validate a wire response before publishing names or adopting a Host path. */
|
|
8115
|
+
function parseMobileDirectoryListing(value) {
|
|
8116
|
+
const record = object(value);
|
|
8117
|
+
const fail = () => {
|
|
8118
|
+
throw new MobileDirectoryReadError("invalid-response");
|
|
8119
|
+
};
|
|
8120
|
+
if (record === void 0 || typeof record.path !== "string" || !isMobileDirectoryPath(record.path) || typeof record.truncated !== "boolean" || !Array.isArray(record.entries) || record.entries.length > 500) return fail();
|
|
8121
|
+
const parent = record.parent;
|
|
8122
|
+
if (parent !== void 0 && (typeof parent !== "string" || !isMobileDirectoryPath(parent))) return fail();
|
|
8123
|
+
const paths = /* @__PURE__ */ new Set();
|
|
8124
|
+
const entries = [];
|
|
8125
|
+
for (const candidate of record.entries) {
|
|
8126
|
+
const entry = object(candidate);
|
|
8127
|
+
if (entry === void 0 || typeof entry.name !== "string" || entry.name.length === 0 || entry.name.includes("\0") || typeof entry.path !== "string" || !isMobileDirectoryPath(entry.path) || paths.has(entry.path)) return fail();
|
|
8128
|
+
paths.add(entry.path);
|
|
8129
|
+
entries.push(Object.freeze({
|
|
8130
|
+
name: entry.name,
|
|
8131
|
+
path: entry.path
|
|
8132
|
+
}));
|
|
8133
|
+
}
|
|
8134
|
+
return Object.freeze({
|
|
8135
|
+
path: record.path,
|
|
8136
|
+
...parent === void 0 ? {} : { parent },
|
|
8137
|
+
entries: Object.freeze(entries),
|
|
8138
|
+
truncated: record.truncated
|
|
8139
|
+
});
|
|
8140
|
+
}
|
|
8141
|
+
const CLOSED = Object.freeze({
|
|
8142
|
+
open: false,
|
|
8143
|
+
phase: "idle",
|
|
8144
|
+
listing: null,
|
|
8145
|
+
draft: "",
|
|
8146
|
+
error: null,
|
|
8147
|
+
hostBusy: false,
|
|
8148
|
+
committed: false
|
|
8149
|
+
});
|
|
8150
|
+
/** Read navigation stops on close; committing is final until the owner withdraws this flow. */
|
|
8151
|
+
var MobileDirectoryBrowser = class {
|
|
8152
|
+
read;
|
|
8153
|
+
deadlineMs;
|
|
8154
|
+
snapshot = CLOSED;
|
|
8155
|
+
listeners = /* @__PURE__ */ new Set();
|
|
8156
|
+
tasks = /* @__PURE__ */ new Set();
|
|
8157
|
+
taskFailures = [];
|
|
8158
|
+
activeRead;
|
|
8159
|
+
generation = 0;
|
|
8160
|
+
disposed = false;
|
|
8161
|
+
lastTarget = null;
|
|
8162
|
+
constructor(read, deadlineMs = 3e4) {
|
|
8163
|
+
this.read = read;
|
|
8164
|
+
this.deadlineMs = deadlineMs;
|
|
8165
|
+
}
|
|
8166
|
+
getSnapshot = () => this.snapshot;
|
|
8167
|
+
/** Subscribe to this occurrence; the returned disposer removes the observer. */
|
|
8168
|
+
subscribe(listener) {
|
|
8169
|
+
this.listeners.add(listener);
|
|
8170
|
+
return () => {
|
|
8171
|
+
this.listeners.delete(listener);
|
|
8172
|
+
};
|
|
8173
|
+
}
|
|
8174
|
+
/** Start a new open edge at the Host home; returns false after permanent disposal. */
|
|
8175
|
+
start() {
|
|
8176
|
+
if (this.disposed || this.snapshot.open) return false;
|
|
8177
|
+
this.publish({
|
|
8178
|
+
...CLOSED,
|
|
8179
|
+
open: true
|
|
8180
|
+
});
|
|
8181
|
+
return this.navigate(null);
|
|
8182
|
+
}
|
|
8183
|
+
/** Editing invalidates any pending scan and cannot commit the previously listed path. */
|
|
8184
|
+
setDraft(draft) {
|
|
8185
|
+
if (!this.canNavigate() || draft === this.snapshot.draft) return;
|
|
8186
|
+
this.withdrawRead();
|
|
8187
|
+
this.publish({
|
|
8188
|
+
...this.snapshot,
|
|
8189
|
+
draft,
|
|
8190
|
+
phase: "idle",
|
|
8191
|
+
error: isMobileDirectoryPath(draft) ? null : "bad-path"
|
|
8192
|
+
});
|
|
8193
|
+
}
|
|
8194
|
+
/** Project the owner's adoption state without undoing this flow's synchronous commit claim. */
|
|
8195
|
+
setHostBusy(hostBusy) {
|
|
8196
|
+
if (this.disposed || this.snapshot.hostBusy === hostBusy) return;
|
|
8197
|
+
this.publish({
|
|
8198
|
+
...this.snapshot,
|
|
8199
|
+
hostBusy
|
|
8200
|
+
});
|
|
8201
|
+
}
|
|
8202
|
+
/** Read a folder or the initial home; superseded results cannot publish into the new request. */
|
|
8203
|
+
navigate(path) {
|
|
8204
|
+
if (!this.canNavigate()) return false;
|
|
8205
|
+
this.withdrawRead();
|
|
8206
|
+
if (path !== null && !isMobileDirectoryPath(path)) {
|
|
8207
|
+
this.publish({
|
|
8208
|
+
...this.snapshot,
|
|
8209
|
+
draft: path,
|
|
8210
|
+
phase: "error",
|
|
8211
|
+
error: "bad-path"
|
|
8212
|
+
});
|
|
8213
|
+
return false;
|
|
8214
|
+
}
|
|
8215
|
+
this.lastTarget = path;
|
|
8216
|
+
const controller = new AbortController();
|
|
8217
|
+
this.activeRead = controller;
|
|
8218
|
+
const generation = this.generation;
|
|
8219
|
+
this.publish({
|
|
8220
|
+
...this.snapshot,
|
|
8221
|
+
phase: "loading",
|
|
8222
|
+
error: null,
|
|
8223
|
+
...path === null ? {} : { draft: path }
|
|
8224
|
+
});
|
|
8225
|
+
const task = (async () => {
|
|
8226
|
+
const timeout = setTimeout(() => {
|
|
8227
|
+
controller.abort(new MobileDirectoryReadError("timeout"));
|
|
8228
|
+
}, this.deadlineMs);
|
|
8229
|
+
try {
|
|
8230
|
+
const value = await this.read(path, controller.signal);
|
|
8231
|
+
controller.signal.throwIfAborted();
|
|
8232
|
+
const listing = parseMobileDirectoryListing(value);
|
|
8233
|
+
if (!this.current(generation)) return;
|
|
8234
|
+
this.publish({
|
|
8235
|
+
...this.snapshot,
|
|
8236
|
+
phase: "idle",
|
|
8237
|
+
listing,
|
|
8238
|
+
draft: listing.path,
|
|
8239
|
+
error: null
|
|
8240
|
+
});
|
|
8241
|
+
} catch (error) {
|
|
8242
|
+
if (!this.current(generation)) return;
|
|
8243
|
+
const reason = controller.signal.aborted ? controller.signal.reason : error;
|
|
8244
|
+
this.publish({
|
|
8245
|
+
...this.snapshot,
|
|
8246
|
+
phase: "error",
|
|
8247
|
+
error: reason instanceof MobileDirectoryReadError ? reason.code : "request-failed"
|
|
8248
|
+
});
|
|
8249
|
+
} finally {
|
|
8250
|
+
clearTimeout(timeout);
|
|
8251
|
+
if (this.activeRead === controller) this.activeRead = void 0;
|
|
8252
|
+
}
|
|
8253
|
+
})();
|
|
8254
|
+
this.tasks.add(task);
|
|
8255
|
+
task.then(() => {
|
|
8256
|
+
this.tasks.delete(task);
|
|
8257
|
+
}, (error) => {
|
|
8258
|
+
this.tasks.delete(task);
|
|
8259
|
+
this.taskFailures.push(error);
|
|
8260
|
+
});
|
|
8261
|
+
return true;
|
|
8262
|
+
}
|
|
8263
|
+
/** Retry only the explicitly failed read, without adopting or replaying a write. */
|
|
8264
|
+
retry() {
|
|
8265
|
+
return this.navigate(this.lastTarget);
|
|
8266
|
+
}
|
|
8267
|
+
/** Whether the confirmed listing still matches the editable path and can be adopted. */
|
|
8268
|
+
canConfirm() {
|
|
8269
|
+
const value = this.snapshot;
|
|
8270
|
+
return this.canNavigate() && value.phase === "idle" && value.error === null && value.listing !== null && value.draft === value.listing.path;
|
|
8271
|
+
}
|
|
8272
|
+
/** Claim one adoption synchronously; returns no path for stale input, busy state or duplicate taps. */
|
|
8273
|
+
confirm() {
|
|
8274
|
+
if (!this.canConfirm()) return void 0;
|
|
8275
|
+
const path = this.snapshot.listing.path;
|
|
8276
|
+
this.publish({
|
|
8277
|
+
...this.snapshot,
|
|
8278
|
+
committed: true
|
|
8279
|
+
});
|
|
8280
|
+
return path;
|
|
8281
|
+
}
|
|
8282
|
+
/** Cancel reading immediately; a committed/Host-busy adoption cannot be reported as cancelled. */
|
|
8283
|
+
cancel() {
|
|
8284
|
+
if (!this.canNavigate()) return false;
|
|
8285
|
+
this.withdrawRead();
|
|
8286
|
+
this.publish(CLOSED);
|
|
8287
|
+
return true;
|
|
8288
|
+
}
|
|
8289
|
+
/** Withdraw callbacks immediately and await every admitted read before disposal completes. */
|
|
8290
|
+
async dispose() {
|
|
8291
|
+
this.disposed = true;
|
|
8292
|
+
this.withdrawRead();
|
|
8293
|
+
this.snapshot = CLOSED;
|
|
8294
|
+
this.listeners.clear();
|
|
8295
|
+
await Promise.allSettled([...this.tasks]);
|
|
8296
|
+
if (this.taskFailures.length > 0) throw new AggregateError(this.taskFailures, "Mobile directory observers failed during a listing");
|
|
8297
|
+
}
|
|
8298
|
+
canNavigate() {
|
|
8299
|
+
return !this.disposed && this.snapshot.open && !this.snapshot.hostBusy && !this.snapshot.committed;
|
|
8300
|
+
}
|
|
8301
|
+
current(generation) {
|
|
8302
|
+
return !this.disposed && this.snapshot.open && generation === this.generation;
|
|
8303
|
+
}
|
|
8304
|
+
withdrawRead() {
|
|
8305
|
+
this.generation++;
|
|
8306
|
+
this.activeRead?.abort();
|
|
8307
|
+
this.activeRead = void 0;
|
|
8308
|
+
}
|
|
8309
|
+
publish(snapshot) {
|
|
8310
|
+
this.snapshot = Object.freeze(snapshot);
|
|
8311
|
+
for (const listener of this.listeners) listener();
|
|
8312
|
+
}
|
|
8313
|
+
};
|
|
8314
|
+
//#endregion
|
|
8315
|
+
//#region src/mobile-directory-picker.ts
|
|
8316
|
+
/** Dedicated Mobile directory-flow occupant; stock DSH retains its own chooser. */
|
|
8317
|
+
const MESSAGES = {
|
|
8318
|
+
zh: {
|
|
8319
|
+
title: "选择工作区目录",
|
|
8320
|
+
path: "目录路径",
|
|
8321
|
+
go: "前往",
|
|
8322
|
+
home: "主目录",
|
|
8323
|
+
up: "上一级",
|
|
8324
|
+
cancel: "取消",
|
|
8325
|
+
close: "关闭",
|
|
8326
|
+
open: "打开",
|
|
8327
|
+
retry: "重试",
|
|
8328
|
+
loading: "正在读取文件夹…",
|
|
8329
|
+
adopting: "正在打开工作区…",
|
|
8330
|
+
empty: "此目录中没有子文件夹",
|
|
8331
|
+
truncated: "文件夹过多,仅显示开头部分;可输入完整路径前往其他目录",
|
|
8332
|
+
"bad-path": "请输入电脑上的绝对目录路径,例如 C:\\projects 或 /home/user/projects",
|
|
8333
|
+
unavailable: "无法读取此目录,请检查路径和访问权限后重试",
|
|
8334
|
+
unauthorized: "连接授权不可用,请关闭窗口并重新连接电脑",
|
|
8335
|
+
"invalid-response": "目录响应不可用,请重试或重新连接电脑",
|
|
8336
|
+
"request-failed": "读取目录失败,请检查连接后重试",
|
|
8337
|
+
timeout: "读取目录超时,请重试或取消",
|
|
8338
|
+
failedOpen: "无法打开目录选择器,请重新连接后重试"
|
|
8339
|
+
},
|
|
8340
|
+
en: {
|
|
8341
|
+
title: "Select workspace directory",
|
|
8342
|
+
path: "Directory path",
|
|
8343
|
+
go: "Go",
|
|
8344
|
+
home: "Home",
|
|
8345
|
+
up: "Up one level",
|
|
8346
|
+
cancel: "Cancel",
|
|
8347
|
+
close: "Close",
|
|
8348
|
+
open: "Open",
|
|
8349
|
+
retry: "Retry",
|
|
8350
|
+
loading: "Loading folders…",
|
|
8351
|
+
adopting: "Opening workspace…",
|
|
8352
|
+
empty: "No subfolders in this directory",
|
|
8353
|
+
truncated: "Only the first folders are shown; enter a full path to open another directory",
|
|
8354
|
+
"bad-path": "Enter an absolute computer path, such as C:\\projects or /home/user/projects",
|
|
8355
|
+
unavailable: "Could not read this directory. Check its path and access permissions, then retry.",
|
|
8356
|
+
unauthorized: "Connection authorization is unavailable. Close this dialog and reconnect to the computer.",
|
|
8357
|
+
"invalid-response": "The directory response is unavailable. Retry or reconnect to the computer.",
|
|
8358
|
+
"request-failed": "Could not load this directory. Check the connection and retry.",
|
|
8359
|
+
timeout: "Loading timed out. Retry or cancel.",
|
|
8360
|
+
failedOpen: "Could not open the directory picker. Reconnect and retry."
|
|
8361
|
+
},
|
|
8362
|
+
it: {
|
|
8363
|
+
title: "Seleziona la cartella del workspace",
|
|
8364
|
+
path: "Percorso della cartella",
|
|
8365
|
+
go: "Vai",
|
|
8366
|
+
home: "Home",
|
|
8367
|
+
up: "Cartella superiore",
|
|
8368
|
+
cancel: "Annulla",
|
|
8369
|
+
close: "Chiudi",
|
|
8370
|
+
open: "Apri",
|
|
8371
|
+
retry: "Riprova",
|
|
8372
|
+
loading: "Caricamento delle cartelle…",
|
|
8373
|
+
adopting: "Apertura del workspace…",
|
|
8374
|
+
empty: "Nessuna sottocartella in questa cartella",
|
|
8375
|
+
truncated: "Sono mostrate solo le prime cartelle; inserisci un percorso completo per aprirne un’altra",
|
|
8376
|
+
"bad-path": "Inserisci un percorso assoluto del computer, come C:\\projects o /home/user/projects",
|
|
8377
|
+
unavailable: "Impossibile leggere questa cartella. Controlla il percorso e i permessi, poi riprova.",
|
|
8378
|
+
unauthorized: "Autorizzazione non disponibile. Chiudi questa finestra e riconnettiti al computer.",
|
|
8379
|
+
"invalid-response": "Risposta della cartella non disponibile. Riprova o riconnettiti al computer.",
|
|
8380
|
+
"request-failed": "Impossibile caricare la cartella. Controlla la connessione e riprova.",
|
|
8381
|
+
timeout: "Caricamento scaduto. Riprova o annulla.",
|
|
8382
|
+
failedOpen: "Impossibile aprire la selezione della cartella. Riconnettiti e riprova."
|
|
8383
|
+
}
|
|
8384
|
+
};
|
|
8385
|
+
/** Authenticated read-only Gateway route; no native picker or Host service is replaced. */
|
|
8386
|
+
const WORKSPACE_DIRECTORIES_PATH = "/mobile-access/workspace-directories";
|
|
8387
|
+
/** Ask only this plugin's live directory dialog to handle a routed native Back. */
|
|
8388
|
+
function dismissMobileDirectoryPicker(dialog) {
|
|
8389
|
+
if (dialog.tagName !== "DIALOG" || dialog.getAttribute("data-dshm-picker-dialog") !== "true" || dialog.getAttribute("open") === null) return false;
|
|
8390
|
+
const dismiss = new Event("dsh-mobile:directory-picker-dismiss", { cancelable: true });
|
|
8391
|
+
dialog.dispatchEvent(dismiss);
|
|
8392
|
+
return dismiss.defaultPrevented;
|
|
8393
|
+
}
|
|
8394
|
+
function localeOf(doc) {
|
|
8395
|
+
const languages = [doc.documentElement.lang, ...doc.defaultView?.navigator.languages ?? []];
|
|
8396
|
+
for (const language of languages) {
|
|
8397
|
+
const code = language.toLowerCase().split("-")[0];
|
|
8398
|
+
if (code === "zh" || code === "en" || code === "it") return code;
|
|
8399
|
+
}
|
|
8400
|
+
return "en";
|
|
8401
|
+
}
|
|
8402
|
+
async function readDirectories(path, signal) {
|
|
8403
|
+
const query = path === null ? "" : `?path=${encodeURIComponent(path)}`;
|
|
8404
|
+
const response = await fetch(`${WORKSPACE_DIRECTORIES_PATH}${query}`, {
|
|
8405
|
+
credentials: "same-origin",
|
|
8406
|
+
cache: "no-store",
|
|
8407
|
+
signal
|
|
8408
|
+
});
|
|
8409
|
+
if (!response.ok) {
|
|
8410
|
+
const code = response.status === 400 ? "bad-path" : response.status === 401 || response.status === 403 ? "unauthorized" : "unavailable";
|
|
8411
|
+
try {
|
|
8412
|
+
await response.body?.cancel();
|
|
8413
|
+
} catch (error) {
|
|
8414
|
+
throw new MobileDirectoryReadError(code, { cause: error });
|
|
8415
|
+
}
|
|
8416
|
+
throw new MobileDirectoryReadError(code);
|
|
8417
|
+
}
|
|
8418
|
+
try {
|
|
8419
|
+
return await response.json();
|
|
8420
|
+
} catch (error) {
|
|
8421
|
+
throw new MobileDirectoryReadError("invalid-response", { cause: error });
|
|
8422
|
+
}
|
|
8423
|
+
}
|
|
8424
|
+
const STYLES = `
|
|
8425
|
+
dialog.dshm-picker{position:fixed;inset:0;box-sizing:border-box;width:100%;max-width:none;height:100%;max-height:none;margin:0;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));border:0;background:transparent;color:var(--dsw-alias-label-primary,#16181d);font:400 14px/1.5 system-ui;overflow:hidden}
|
|
8426
|
+
dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::backdrop{background:rgb(0 0 0 / 42%)}
|
|
8427
|
+
.dshm-picker__panel{display:flex;flex-direction:column;box-sizing:border-box;width:min(680px,100%);height:min(500px,100%);min-height:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2,#dde0e4);border-radius:16px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 16px 56px rgb(0 0 0 / 18%)}
|
|
8428
|
+
.dshm-picker__header{display:flex;align-items:center;flex:none;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#dde0e4)}
|
|
8429
|
+
.dshm-picker__title{flex:1;min-width:0;margin:0;font-size:16px;font-weight:500;overflow-wrap:anywhere}
|
|
8430
|
+
.dshm-picker__toolbar{display:flex;flex-wrap:wrap;gap:8px;flex:none;padding:12px 16px 8px}.dshm-picker__path-label{flex:1;min-width:min(180px,100%);display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary,#606873)}
|
|
8431
|
+
.dshm-picker__path{box-sizing:border-box;width:100%;min-width:0;min-height:48px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:400 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace}
|
|
8432
|
+
.dshm-picker__path[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary,#b42318)}.dshm-picker__go{align-self:flex-end}
|
|
8433
|
+
.dshm-picker__navigation{display:flex;flex:none;gap:8px;padding:0 16px 8px}.dshm-picker__navigation .dshm-picker__button{flex:1;white-space:normal}
|
|
8434
|
+
.dshm-picker__list{flex:1;min-height:0;margin:0;padding:4px 12px;overflow:auto;overscroll-behavior:contain;list-style:none;scroll-padding:4px}
|
|
8435
|
+
.dshm-picker__row{display:flex;align-items:center;gap:12px;box-sizing:border-box;width:100%;min-height:48px;padding:10px 12px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;touch-action:manipulation}
|
|
8436
|
+
.dshm-picker__folder{flex:none;width:20px;height:20px;color:var(--dsw-alias-label-secondary,#606873)}
|
|
8437
|
+
.dshm-picker__name{flex:1;min-width:0;overflow-wrap:anywhere}.dshm-picker__row:hover:not(:disabled),.dshm-picker__button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#eef1f5)}
|
|
8438
|
+
.dshm-picker__row:active:not(:disabled),.dshm-picker__button:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active,#e4e8ee)}
|
|
8439
|
+
.dshm-picker__button{flex:none;box-sizing:border-box;min-width:48px;min-height:48px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:transparent;color:inherit;font:500 14px/1.4 system-ui;cursor:pointer;touch-action:manipulation}
|
|
8440
|
+
.dshm-picker :is(button,input):focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#2563eb);outline-offset:2px}.dshm-picker button:disabled{opacity:.5;cursor:default}
|
|
8441
|
+
.dshm-picker__status{flex:none;margin:0;padding:8px 16px;font-size:13px;color:var(--dsw-alias-label-secondary,#606873);overflow-wrap:anywhere}.dshm-picker__error{color:var(--dsw-alias-state-error-primary,#b42318)}
|
|
8442
|
+
.dshm-picker__status[hidden],.dshm-picker__button[hidden]{display:none}.dshm-picker__footer{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;flex:none;gap:8px;padding:12px 16px;border-top:1px solid var(--dsw-alias-border-l2,#dde0e4)}
|
|
8443
|
+
.dshm-picker__confirm{border-color:transparent;background:var(--dsw-alias-button-primary-fill,#2563eb);color:var(--dsw-alias-label-primary-foreground,#fff)}.dshm-picker__confirm:hover:not(:disabled),.dshm-picker__confirm:active:not(:disabled){background:var(--dsw-alias-button-primary-hover,#1d4ed8)}
|
|
8444
|
+
@media(max-width:420px){dialog.dshm-picker{padding:max(8px,env(safe-area-inset-top)) max(8px,env(safe-area-inset-right)) max(8px,env(safe-area-inset-bottom)) max(8px,env(safe-area-inset-left))}.dshm-picker__header,.dshm-picker__toolbar,.dshm-picker__footer{padding-left:12px;padding-right:12px}.dshm-picker__footer .dshm-picker__button{flex:1}.dshm-picker__path-label{min-width:0}}
|
|
8445
|
+
`;
|
|
8446
|
+
function openDirectoryDialog(doc, getProps, trackDisposal) {
|
|
8447
|
+
const view = doc.defaultView;
|
|
8448
|
+
if (view === null) throw new Error("Directory picker requires a browser window");
|
|
8449
|
+
const copy = MESSAGES[localeOf(doc)];
|
|
8450
|
+
const active = doc.activeElement;
|
|
8451
|
+
const activeText = active instanceof HTMLElement && (active.isContentEditable || active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement);
|
|
8452
|
+
const trigger = active instanceof HTMLElement && !activeText ? active : void 0;
|
|
8453
|
+
if (activeText) active.blur();
|
|
8454
|
+
const dialog = doc.createElement("dialog");
|
|
8455
|
+
dialog.className = "dshm-picker";
|
|
8456
|
+
dialog.lang = localeOf(doc);
|
|
8457
|
+
dialog.setAttribute("role", "dialog");
|
|
8458
|
+
dialog.setAttribute("aria-modal", "true");
|
|
8459
|
+
dialog.setAttribute("aria-label", copy.title);
|
|
8460
|
+
dialog.setAttribute("data-dshm-picker-dialog", "true");
|
|
8461
|
+
const panel = doc.createElement("section");
|
|
8462
|
+
panel.className = "dshm-picker__panel";
|
|
8463
|
+
const button = (text, name) => {
|
|
8464
|
+
const value = doc.createElement("button");
|
|
8465
|
+
value.type = "button";
|
|
8466
|
+
value.textContent = text;
|
|
8467
|
+
value.className = "dshm-picker__button";
|
|
8468
|
+
if (name !== void 0) value.setAttribute(`data-dshm-picker-${name}`, "");
|
|
8469
|
+
return value;
|
|
8470
|
+
};
|
|
8471
|
+
const header = doc.createElement("header");
|
|
8472
|
+
header.className = "dshm-picker__header";
|
|
8473
|
+
const title = doc.createElement("h2");
|
|
8474
|
+
title.className = "dshm-picker__title";
|
|
8475
|
+
title.textContent = copy.title;
|
|
8476
|
+
const close = button(copy.close);
|
|
8477
|
+
header.append(title, close);
|
|
8478
|
+
const toolbar = doc.createElement("form");
|
|
8479
|
+
toolbar.className = "dshm-picker__toolbar";
|
|
8480
|
+
const pathLabel = doc.createElement("label");
|
|
8481
|
+
pathLabel.className = "dshm-picker__path-label";
|
|
8482
|
+
pathLabel.textContent = copy.path;
|
|
8483
|
+
const path = doc.createElement("input");
|
|
8484
|
+
path.type = "text";
|
|
8485
|
+
path.className = "dshm-picker__path";
|
|
8486
|
+
path.setAttribute("data-dshm-picker-path", "");
|
|
8487
|
+
path.spellcheck = false;
|
|
8488
|
+
path.autocomplete = "off";
|
|
8489
|
+
path.maxLength = 8192;
|
|
8490
|
+
pathLabel.append(path);
|
|
8491
|
+
const go = button(copy.go, "load");
|
|
8492
|
+
go.type = "submit";
|
|
8493
|
+
go.classList.add("dshm-picker__go");
|
|
8494
|
+
toolbar.append(pathLabel, go);
|
|
8495
|
+
const navigation = doc.createElement("nav");
|
|
8496
|
+
navigation.className = "dshm-picker__navigation";
|
|
8497
|
+
navigation.setAttribute("aria-label", copy.title);
|
|
8498
|
+
const home = button(copy.home, "home");
|
|
8499
|
+
const up = button(copy.up, "up");
|
|
8500
|
+
navigation.append(home, up);
|
|
8501
|
+
const status = doc.createElement("p");
|
|
8502
|
+
status.className = "dshm-picker__status";
|
|
8503
|
+
status.setAttribute("role", "status");
|
|
8504
|
+
status.setAttribute("aria-live", "polite");
|
|
8505
|
+
const error = doc.createElement("p");
|
|
8506
|
+
error.className = "dshm-picker__status dshm-picker__error";
|
|
8507
|
+
error.setAttribute("role", "alert");
|
|
8508
|
+
error.hidden = true;
|
|
8509
|
+
const list = doc.createElement("ul");
|
|
8510
|
+
list.className = "dshm-picker__list";
|
|
8511
|
+
list.setAttribute("aria-label", copy.title);
|
|
8512
|
+
const footer = doc.createElement("footer");
|
|
8513
|
+
footer.className = "dshm-picker__footer";
|
|
8514
|
+
const retry = button(copy.retry, "retry");
|
|
8515
|
+
retry.hidden = true;
|
|
8516
|
+
const cancel = button(copy.cancel, "cancel");
|
|
8517
|
+
const confirm = button(copy.open, "confirm");
|
|
8518
|
+
confirm.classList.add("dshm-picker__confirm");
|
|
8519
|
+
footer.append(retry, cancel, confirm);
|
|
8520
|
+
panel.append(header, toolbar, navigation, status, error, list, footer);
|
|
8521
|
+
dialog.append(panel);
|
|
8522
|
+
const browser = new MobileDirectoryBrowser(readDirectories);
|
|
8523
|
+
let disposed = false;
|
|
8524
|
+
let closedByOwner = false;
|
|
8525
|
+
let renderedListing;
|
|
8526
|
+
let disposal;
|
|
8527
|
+
const render = () => {
|
|
8528
|
+
if (disposed) return;
|
|
8529
|
+
const state = browser.getSnapshot();
|
|
8530
|
+
const adopting = state.committed || state.hostBusy;
|
|
8531
|
+
path.disabled = adopting;
|
|
8532
|
+
if (path.value !== state.draft) path.value = state.draft;
|
|
8533
|
+
path.setAttribute("aria-invalid", String(state.error === "bad-path"));
|
|
8534
|
+
go.disabled = adopting || state.phase === "loading" || !isMobileDirectoryPath(state.draft);
|
|
8535
|
+
home.disabled = adopting;
|
|
8536
|
+
up.disabled = adopting || state.listing?.parent === void 0;
|
|
8537
|
+
confirm.disabled = !browser.canConfirm();
|
|
8538
|
+
cancel.disabled = adopting;
|
|
8539
|
+
close.disabled = adopting;
|
|
8540
|
+
retry.hidden = state.phase !== "error" || state.error === "bad-path";
|
|
8541
|
+
retry.disabled = adopting;
|
|
8542
|
+
status.textContent = adopting ? copy.adopting : state.phase === "loading" ? copy.loading : state.listing?.truncated === true ? copy.truncated : state.listing?.entries.length === 0 ? copy.empty : "";
|
|
8543
|
+
status.hidden = status.textContent === "";
|
|
8544
|
+
error.textContent = state.error === null ? "" : copy[state.error];
|
|
8545
|
+
error.hidden = state.error === null;
|
|
8546
|
+
list.setAttribute("aria-busy", String(state.phase === "loading"));
|
|
8547
|
+
if (renderedListing !== state.listing) {
|
|
8548
|
+
renderedListing = state.listing;
|
|
8549
|
+
list.replaceChildren();
|
|
8550
|
+
for (const entry of state.listing?.entries ?? []) {
|
|
8551
|
+
const item = doc.createElement("li");
|
|
8552
|
+
const row = button("");
|
|
8553
|
+
row.className = "dshm-picker__row";
|
|
8554
|
+
const icon = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
8555
|
+
icon.setAttribute("class", "dshm-picker__folder");
|
|
8556
|
+
icon.setAttribute("aria-hidden", "true");
|
|
8557
|
+
icon.setAttribute("viewBox", "0 0 24 24");
|
|
8558
|
+
icon.setAttribute("fill", "none");
|
|
8559
|
+
icon.setAttribute("stroke", "currentColor");
|
|
8560
|
+
icon.setAttribute("stroke-width", "1.5");
|
|
8561
|
+
const outline = doc.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
8562
|
+
outline.setAttribute("d", "M3 7V6a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z");
|
|
8563
|
+
icon.append(outline);
|
|
8564
|
+
const name = doc.createElement("span");
|
|
8565
|
+
name.className = "dshm-picker__name";
|
|
8566
|
+
name.textContent = entry.name;
|
|
8567
|
+
row.append(icon, name);
|
|
8568
|
+
row.addEventListener("click", () => {
|
|
8569
|
+
browser.navigate(entry.path);
|
|
8570
|
+
});
|
|
8571
|
+
item.append(row);
|
|
8572
|
+
list.append(item);
|
|
8573
|
+
}
|
|
8574
|
+
}
|
|
8575
|
+
for (const row of list.querySelectorAll("button")) row.disabled = adopting;
|
|
8576
|
+
};
|
|
8577
|
+
const unsubscribe = browser.subscribe(render);
|
|
8578
|
+
const requestClose = () => {
|
|
8579
|
+
if (disposed || !browser.cancel()) return;
|
|
8580
|
+
closedByOwner = true;
|
|
8581
|
+
if (dialog.open) dialog.close();
|
|
8582
|
+
getProps().onCancel();
|
|
8583
|
+
};
|
|
8584
|
+
const onBack = (event) => {
|
|
8585
|
+
if (!event.cancelable || event.defaultPrevented || !dialog.open) return;
|
|
8586
|
+
event.preventDefault();
|
|
8587
|
+
event.stopImmediatePropagation();
|
|
8588
|
+
requestClose();
|
|
8589
|
+
};
|
|
8590
|
+
const onEscape = (event) => {
|
|
8591
|
+
if (event.key !== "Escape" || event.isComposing || event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented || !dialog.open) return;
|
|
8592
|
+
event.preventDefault();
|
|
8593
|
+
event.stopImmediatePropagation();
|
|
8594
|
+
requestClose();
|
|
8595
|
+
};
|
|
8596
|
+
const dispose = () => {
|
|
8597
|
+
if (disposal !== void 0) return disposal;
|
|
8598
|
+
disposed = true;
|
|
8599
|
+
unsubscribe();
|
|
8600
|
+
const stopped = browser.dispose();
|
|
8601
|
+
const failures = [];
|
|
8602
|
+
const clean = (action) => {
|
|
8603
|
+
try {
|
|
8604
|
+
action();
|
|
8605
|
+
} catch (error) {
|
|
8606
|
+
failures.push(error);
|
|
8607
|
+
}
|
|
8608
|
+
};
|
|
8609
|
+
clean(() => {
|
|
8610
|
+
view.removeEventListener("dsh-mobile:native-back", onBack, true);
|
|
8611
|
+
});
|
|
8612
|
+
clean(() => {
|
|
8613
|
+
view.removeEventListener("keydown", onEscape, true);
|
|
8614
|
+
});
|
|
8615
|
+
closedByOwner = true;
|
|
8616
|
+
clean(() => {
|
|
8617
|
+
if (dialog.open) dialog.close();
|
|
8618
|
+
});
|
|
8619
|
+
clean(() => {
|
|
8620
|
+
dialog.remove();
|
|
8621
|
+
});
|
|
8622
|
+
clean(() => {
|
|
8623
|
+
if (trigger?.isConnected) trigger.focus({ preventScroll: true });
|
|
8624
|
+
});
|
|
8625
|
+
disposal = (async () => {
|
|
8626
|
+
try {
|
|
8627
|
+
await stopped;
|
|
8628
|
+
} catch (error) {
|
|
8629
|
+
failures.push(error);
|
|
8630
|
+
}
|
|
8631
|
+
if (failures.length > 0) throw new AggregateError(failures, "Mobile directory dialog disposal failed");
|
|
8632
|
+
})();
|
|
8633
|
+
return disposal;
|
|
8634
|
+
};
|
|
8635
|
+
toolbar.addEventListener("submit", (event) => {
|
|
8636
|
+
event.preventDefault();
|
|
8637
|
+
browser.navigate(path.value);
|
|
8638
|
+
});
|
|
8639
|
+
path.addEventListener("input", () => {
|
|
8640
|
+
browser.setDraft(path.value);
|
|
8641
|
+
});
|
|
8642
|
+
home.addEventListener("click", () => {
|
|
8643
|
+
browser.navigate(null);
|
|
8644
|
+
});
|
|
8645
|
+
up.addEventListener("click", () => {
|
|
8646
|
+
const parent = browser.getSnapshot().listing?.parent;
|
|
8647
|
+
if (parent !== void 0) browser.navigate(parent);
|
|
8648
|
+
});
|
|
8649
|
+
retry.addEventListener("click", () => {
|
|
8650
|
+
browser.retry();
|
|
8651
|
+
});
|
|
8652
|
+
cancel.addEventListener("click", requestClose);
|
|
8653
|
+
close.addEventListener("click", requestClose);
|
|
8654
|
+
confirm.addEventListener("click", () => {
|
|
8655
|
+
const picked = browser.confirm();
|
|
8656
|
+
if (picked !== void 0) getProps().onPicked(picked);
|
|
8657
|
+
});
|
|
8658
|
+
dialog.addEventListener("cancel", (event) => {
|
|
8659
|
+
event.preventDefault();
|
|
8660
|
+
requestClose();
|
|
8661
|
+
});
|
|
8662
|
+
dialog.addEventListener("dsh-mobile:directory-picker-dismiss", (event) => {
|
|
8663
|
+
if (!dialog.open || disposed || !event.cancelable) return;
|
|
8664
|
+
event.preventDefault();
|
|
8665
|
+
requestClose();
|
|
8666
|
+
});
|
|
8667
|
+
dialog.addEventListener("click", (event) => {
|
|
8668
|
+
if (event.target === dialog) requestClose();
|
|
8669
|
+
});
|
|
8670
|
+
dialog.addEventListener("close", () => {
|
|
8671
|
+
if (!disposed && !closedByOwner && browser.cancel()) getProps().onCancel();
|
|
8672
|
+
});
|
|
8673
|
+
view.addEventListener("dsh-mobile:native-back", onBack, true);
|
|
8674
|
+
view.addEventListener("keydown", onEscape, true);
|
|
8675
|
+
try {
|
|
8676
|
+
doc.body.append(dialog);
|
|
8677
|
+
dialog.showModal();
|
|
8678
|
+
close.focus({ preventScroll: true });
|
|
8679
|
+
browser.start();
|
|
8680
|
+
browser.setHostBusy(getProps().busy === true);
|
|
8681
|
+
} catch (error) {
|
|
8682
|
+
trackDisposal(dispose());
|
|
8683
|
+
throw error;
|
|
8684
|
+
}
|
|
8685
|
+
return {
|
|
8686
|
+
setHostBusy: (value) => {
|
|
8687
|
+
browser.setHostBusy(value);
|
|
8688
|
+
},
|
|
8689
|
+
dispose
|
|
8690
|
+
};
|
|
8691
|
+
}
|
|
8692
|
+
/** Register two public directory flows and drain their read work before removing styles. */
|
|
8693
|
+
function installMobileDirectoryPicker(ctx, doc) {
|
|
8694
|
+
const style = doc.createElement("style");
|
|
8695
|
+
style.dataset.plugin = "dsh-mobile-directory-picker";
|
|
8696
|
+
style.textContent = STYLES;
|
|
8697
|
+
doc.head.append(style);
|
|
8698
|
+
const owners = /* @__PURE__ */ new Set();
|
|
8699
|
+
const pending = /* @__PURE__ */ new Set();
|
|
8700
|
+
const cleanupFailures = [];
|
|
8701
|
+
let disposed = false;
|
|
8702
|
+
const trackDisposal = (task) => {
|
|
8703
|
+
pending.add(task);
|
|
8704
|
+
task.then(() => {
|
|
8705
|
+
pending.delete(task);
|
|
8706
|
+
}, (error) => {
|
|
8707
|
+
pending.delete(task);
|
|
8708
|
+
cleanupFailures.push(error);
|
|
8709
|
+
console.error("DSH Mobile directory picker cleanup failed");
|
|
8710
|
+
});
|
|
8711
|
+
};
|
|
8712
|
+
const retire = (owner) => {
|
|
8713
|
+
if (owners.delete(owner)) trackDisposal(owner.dispose());
|
|
8714
|
+
};
|
|
8715
|
+
function MobileDirectoryFlow(props) {
|
|
8716
|
+
const current = (0, react.useRef)(props);
|
|
8717
|
+
current.current = props;
|
|
8718
|
+
const owner = (0, react.useRef)(void 0);
|
|
8719
|
+
(0, react.useEffect)(() => {
|
|
8720
|
+
if (!props.open || disposed) return;
|
|
8721
|
+
try {
|
|
8722
|
+
const dialog = openDirectoryDialog(doc, () => current.current, trackDisposal);
|
|
8723
|
+
owners.add(dialog);
|
|
8724
|
+
owner.current = dialog;
|
|
8725
|
+
return () => {
|
|
8726
|
+
owner.current = void 0;
|
|
8727
|
+
retire(dialog);
|
|
8728
|
+
};
|
|
8729
|
+
} catch (error) {
|
|
8730
|
+
console.error("DSH Mobile directory picker could not open");
|
|
8731
|
+
current.current.onError(MESSAGES[localeOf(doc)].failedOpen);
|
|
8732
|
+
}
|
|
8733
|
+
}, [props.open]);
|
|
8734
|
+
(0, react.useEffect)(() => {
|
|
8735
|
+
owner.current?.setHostBusy(props.busy === true);
|
|
8736
|
+
}, [props.busy]);
|
|
8737
|
+
return null;
|
|
8738
|
+
}
|
|
8739
|
+
let remove;
|
|
8740
|
+
try {
|
|
8741
|
+
remove = ctx.slots.inject("sidebar.workspaces.directoryFlow", () => ctx.slots.inject("conversation.hero.workspace.directoryFlow", () => {
|
|
8742
|
+
const removeSidebar = ctx.slots.register({
|
|
8743
|
+
name: "sidebar.workspaces.directoryFlow",
|
|
8744
|
+
id: "dsh-mobile-directory-picker",
|
|
8745
|
+
priority: -1
|
|
8746
|
+
}, MobileDirectoryFlow);
|
|
8747
|
+
let removeHero;
|
|
8748
|
+
try {
|
|
8749
|
+
removeHero = ctx.slots.register({
|
|
8750
|
+
name: "conversation.hero.workspace.directoryFlow",
|
|
8751
|
+
id: "dsh-mobile-directory-picker",
|
|
8752
|
+
priority: -1
|
|
8753
|
+
}, MobileDirectoryFlow);
|
|
8754
|
+
} catch (error) {
|
|
8755
|
+
removeSidebar();
|
|
8756
|
+
throw error;
|
|
8757
|
+
}
|
|
8758
|
+
return () => {
|
|
8759
|
+
removeHero();
|
|
8760
|
+
removeSidebar();
|
|
8761
|
+
};
|
|
8762
|
+
}));
|
|
8763
|
+
} catch (error) {
|
|
8764
|
+
style.remove();
|
|
8765
|
+
throw error;
|
|
8766
|
+
}
|
|
8767
|
+
return async () => {
|
|
8768
|
+
disposed = true;
|
|
8769
|
+
const failures = [];
|
|
8770
|
+
try {
|
|
8771
|
+
remove();
|
|
8772
|
+
} catch (error) {
|
|
8773
|
+
failures.push(error);
|
|
8774
|
+
}
|
|
8775
|
+
for (const owner of [...owners]) try {
|
|
8776
|
+
retire(owner);
|
|
8777
|
+
} catch (error) {
|
|
8778
|
+
failures.push(error);
|
|
8779
|
+
}
|
|
8780
|
+
await Promise.allSettled([...pending]);
|
|
8781
|
+
style.remove();
|
|
8782
|
+
failures.push(...cleanupFailures);
|
|
8783
|
+
if (failures.length > 0) throw new AggregateError(failures, "Mobile directory picker disposal failed");
|
|
8784
|
+
};
|
|
8785
|
+
}
|
|
8786
|
+
//#endregion
|
|
7202
8787
|
//#region src/client.ts
|
|
7203
8788
|
/** Map a host-reported provider onto the UI union, defaulting to the built-in one. */
|
|
7204
8789
|
function parseMobileRemoteProvider(value) {
|
|
@@ -7253,6 +8838,7 @@ window.__ModuleLoader__.load({
|
|
|
7253
8838
|
const top = layers.at(-1);
|
|
7254
8839
|
if (top === void 0) return;
|
|
7255
8840
|
claim();
|
|
8841
|
+
if (dismissMobileDirectoryPicker(top)) return;
|
|
7256
8842
|
const modelEntry = [...modelPopups].find(([popup]) => popup.contains(top));
|
|
7257
8843
|
if (modelEntry !== void 0) {
|
|
7258
8844
|
const [modelPopup, trigger] = modelEntry;
|
|
@@ -7747,6 +9333,10 @@ window.__ModuleLoader__.load({
|
|
|
7747
9333
|
frp_discovery_invalid: "frpDiscoveryInvalid",
|
|
7748
9334
|
frp_stopped: "frpStopped",
|
|
7749
9335
|
frp_exited: "frpExited",
|
|
9336
|
+
frp_server_address_invalid: "frpServerAddressInvalid",
|
|
9337
|
+
frp_server_port_invalid: "frpServerPortInvalid",
|
|
9338
|
+
frp_token_invalid: "frpTokenInvalid",
|
|
9339
|
+
frp_public_origin_invalid: "frpPublicOriginInvalid",
|
|
7750
9340
|
frp_attach_mode_requires_vhost_port: "frpAttachModeRequiresVhostPort",
|
|
7751
9341
|
frp_attach_cert_unknown: "frpAttachCertUnknownError",
|
|
7752
9342
|
frp_entry_tls_invalid: "frpEntryTlsInvalid",
|
|
@@ -8480,6 +10070,52 @@ window.__ModuleLoader__.load({
|
|
|
8480
10070
|
frpPortLabel.append(frpPort);
|
|
8481
10071
|
frpTokenLabel.append(frpToken);
|
|
8482
10072
|
frpOriginLabel.append(frpOrigin);
|
|
10073
|
+
const frpInputs = {
|
|
10074
|
+
serverAddress: frpServer,
|
|
10075
|
+
serverPort: frpPort,
|
|
10076
|
+
token: frpToken,
|
|
10077
|
+
publicOrigin: frpOrigin,
|
|
10078
|
+
mode: frpMode,
|
|
10079
|
+
entryTls: frpEntryTls,
|
|
10080
|
+
vhostHttpPort: frpVhostPort,
|
|
10081
|
+
publicPort: frpPublicPort
|
|
10082
|
+
};
|
|
10083
|
+
const frpInputLabels = {
|
|
10084
|
+
serverAddress: frpServerLabel,
|
|
10085
|
+
serverPort: frpPortLabel,
|
|
10086
|
+
token: frpTokenLabel,
|
|
10087
|
+
publicOrigin: frpOriginLabel,
|
|
10088
|
+
mode: frpModeLabel,
|
|
10089
|
+
entryTls: frpEntryTlsLabel,
|
|
10090
|
+
vhostHttpPort: frpVhostLabel,
|
|
10091
|
+
publicPort: frpPublicPortLabel
|
|
10092
|
+
};
|
|
10093
|
+
const frpLabelKeys = {
|
|
10094
|
+
serverAddress: "frpServerAddress",
|
|
10095
|
+
serverPort: "frpServerPort",
|
|
10096
|
+
token: "frpToken",
|
|
10097
|
+
publicOrigin: "frpPublicOrigin",
|
|
10098
|
+
mode: "frpMode",
|
|
10099
|
+
entryTls: "frpEntryTls",
|
|
10100
|
+
vhostHttpPort: "frpVhostHttpPort",
|
|
10101
|
+
publicPort: "frpPublicPort"
|
|
10102
|
+
};
|
|
10103
|
+
const frpErrors = /* @__PURE__ */ new Map();
|
|
10104
|
+
for (const [name, input] of Object.entries(frpInputs)) {
|
|
10105
|
+
input.id = `dsh-mobile-frp-${name}`;
|
|
10106
|
+
input.setAttribute("aria-label", t(frpLabelKeys[name]));
|
|
10107
|
+
const error = element("p", "dsh-mobile-control__origin-feedback is-error");
|
|
10108
|
+
error.id = `${input.id}-error`;
|
|
10109
|
+
error.hidden = true;
|
|
10110
|
+
input.setAttribute("aria-describedby", [input.getAttribute("aria-describedby"), error.id].filter(Boolean).join(" "));
|
|
10111
|
+
frpInputLabels[name].append(error);
|
|
10112
|
+
frpErrors.set(name, error);
|
|
10113
|
+
}
|
|
10114
|
+
const frpOriginHint = element("p", "dsh-mobile-control__frp-hint");
|
|
10115
|
+
frpOriginHint.id = "dsh-mobile-frp-origin-hint";
|
|
10116
|
+
frpOriginHint.textContent = t("frpPublicOriginHint");
|
|
10117
|
+
frpOriginLabel.append(frpOriginHint);
|
|
10118
|
+
frpOrigin.setAttribute("aria-describedby", `${frpOriginHint.id} ${frpOrigin.getAttribute("aria-describedby") ?? ""}`);
|
|
8483
10119
|
frpFields.append(frpServerLabel, frpPortLabel, frpTokenLabel, frpOriginLabel, frpModeLabel, frpEntryTlsLabel, frpVhostLabel, frpPublicPortLabel);
|
|
8484
10120
|
frpStep1.append(frpStep1Title, frpStep1Text, frpFields);
|
|
8485
10121
|
const frpStep2 = element("section", "dsh-mobile-control__frp-step");
|
|
@@ -8581,10 +10217,18 @@ window.__ModuleLoader__.load({
|
|
|
8581
10217
|
frpAppRequirement.textContent = t("frpAppRequirement");
|
|
8582
10218
|
const frpConfigurationStatus = element("p", "dsh-mobile-control__component-status");
|
|
8583
10219
|
frpConfigurationStatus.textContent = t("frpConfigurationMissing");
|
|
10220
|
+
const frpFeedback = element("p", "dsh-mobile-control__origin-feedback is-error");
|
|
10221
|
+
frpFeedback.setAttribute("role", "status");
|
|
10222
|
+
frpFeedback.setAttribute("aria-live", "polite");
|
|
10223
|
+
frpFeedback.hidden = true;
|
|
10224
|
+
const frpActionHint = element("p", "dsh-mobile-control__frp-hint");
|
|
10225
|
+
frpActionHint.id = "dsh-mobile-frp-action-hint";
|
|
10226
|
+
frpActionHint.hidden = true;
|
|
8584
10227
|
const frpConfigure = element("button", "dsh-mobile-control__primary dsh-mobile-control__frp-action");
|
|
8585
10228
|
frpConfigure.type = "button";
|
|
8586
10229
|
frpConfigure.textContent = t("frpSaveConnect");
|
|
8587
|
-
|
|
10230
|
+
frpConfigure.setAttribute("aria-describedby", frpActionHint.id);
|
|
10231
|
+
frpStep4.append(frpStep4Title, frpStep4Text, frpAppRequirement, frpConfigurationStatus, frpFeedback, frpActionHint, frpConfigure);
|
|
8588
10232
|
const frpDetails = element("details", "dsh-mobile-control__details");
|
|
8589
10233
|
const frpDetailsSummary = element("summary");
|
|
8590
10234
|
frpDetailsSummary.textContent = t("frpComponentDetails");
|
|
@@ -9660,6 +11304,8 @@ window.__ModuleLoader__.load({
|
|
|
9660
11304
|
frpInstall.textContent = frpDownloadBytes > 0 ? t("installWithSize", { size: formatMegabytes(frpDownloadBytes) }) : t("installFrpc");
|
|
9661
11305
|
frpInstall.disabled = remoteProviderBusy;
|
|
9662
11306
|
frpConfigure.disabled = remoteProviderBusy || !frpInstalled;
|
|
11307
|
+
frpActionHint.hidden = !frpConfigure.disabled;
|
|
11308
|
+
frpActionHint.textContent = t(remoteProviderBusy ? "frpActionBusy" : !frpSupported ? "frpUnsupported" : "frpInstallBeforeConnect");
|
|
9663
11309
|
vpsDeploy.disabled = remoteProviderBusy || !validVpsDeploymentForm();
|
|
9664
11310
|
vpsCopyUninstall.disabled = vpsDeploy.disabled;
|
|
9665
11311
|
vpsUninstall.disabled = vpsDeploy.disabled;
|
|
@@ -10097,7 +11743,6 @@ window.__ModuleLoader__.load({
|
|
|
10097
11743
|
loadRemote();
|
|
10098
11744
|
});
|
|
10099
11745
|
});
|
|
10100
|
-
const validFrpServer = (value) => value === value.trim() && value.length > 0 && value.length <= 253 && !/[\s\u0000-\u001f\u007f/\\@?#]/u.test(value);
|
|
10101
11746
|
const frpForm = () => {
|
|
10102
11747
|
const vhostHttpPort = Number(frpVhostPort.value);
|
|
10103
11748
|
const publicPort = Number(frpPublicPort.value);
|
|
@@ -10109,8 +11754,8 @@ window.__ModuleLoader__.load({
|
|
|
10109
11754
|
publicOrigin: String(frpOrigin.value ?? "").trim(),
|
|
10110
11755
|
mode,
|
|
10111
11756
|
entryTls: frpEntryTls.value === "self-signed" ? "self-signed" : "public-ip-cert",
|
|
10112
|
-
...
|
|
10113
|
-
...
|
|
11757
|
+
...frpEntryTls.value === "self-signed" || frpVhostPort.value.trim() === "" ? {} : { vhostHttpPort },
|
|
11758
|
+
...frpEntryTls.value === "self-signed" ? { publicPort } : {}
|
|
10114
11759
|
};
|
|
10115
11760
|
};
|
|
10116
11761
|
/**
|
|
@@ -10127,20 +11772,39 @@ window.__ModuleLoader__.load({
|
|
|
10127
11772
|
...form.publicPort === void 0 ? {} : { publicPort: form.publicPort }
|
|
10128
11773
|
});
|
|
10129
11774
|
const validFrpForm = (form) => {
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
11775
|
+
return validateFrpForm(form).length === 0;
|
|
11776
|
+
};
|
|
11777
|
+
let frpValidationShown = false;
|
|
11778
|
+
let frpValidationAllowsSavedToken = false;
|
|
11779
|
+
const renderFrpIssues = (issues, focus = false) => {
|
|
11780
|
+
for (const [name, input] of Object.entries(frpInputs)) {
|
|
11781
|
+
const issue = issues.find((item) => item.field === name);
|
|
11782
|
+
const feedback = frpErrors.get(name);
|
|
11783
|
+
if (issue === void 0) input.removeAttribute("aria-invalid");
|
|
11784
|
+
else input.setAttribute("aria-invalid", "true");
|
|
11785
|
+
feedback.hidden = issue === void 0;
|
|
11786
|
+
feedback.textContent = issue === void 0 ? "" : t(issue.messageKey);
|
|
11787
|
+
}
|
|
11788
|
+
frpFeedback.hidden = issues.length === 0;
|
|
11789
|
+
frpFeedback.textContent = issues.map((issue) => t(issue.messageKey)).join(" ");
|
|
11790
|
+
if (issues.length !== 0 && focus) {
|
|
11791
|
+
frpConnectionGroup.open = true;
|
|
11792
|
+
frpInputs[issues[0].field].focus();
|
|
11793
|
+
}
|
|
11794
|
+
};
|
|
11795
|
+
const checkFrpForm = (form, allowSavedToken = false) => {
|
|
11796
|
+
const issues = validateFrpForm(form, allowSavedToken);
|
|
11797
|
+
frpValidationShown = true;
|
|
11798
|
+
frpValidationAllowsSavedToken = allowSavedToken;
|
|
11799
|
+
renderFrpIssues(issues, true);
|
|
11800
|
+
return issues.length === 0;
|
|
11801
|
+
};
|
|
11802
|
+
for (const [name, input] of Object.entries(frpInputs)) input.addEventListener(name === "mode" || name === "entryTls" ? "change" : "input", () => {
|
|
11803
|
+
if (frpValidationShown) renderFrpIssues(validateFrpForm(frpForm(), frpValidationAllowsSavedToken));
|
|
11804
|
+
});
|
|
10138
11805
|
frpCopyTemplate.addEventListener("click", () => {
|
|
10139
11806
|
const form = frpForm();
|
|
10140
|
-
if (!
|
|
10141
|
-
remoteStatus.textContent = t("frpInputInvalid");
|
|
10142
|
-
return;
|
|
10143
|
-
}
|
|
11807
|
+
if (!checkFrpForm(form)) return;
|
|
10144
11808
|
navigator.clipboard.writeText(createFrpServerTemplateForClipboard(form.serverPort, form.token, form.publicOrigin)).then(() => {
|
|
10145
11809
|
remoteStatus.textContent = t("templateCopied");
|
|
10146
11810
|
}, () => {
|
|
@@ -10150,10 +11814,13 @@ window.__ModuleLoader__.load({
|
|
|
10150
11814
|
const frpAttachErrorText = (code) => t(REMOTE_ERROR_MESSAGE_KEYS[code] ?? "frpInputInvalid");
|
|
10151
11815
|
const showFrpAttachError = (code) => {
|
|
10152
11816
|
const message = frpAttachErrorText(code);
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
11817
|
+
const issue = FRP_FORM_ERRORS[code];
|
|
11818
|
+
if (issue !== void 0) {
|
|
11819
|
+
frpValidationShown = true;
|
|
11820
|
+
renderFrpIssues([issue], true);
|
|
11821
|
+
} else {
|
|
11822
|
+
frpFeedback.textContent = message;
|
|
11823
|
+
frpFeedback.hidden = false;
|
|
10157
11824
|
}
|
|
10158
11825
|
remoteStatus.textContent = message;
|
|
10159
11826
|
};
|
|
@@ -10166,10 +11833,7 @@ window.__ModuleLoader__.load({
|
|
|
10166
11833
|
};
|
|
10167
11834
|
frpCopyAttachPlan.addEventListener("click", () => {
|
|
10168
11835
|
const form = frpForm();
|
|
10169
|
-
if (!
|
|
10170
|
-
remoteStatus.textContent = t("frpInputInvalid");
|
|
10171
|
-
return;
|
|
10172
|
-
}
|
|
11836
|
+
if (!checkFrpForm(form, frpConfigured)) return;
|
|
10173
11837
|
if (form.token !== "") {
|
|
10174
11838
|
const attachForm = attachClipboardForm(form);
|
|
10175
11839
|
const code = frpAttachFormErrorCode(attachForm);
|
|
@@ -10202,7 +11866,7 @@ window.__ModuleLoader__.load({
|
|
|
10202
11866
|
remoteStatus.textContent = t("frpAttachPlanCopied");
|
|
10203
11867
|
});
|
|
10204
11868
|
}, (error) => {
|
|
10205
|
-
|
|
11869
|
+
showFrpAttachError(error instanceof Error ? error.message : String(error));
|
|
10206
11870
|
}).catch((error) => {
|
|
10207
11871
|
remoteStatus.textContent = t("frpAttachPlanFailed", { error: String(error) });
|
|
10208
11872
|
}).finally(() => {
|
|
@@ -10213,10 +11877,7 @@ window.__ModuleLoader__.load({
|
|
|
10213
11877
|
/** Copy a token-bearing frpc.toml only from the token the user just typed. */
|
|
10214
11878
|
frpCopyAttachToken.addEventListener("click", () => {
|
|
10215
11879
|
const form = frpForm();
|
|
10216
|
-
if (!
|
|
10217
|
-
remoteStatus.textContent = t("frpInputInvalid");
|
|
10218
|
-
return;
|
|
10219
|
-
}
|
|
11880
|
+
if (!checkFrpForm(form, frpConfigured)) return;
|
|
10220
11881
|
if (form.token !== "") {
|
|
10221
11882
|
const attachForm = attachClipboardForm(form);
|
|
10222
11883
|
const code = frpAttachFormErrorCode(attachForm);
|
|
@@ -10563,15 +12224,13 @@ window.__ModuleLoader__.load({
|
|
|
10563
12224
|
const form = frpForm();
|
|
10564
12225
|
const unchanged = frpConfigured && form.token === "" && form.serverAddress === configuredFrpServer && form.serverPort === configuredFrpPort && form.publicOrigin === configuredFrpOrigin && form.mode === (frpConfiguredMode === "attach" ? "attach" : "deploy") && form.entryTls === (frpConfiguredEntryTls === "self-signed" ? "self-signed" : "public-ip-cert") && (form.vhostHttpPort ?? configuredFrpVhostPort) === configuredFrpVhostPort && (form.publicPort ?? configuredFrpPublicPort) === configuredFrpPublicPort;
|
|
10565
12226
|
if (!unchanged) {
|
|
10566
|
-
|
|
10567
|
-
if (code !== void 0) {
|
|
10568
|
-
if (form.mode === "attach") showFrpAttachError(code);
|
|
10569
|
-
else remoteStatus.textContent = t("frpInputInvalid");
|
|
10570
|
-
return;
|
|
10571
|
-
}
|
|
12227
|
+
if (!checkFrpForm(form)) return;
|
|
10572
12228
|
}
|
|
12229
|
+
renderFrpIssues([]);
|
|
10573
12230
|
remoteProviderBusy = true;
|
|
10574
12231
|
frpConfigure.disabled = true;
|
|
12232
|
+
frpActionHint.hidden = false;
|
|
12233
|
+
frpActionHint.textContent = t("frpActionBusy");
|
|
10575
12234
|
frpConfigure.setAttribute("aria-busy", "true");
|
|
10576
12235
|
frpConfigure.textContent = t("saving");
|
|
10577
12236
|
(unchanged ? Promise.resolve({}) : controlRequestJson("/api/mobile-access/remote/frp/configure", {
|
|
@@ -10585,8 +12244,12 @@ window.__ModuleLoader__.load({
|
|
|
10585
12244
|
body: JSON.stringify({ running: true })
|
|
10586
12245
|
});
|
|
10587
12246
|
}).then(renderRemote, (error) => {
|
|
10588
|
-
|
|
10589
|
-
|
|
12247
|
+
const code = error instanceof Error ? error.message : String(error);
|
|
12248
|
+
if (FRP_FORM_ERRORS[code] !== void 0) showFrpAttachError(code);
|
|
12249
|
+
else {
|
|
12250
|
+
remoteFailureText = t("configureFailed", { error: String(error) });
|
|
12251
|
+
remoteStatus.textContent = remoteFailureText;
|
|
12252
|
+
}
|
|
10590
12253
|
}).finally(() => {
|
|
10591
12254
|
remoteProviderBusy = false;
|
|
10592
12255
|
frpConfigure.setAttribute("aria-busy", "false");
|
|
@@ -12460,6 +14123,8 @@ window.__ModuleLoader__.load({
|
|
|
12460
14123
|
.dsh-mobile-control__switcher{display:grid;grid-template-columns:repeat(2,1fr);gap:4px;margin:0 0 14px;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f3f5f8)}.dsh-mobile-control__switcher[hidden]{display:none}.dsh-mobile-control__tab{min-height:36px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-secondary,#606873);font:600 13px/1 system-ui;cursor:pointer}.dsh-mobile-control__tab.is-active{background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);box-shadow:0 1px 3px rgb(15 23 42 / 10%)}.dsh-mobile-control__view[hidden]{display:none}.dsh-mobile-control__intro{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.55}.dsh-mobile-control__http-frame-warning{box-sizing:border-box;margin:-4px 0 12px;padding:8px 10px;border:1px solid #d6a83f;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#fff7df);color:var(--dsw-alias-label-primary,#694b00);font-size:11px;line-height:1.5;overflow-wrap:anywhere}.dsh-mobile-control__http-frame-warning[hidden]{display:none}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions button[hidden]{display:none}
|
|
12461
14124
|
.dsh-mobile-control__provider-section{position:relative;margin:0 0 14px}.dsh-mobile-control__section-title{margin:0 0 8px;color:var(--dsw-alias-label-primary,#16181d);font:650 13px/1.4 system-ui}.dsh-mobile-control__provider-section>.dsh-mobile-control__section-title{padding-right:42px}.dsh-mobile-control__provider-choices{display:grid;grid-template-columns:1fr;gap:8px}.dsh-mobile-control__provider{display:flex;min-width:0;flex-direction:column;gap:6px;min-height:94px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);text-align:left;cursor:pointer;touch-action:manipulation;transition:border-color 160ms ease,background-color 160ms ease,box-shadow 160ms ease}.dsh-mobile-control__provider:hover{border-color:#6f96db;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-1,#f8fbff))}.dsh-mobile-control__provider.is-selected{border-color:var(--dsw-alias-label-primary-bluish,#2563eb);background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-bg-layer-1,#f5f8ff));box-shadow:0 0 0 1px var(--dsw-alias-label-primary-bluish,#2563eb) inset}.dsh-mobile-control__provider:disabled{cursor:wait;opacity:.62}.dsh-mobile-control__provider-top{display:flex;min-width:0;align-items:flex-start;justify-content:space-between;gap:5px}.dsh-mobile-control__provider-top strong{min-width:0;font-size:12px;line-height:1.3}.dsh-mobile-control__provider-badge{flex:none;padding:2px 5px;border-radius:999px;background:var(--dsw-alias-state-business-tertiary,#e8f0ff);color:var(--dsw-alias-label-primary,#1d4ed8);font:650 11px/1.25 system-ui}.dsh-mobile-control__provider-badge.is-cpolar{background:var(--dsw-alias-state-success-tertiary,#eaf8f2);color:var(--dsw-alias-label-primary,#087454)}.dsh-mobile-control__provider-description{color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.45}.dsh-mobile-control__provider-info{position:absolute;z-index:5;top:-13px;right:-8px}.dsh-mobile-control__provider-info-button{display:flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary,#475569);cursor:pointer;touch-action:manipulation}.dsh-mobile-control__provider-info-button:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f5f9);color:var(--dsw-alias-label-primary-bluish,#2563eb)}.dsh-mobile-control__provider-info-glyph{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:18px;height:18px;border:1.5px solid currentColor;border-radius:50%;font:700 12px/1 system-ui}.dsh-mobile-control__provider-info-popover{position:absolute;z-index:6;top:38px;right:4px;box-sizing:border-box;width:min(292px,calc(100vw - 72px));padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 10px 28px rgb(15 23 42 / 16%)}.dsh-mobile-control__provider-info-popover[hidden]{display:none}.dsh-mobile-control__provider-info-popover strong,.dsh-mobile-control__provider-info-popover span{display:block}.dsh-mobile-control__provider-info-popover strong{margin-bottom:3px;font-size:12px}.dsh-mobile-control__provider-info-popover span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}
|
|
12462
14125
|
.dsh-mobile-control__self-hosted{margin:8px 0 0;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__self-hosted-summary{display:flex;box-sizing:border-box;min-height:48px;align-items:center;justify-content:space-between;gap:10px;padding:8px 11px;cursor:pointer;list-style-position:inside}.dsh-mobile-control__self-hosted-summary>span:first-child{display:flex;min-width:0;flex-direction:column;gap:1px}.dsh-mobile-control__self-hosted-summary strong{font-size:11px}.dsh-mobile-control__self-hosted-summary span span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.35}.dsh-mobile-control__provider-badge.is-frp{background:var(--dsw-alias-bg-layer-1,#eef0f3);color:var(--dsw-alias-label-secondary,#475569)}.dsh-mobile-control__self-hosted-body{padding:0 8px 8px}.dsh-mobile-control__provider.is-frp{width:100%;min-height:64px;background:var(--dsw-alias-bg-layer-2,#fff)}
|
|
14126
|
+
.dsh-mobile-control__frp-setup input[aria-invalid=true],.dsh-mobile-control__frp-setup select[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary,#bc3030)}
|
|
14127
|
+
.dsh-mobile-control__frp-setup .dsh-mobile-control__field>.dsh-mobile-control__origin-feedback{margin:0}
|
|
12463
14128
|
/* The cloudflared setup section deliberately reuses this card, so
|
|
12464
14129
|
.dsh-mobile-control__cloudflared-setup is a DOM hook for targeting rather than a
|
|
12465
14130
|
style rule of its own. */
|
|
@@ -12495,10 +14160,16 @@ window.__ModuleLoader__.load({
|
|
|
12495
14160
|
` + EXTENSION_RECOVERY_STYLES;
|
|
12496
14161
|
/** Mount the desktop control or mobile feature enhancements. */
|
|
12497
14162
|
function apply(ctx) {
|
|
14163
|
+
ctx.effect(() => installMobileUiLocalePersistence(document, window), "dsh-mobile: browser UI locale persistence");
|
|
12498
14164
|
ctx.effect(() => {
|
|
12499
14165
|
if (window.__DSH_MOBILE_FRONTEND__ !== "dedicated") return;
|
|
12500
|
-
|
|
14166
|
+
const connection = ctx.get("connection");
|
|
14167
|
+
if (connection !== void 0) return trustAuthenticatedGatewayConnection(connection);
|
|
12501
14168
|
}, "dsh-mobile: authenticated gateway client trust");
|
|
14169
|
+
ctx.effect(() => {
|
|
14170
|
+
if (!ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__)) return;
|
|
14171
|
+
return installMobileConnectionRecovery(() => ctx.get("connection"), window, document);
|
|
14172
|
+
}, "dsh-mobile: connection recovery");
|
|
12502
14173
|
ctx.effect(() => {
|
|
12503
14174
|
const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
|
|
12504
14175
|
const gatewayCustomization = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
|
|
@@ -12509,6 +14180,7 @@ window.__ModuleLoader__.load({
|
|
|
12509
14180
|
document.head.append(style);
|
|
12510
14181
|
const removeCustom = !desktopAdmin || gatewayCustomization ? installCustomAssets() : () => void 0;
|
|
12511
14182
|
const removeVoiceSession = installVoiceSession(!desktopAdmin);
|
|
14183
|
+
const removeDraftTransfer = !desktopAdmin && document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null ? installDraftTransfer(ctx, window, document) : () => void 0;
|
|
12512
14184
|
const removeModuleSettings = ctx.slots.inject("settings.general.item", () => ctx.slots.register({
|
|
12513
14185
|
name: "settings.general.item",
|
|
12514
14186
|
id: "dsh-mobile-client-modules",
|
|
@@ -12555,15 +14227,22 @@ window.__ModuleLoader__.load({
|
|
|
12555
14227
|
getLayout: () => ctx.get("layout"),
|
|
12556
14228
|
getSidebarRight: () => ctx.get("sidebarRight")
|
|
12557
14229
|
}));
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
14230
|
+
const removeDirectoryPicker = window.__DSH_MOBILE_FRONTEND__ === "dedicated" ? installMobileDirectoryPicker(ctx, document) : () => Promise.resolve();
|
|
14231
|
+
return async () => {
|
|
14232
|
+
const removal = removeDirectoryPicker();
|
|
14233
|
+
try {
|
|
14234
|
+
disposeClientModuleDialogs();
|
|
14235
|
+
removeModuleSettings();
|
|
14236
|
+
removeSettingsAction();
|
|
14237
|
+
removeCustom();
|
|
14238
|
+
removeSurface();
|
|
14239
|
+
removeVoiceSession();
|
|
14240
|
+
removeDraftTransfer();
|
|
14241
|
+
localFont.dispose();
|
|
14242
|
+
style.remove();
|
|
14243
|
+
} finally {
|
|
14244
|
+
await removal;
|
|
14245
|
+
}
|
|
12567
14246
|
};
|
|
12568
14247
|
}
|
|
12569
14248
|
const removeControl = installDshLanguageBoundSurface(() => {
|
|
@@ -12612,6 +14291,7 @@ window.__ModuleLoader__.load({
|
|
|
12612
14291
|
removeControl();
|
|
12613
14292
|
removeCustom();
|
|
12614
14293
|
removeVoiceSession();
|
|
14294
|
+
removeDraftTransfer();
|
|
12615
14295
|
style.remove();
|
|
12616
14296
|
};
|
|
12617
14297
|
}, "dsh-mobile: stock mobile adaptation and local control");
|
|
@@ -12660,6 +14340,8 @@ window.__ModuleLoader__.load({
|
|
|
12660
14340
|
exports.selectMobileControlLocale = selectMobileControlLocale;
|
|
12661
14341
|
exports.selectedMobileControlLocale = selectedMobileControlLocale;
|
|
12662
14342
|
exports.setMobileSurfaceOpen = setMobileSurfaceOpen;
|
|
14343
|
+
exports.shouldNudgeReconnect = shouldNudgeReconnect;
|
|
14344
|
+
exports.shouldRenewSession = shouldRenewSession;
|
|
12663
14345
|
exports.startExtensionChangeStream = startExtensionChangeStream;
|
|
12664
14346
|
exports.startLifecycleRefreshScheduler = startLifecycleRefreshScheduler;
|
|
12665
14347
|
exports.trustAuthenticatedGatewayConnection = trustAuthenticatedGatewayConnection;
|