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/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 = "camera";
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
- icon.append(mediaIcon());
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
- if (nativeItem !== void 0) {
4563
- cameraButton.className = nativeItem.className;
4564
- const cameraIcon = cameraButton.firstElementChild;
4565
- const cameraLabel = cameraButton.lastElementChild;
4566
- const nativeIcon = firstByClassSuffix(nativeItem, "_itemIcon");
4567
- const nativeLabel = firstByClassSuffix(nativeItem, "_itemName");
4568
- if (cameraIcon instanceof HTMLElement && nativeIcon !== void 0) cameraIcon.className = nativeIcon.className;
4569
- if (cameraLabel instanceof HTMLElement && nativeLabel !== void 0) cameraLabel.className = nativeLabel.className;
4570
- }
4571
- cameraButton.setAttribute("role", "option");
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 (cameraButton.parentElement !== sectionTitle.parentElement || cameraButton.previousElementSibling !== lastItem) lastItem.after(cameraButton);
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 cameraButton.remove();
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.entries = entries;
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(body !== void 0));
7101
+ rejectCancellation(new ClientModuleRequestTimeout(mutation));
6411
7102
  controller.abort();
6412
- }, body === void 0 ? CLIENT_MODULE_REQUEST_TIMEOUT_MS.read : CLIENT_MODULE_REQUEST_TIMEOUT_MS.write);
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
- header.append(title, close);
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
- changes.textContent = `${changed === 0 ? copy.unchanged : copy.changes.replace("{count}", String(changed))} · ${copy.nextOpen}`;
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
- name.append(shortName, selected);
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, paragraph(entry.dependencies.length === 0 ? copy.noDependencies : `${copy.dependencies}${entry.dependencies.join(", ")}`));
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 = payload === void 0 ? "read" : "write";
6753
- if (payload !== void 0) unconfirmedWrite = true;
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 = payload === void 0 ? copy.loading : copy.saving;
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
- unconfirmedWrite = false;
6760
- render(view);
6761
- status.textContent = payload === void 0 ? "" : copy.saved;
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, payload !== void 0);
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
- frpStep4.append(frpStep4Title, frpStep4Text, frpAppRequirement, frpConfigurationStatus, frpConfigure);
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
- ...Number.isSafeInteger(vhostHttpPort) && vhostHttpPort >= 1 ? { vhostHttpPort } : {},
10113
- ...Number.isSafeInteger(publicPort) && publicPort >= 1 ? { publicPort } : {}
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
- if (!validFrpServer(form.serverAddress)) return false;
10131
- try {
10132
- createFrpServerTemplateForClipboard(form.serverPort, form.token, form.publicOrigin);
10133
- return true;
10134
- } catch {
10135
- return false;
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 (!validFrpForm(form)) {
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
- if (code === "frp_attach_mode_requires_vhost_port") {
10154
- frpVhostPort.setAttribute("aria-invalid", "true");
10155
- frpVhostHint.textContent = message;
10156
- frpVhostPort.focus();
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 (!validFrpServer(form.serverAddress) || !Number.isSafeInteger(form.serverPort) || form.serverPort < 1 || form.serverPort > 65535 || form.publicOrigin === "") {
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
- remoteStatus.textContent = frpAttachErrorText(String(error));
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 (!validFrpServer(form.serverAddress) || !Number.isSafeInteger(form.serverPort) || form.serverPort < 1 || form.serverPort > 65535 || form.publicOrigin === "") {
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
- const code = form.mode === "attach" ? frpAttachFormErrorCode(attachClipboardForm(form)) : validFrpForm(form) ? void 0 : "frp_settings_invalid";
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
- remoteFailureText = t("configureFailed", { error: String(error) });
10589
- remoteStatus.textContent = remoteFailureText;
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
- return trustAuthenticatedGatewayConnection(ctx.get("connection"));
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
- return () => {
12559
- disposeClientModuleDialogs();
12560
- removeModuleSettings();
12561
- removeSettingsAction();
12562
- removeCustom();
12563
- removeSurface();
12564
- removeVoiceSession();
12565
- localFont.dispose();
12566
- style.remove();
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;