dsh-mobile 0.6.4 → 0.6.6

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
@@ -598,7 +598,12 @@ window.__ModuleLoader__.load({
598
598
  cpolarDownloadTimeout: "The cpolar download timed out. Check this computer’s network or proxy, then retry.",
599
599
  cpolarDownloadHttp: "The official cpolar download returned HTTP {status}. Retry later or check the official download page.",
600
600
  cpolarIntegrityFailed: "The cpolar download did not match the pinned build, so it was not installed. Retry later or update DSH Mobile; do not bypass verification.",
601
- cpolarExtractFailed: "The cpolar download completed, but extraction failed. Check system extraction tools and security software, then retry.",
601
+ cpolarExtractFailed: "The cpolar download was verified, but extraction failed. Check the extraction stage in the DSH log; the message alone does not establish a security-software block.",
602
+ cpolarExtractToolsFailed: "A required Windows extraction tool is unavailable. Check system PowerShell, tar and expand; no administrator startup or service installation is required.",
603
+ cpolarExtractArchiveFailed: "Could not extract the verified cpolar ZIP or locate its MSI. Check the ZIP extraction stage in the DSH log.",
604
+ cpolarExtractDatabaseFailed: "Could not read the cpolar MSI database or its embedded cabinet. Check the MSI read stage in the DSH log; do not install the MSI as a workaround.",
605
+ cpolarExtractCabinetFailed: "Could not extract the cpolar executable from its cabinet. Check the cabinet extraction stage and private-directory permissions.",
606
+ cpolarExtractFileFailed: "Could not read or copy the extracted cpolar executable. Check private-directory permissions and disk space; your existing component is retained.",
602
607
  cpolarStorageFailed: "Could not write the cpolar component in DSH Mobile’s private directory. Check disk space, permissions, and security software, then retry."
603
608
  });
604
609
  Object.assign(MOBILE_CONTROL_MESSAGES.it, {
@@ -623,7 +628,12 @@ window.__ModuleLoader__.load({
623
628
  cpolarDownloadTimeout: "Download di cpolar scaduto. Controlla rete o proxy del computer e riprova.",
624
629
  cpolarDownloadHttp: "Il download ufficiale di cpolar ha restituito HTTP {status}. Riprova più tardi o controlla la pagina ufficiale.",
625
630
  cpolarIntegrityFailed: "Il pacchetto cpolar non corrisponde alla versione verificata e non è stato installato. Riprova più tardi o aggiorna DSH Mobile; non aggirare la verifica.",
626
- cpolarExtractFailed: "Download di cpolar completato, ma estrazione non riuscita. Controlla gli strumenti di estrazione e il software di sicurezza, poi riprova.",
631
+ cpolarExtractFailed: "Download di cpolar verificato, ma estrazione non riuscita. Controlla la fase nel log DSH; il messaggio non dimostra un blocco del software di sicurezza.",
632
+ cpolarExtractToolsFailed: "Uno strumento di estrazione Windows richiesto non è disponibile. Controlla PowerShell di sistema, tar ed expand; non serve avviare come amministratore o installare un servizio.",
633
+ cpolarExtractArchiveFailed: "Impossibile estrarre lo ZIP cpolar verificato o trovare il suo MSI. Controlla la fase ZIP nel log DSH.",
634
+ cpolarExtractDatabaseFailed: "Impossibile leggere il database MSI cpolar o il cabinet incorporato. Controlla la fase di lettura MSI nel log DSH; non installare il MSI per aggirare il problema.",
635
+ cpolarExtractCabinetFailed: "Impossibile estrarre il programma cpolar dal cabinet. Controlla la fase cabinet e i permessi della directory privata.",
636
+ cpolarExtractFileFailed: "Impossibile leggere o copiare il programma cpolar estratto. Controlla permessi e spazio nella directory privata; il componente esistente è conservato.",
627
637
  cpolarStorageFailed: "Impossibile scrivere cpolar nella directory privata di DSH Mobile. Controlla spazio, permessi e software di sicurezza, poi riprova."
628
638
  });
629
639
  Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
@@ -648,7 +658,12 @@ window.__ModuleLoader__.load({
648
658
  cpolarDownloadTimeout: "下载 cpolar 组件超时。请检查这台电脑的网络或代理,然后重试。",
649
659
  cpolarDownloadHttp: "cpolar 官网下载返回 HTTP {status}。请稍后重试,或检查官网下载页。",
650
660
  cpolarIntegrityFailed: "下载的 cpolar 组件未通过固定版本校验,因此没有安装。请稍后重试或更新 DSH Mobile,不要跳过校验。",
651
- cpolarExtractFailed: "cpolar 下载完成,但解包失败。请检查系统解压组件和安全软件,然后重试。",
661
+ cpolarExtractFailed: "cpolar 下载已校验,但提取失败。请查看 DSH 日志中的具体阶段,不能仅凭此提示认定安全软件拦截。",
662
+ cpolarExtractToolsFailed: "所需的 Windows 提取工具不可用。请检查系统 PowerShell、tar 和 expand;无需以管理员启动 DSH 或安装系统服务。",
663
+ cpolarExtractArchiveFailed: "无法展开已校验的 cpolar ZIP 或找到其中的 MSI。请查看 DSH 日志中的 ZIP 提取阶段。",
664
+ cpolarExtractDatabaseFailed: "无法读取 cpolar MSI 数据库或内嵌 CAB。请查看 DSH 日志中的 MSI 读取阶段,不要用运行 MSI 安装程序绕过此问题。",
665
+ cpolarExtractCabinetFailed: "无法从 CAB 中提取 cpolar 程序。请查看 CAB 提取阶段及私有目录权限。",
666
+ cpolarExtractFileFailed: "无法读取或复制已提取的 cpolar 程序。请检查私有目录权限和磁盘空间;已有组件不会被替换。",
652
667
  cpolarStorageFailed: "无法把 cpolar 写入 DSH Mobile 私有目录。请检查磁盘空间、目录权限和安全软件,然后重试。"
653
668
  });
654
669
  Object.assign(MOBILE_CONTROL_MESSAGES.en, {
@@ -3189,6 +3204,127 @@ window.__ModuleLoader__.load({
3189
3204
  [data-dsh-mobile-composer-model-label] { flex:1 1 auto !important; max-width:none !important; min-width:0 !important; overflow:hidden !important; text-overflow:ellipsis !important; white-space:nowrap !important; }
3190
3205
  }
3191
3206
  `;
3207
+ //#endregion
3208
+ //#region src/browser-file-picker.ts
3209
+ /** Browser chooser lifetime shared by camera controls and the extension SDK. */
3210
+ const BROWSER_FILE_PICKER_DEADLINE_MS = 3e5;
3211
+ const browserEnvironment = {
3212
+ createSource: () => {
3213
+ const input = document.createElement("input");
3214
+ return {
3215
+ input,
3216
+ open: () => {
3217
+ document.body.append(input);
3218
+ input.click();
3219
+ },
3220
+ remove: () => {
3221
+ input.remove();
3222
+ }
3223
+ };
3224
+ },
3225
+ deadline: (callback, milliseconds) => {
3226
+ const timer = window.setTimeout(callback, milliseconds);
3227
+ return () => {
3228
+ window.clearTimeout(timer);
3229
+ };
3230
+ }
3231
+ };
3232
+ /** Select files without treating focus/visibility restoration as cancellation.
3233
+ * @param options - MIME selection, lifetime and optional camera capture mode.
3234
+ * @param environment - Browser DOM and timer owner; tests may supply an owned source.
3235
+ * @returns Selected files, or undefined for explicit cancellation or deadline; abort rejects.
3236
+ */
3237
+ function pickBrowserFiles(options, environment = browserEnvironment) {
3238
+ const abortReason = () => options.abortReason?.() ?? options.signal.reason ?? new DOMException("File selection aborted", "AbortError");
3239
+ if (options.signal.aborted) return Promise.reject(abortReason());
3240
+ return new Promise((resolve, reject) => {
3241
+ let source;
3242
+ let input;
3243
+ let settled = false;
3244
+ let cancelDeadline;
3245
+ const cleanup = () => {
3246
+ const errors = [];
3247
+ const attempt = (operation) => {
3248
+ try {
3249
+ operation();
3250
+ } catch (error) {
3251
+ errors.push(error);
3252
+ }
3253
+ };
3254
+ if (cancelDeadline !== void 0) attempt(cancelDeadline);
3255
+ cancelDeadline = void 0;
3256
+ attempt(() => {
3257
+ input?.removeEventListener("change", onChange);
3258
+ });
3259
+ attempt(() => {
3260
+ input?.removeEventListener("cancel", onCancel);
3261
+ });
3262
+ attempt(() => {
3263
+ options.signal.removeEventListener("abort", onAbort);
3264
+ });
3265
+ attempt(() => {
3266
+ source?.remove();
3267
+ });
3268
+ return errors;
3269
+ };
3270
+ const finish = (outcome) => {
3271
+ if (settled) return;
3272
+ settled = true;
3273
+ const cleanupErrors = cleanup();
3274
+ if (outcome.kind === "failed") reject(outcome.error);
3275
+ else if (cleanupErrors.length > 0) reject(cleanupErrors[0]);
3276
+ else if (outcome.kind === "cancelled") resolve(void 0);
3277
+ else resolve(outcome.files);
3278
+ };
3279
+ const onChange = () => {
3280
+ if (settled || input === void 0) return;
3281
+ try {
3282
+ const files = input.files === null ? [] : [...input.files];
3283
+ finish(files.length === 0 ? { kind: "cancelled" } : {
3284
+ kind: "files",
3285
+ files: Object.freeze(files)
3286
+ });
3287
+ } catch (error) {
3288
+ finish({
3289
+ kind: "failed",
3290
+ error
3291
+ });
3292
+ }
3293
+ };
3294
+ const onCancel = () => {
3295
+ finish({ kind: "cancelled" });
3296
+ };
3297
+ const onAbort = () => {
3298
+ finish({
3299
+ kind: "failed",
3300
+ error: abortReason()
3301
+ });
3302
+ };
3303
+ try {
3304
+ source = environment.createSource();
3305
+ input = source.input;
3306
+ input.type = "file";
3307
+ input.accept = options.accept;
3308
+ if (options.capture !== void 0) input.capture = options.capture;
3309
+ input.multiple = options.multiple ?? false;
3310
+ input.hidden = true;
3311
+ input.addEventListener("change", onChange);
3312
+ input.addEventListener("cancel", onCancel);
3313
+ options.signal.addEventListener("abort", onAbort, { once: true });
3314
+ cancelDeadline = environment.deadline(onCancel, BROWSER_FILE_PICKER_DEADLINE_MS);
3315
+ if (options.signal.aborted) {
3316
+ onAbort();
3317
+ return;
3318
+ }
3319
+ source.open();
3320
+ } catch (error) {
3321
+ finish({
3322
+ kind: "failed",
3323
+ error
3324
+ });
3325
+ }
3326
+ });
3327
+ }
3192
3328
  //#endregion
3193
3329
  //#region src/native-mobile.ts
3194
3330
  /**
@@ -3236,6 +3372,8 @@ window.__ModuleLoader__.load({
3236
3372
  .dsh-mobile-settings_fontFamily > span { flex:1 1 100px; }
3237
3373
  .dsh-mobile-settings_fontFamily input { box-sizing:border-box; flex:1 1 180px; width:100%; min-width:0; min-height:48px; padding:8px 12px; border:0.5px solid var(--dsw-alias-border-l2); border-radius:10px; background:var(--dsw-alias-bg-module-platform); color:var(--dsw-alias-label-primary); font:inherit; }
3238
3374
  .dsh-mobile-settings_fontFamily input:focus-visible,.dsh-mobile-settings_fontDetails > summary:focus-visible { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
3375
+ html.dsh-native-mobile-active [data-dsh-mobile-settings-header] { box-sizing:border-box !important; height:56px !important; min-height:56px !important; padding:4px 12px !important; }
3376
+ html.dsh-native-mobile-active [data-dsh-mobile-settings-header] [class*="_close"] { box-sizing:border-box !important; flex:none !important; width:48px !important; min-width:48px !important; height:48px !important; min-height:48px !important; }
3239
3377
  /* The mobile row replaces the Host-backed font control, not the theme selector. */
3240
3378
  html.dsh-native-mobile-active [data-slot="settings.general.item"]:has([data-mobile-font-setting]) > :not([data-mobile-font-setting])[class*="_row"]:has([class*="_control"] > [class*="_stepper"] > [class*="_arrows"]) { display:none !important; }
3241
3379
  @media ${NATIVE_MOBILE_OVERLAY_QUERY} {
@@ -3305,8 +3443,6 @@ window.__ModuleLoader__.load({
3305
3443
  [data-dsh-mobile-settings-list] [class*="_navCell"] { flex:0 0 auto !important; min-width:max-content !important; height:44px !important; padding:10px 12px !important; }
3306
3444
  [data-dsh-mobile-settings-list] [aria-current="true"] { border-color:transparent !important; outline:0 !important; box-shadow:none !important; }
3307
3445
  [data-dsh-mobile-settings-content] { flex:1 1 auto !important; width:100% !important; min-height:0 !important; }
3308
- [data-dsh-mobile-settings-header] { height:48px !important; min-height:48px !important; padding:10px 12px 6px !important; }
3309
- [data-dsh-mobile-settings-header] [class*="_close"] { width:36px !important; height:36px !important; }
3310
3446
  [data-dsh-mobile-settings-options] { box-sizing:border-box !important; width:100% !important; padding:4px 16px max(24px,env(safe-area-inset-bottom)) !important; overflow-x:hidden !important; }
3311
3447
  [data-dsh-mobile-settings-options] > * { width:100% !important; min-width:0 !important; }
3312
3448
  [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] > [class*="_row"] { flex-direction:column !important; align-items:stretch !important; gap:12px !important; }
@@ -3609,19 +3745,66 @@ window.__ModuleLoader__.load({
3609
3745
  }
3610
3746
  };
3611
3747
  }
3612
- function controlledFileDragEvent(type, files, initialDropEffect) {
3613
- const event = new Event(type, {
3614
- bubbles: true,
3615
- cancelable: true
3748
+ /** A read-only, file-only transfer for engines without usable native constructors. */
3749
+ function controlledFileTransfer(files, initialDropEffect) {
3750
+ const snapshot = [...files];
3751
+ const fileList = Object.assign(snapshot, { item: (index) => snapshot[index] ?? null });
3752
+ const items = Object.assign(snapshot.map((file) => ({
3753
+ kind: "file",
3754
+ type: file.type,
3755
+ getAsFile: () => file,
3756
+ getAsString: () => {},
3757
+ webkitGetAsEntry: () => null
3758
+ })), {
3759
+ add: () => null,
3760
+ clear: () => {},
3761
+ remove: () => {}
3616
3762
  });
3617
- const dataTransfer = {
3763
+ Object.freeze(fileList);
3764
+ Object.freeze(items);
3765
+ return {
3618
3766
  dropEffect: initialDropEffect,
3619
3767
  effectAllowed: "copy",
3620
- files: Object.freeze([...files]),
3621
- types: Object.freeze(["Files"])
3622
- };
3623
- Object.defineProperty(event, "dataTransfer", { value: dataTransfer });
3624
- return event;
3768
+ files: fileList,
3769
+ items,
3770
+ types: Object.freeze(["Files"]),
3771
+ clearData: () => {},
3772
+ getData: () => "",
3773
+ setData: () => {},
3774
+ setDragImage: () => {}
3775
+ };
3776
+ }
3777
+ function nativeFileTransfer(files, initialDropEffect) {
3778
+ const transfer = new DataTransfer();
3779
+ for (const file of files) if (transfer.items.add(file) === null) throw new Error("file_transfer_unavailable");
3780
+ if (transfer.files.length !== files.length) throw new Error("file_transfer_unavailable");
3781
+ transfer.effectAllowed = "copy";
3782
+ transfer.dropEffect = "copy";
3783
+ if (transfer.effectAllowed !== "copy" || transfer.dropEffect !== "copy") throw new Error("file_transfer_unavailable");
3784
+ transfer.dropEffect = initialDropEffect;
3785
+ if (transfer.dropEffect !== initialDropEffect) throw new Error("file_transfer_unavailable");
3786
+ return transfer;
3787
+ }
3788
+ function controlledFileDragEvent(type, files, initialDropEffect) {
3789
+ let transfer;
3790
+ if (typeof DataTransfer === "function") try {
3791
+ transfer = nativeFileTransfer(files, initialDropEffect);
3792
+ } catch (_error) {
3793
+ transfer = controlledFileTransfer(files, initialDropEffect);
3794
+ }
3795
+ else transfer = controlledFileTransfer(files, initialDropEffect);
3796
+ if (typeof DragEvent === "function") try {
3797
+ const event = new DragEvent(type, {
3798
+ bubbles: true,
3799
+ cancelable: true,
3800
+ dataTransfer: transfer
3801
+ });
3802
+ if (event.dataTransfer !== null) return event;
3803
+ } catch (_error) {}
3804
+ return Object.assign(new Event(type, {
3805
+ bubbles: true,
3806
+ cancelable: true
3807
+ }), { dataTransfer: transfer });
3625
3808
  }
3626
3809
  /** Ask the official document dragover listener whether the current composer accepts files. */
3627
3810
  function preflightComposerImageDrop(target, files) {
@@ -4071,7 +4254,6 @@ window.__ModuleLoader__.load({
4071
4254
  const preflightFile = new File([], "dsh-mobile-preflight.png", { type: "image/png" });
4072
4255
  const canAcceptComposerDrop = () => preflightComposerImageDrop(document, [preflightFile]);
4073
4256
  const mediaPickerAbortController = new AbortController();
4074
- const browserPickerCleanups = /* @__PURE__ */ new Set();
4075
4257
  let boundEnterEditor = null;
4076
4258
  let boundEnterCard = null;
4077
4259
  let disposeEnterBinding;
@@ -4118,63 +4300,18 @@ window.__ModuleLoader__.load({
4118
4300
  };
4119
4301
  const deliverImages = (files, context) => {
4120
4302
  if (files.length === 0 || !mediaRequestIsCurrent(context)) return;
4121
- dispatchComposerImageDrop(document, files);
4303
+ if (!dispatchComposerImageDrop(document, files)) showMediaToast(label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
4122
4304
  };
4123
4305
  const launchBrowserCamera = (context) => {
4124
- const input = document.createElement("input");
4125
- input.type = "file";
4126
- input.accept = "image/*";
4127
- input.capture = "environment";
4128
- input.hidden = true;
4129
- const signal = mediaPickerAbortController.signal;
4130
- let cleanupTimer = 0;
4131
- let watchdogTimer = 0;
4132
- let cleaned = false;
4133
- const scheduleCleanup = () => {
4134
- if (!cleaned && cleanupTimer === 0) cleanupTimer = window.setTimeout(cleanup, 1e3);
4135
- };
4136
- const onVisibilityChange = () => {
4137
- if (document.visibilityState === "visible") scheduleCleanup();
4138
- };
4139
- const onChange = () => {
4140
- if (cleaned) return;
4141
- const files = input.files === null ? [] : [...input.files];
4142
- cleanup();
4143
- deliverImages(files, context);
4144
- };
4145
- const cleanup = () => {
4146
- if (cleaned) return;
4147
- cleaned = true;
4148
- if (cleanupTimer !== 0) window.clearTimeout(cleanupTimer);
4149
- if (watchdogTimer !== 0) window.clearTimeout(watchdogTimer);
4150
- cleanupTimer = 0;
4151
- watchdogTimer = 0;
4152
- input.removeEventListener("change", onChange);
4153
- input.removeEventListener("cancel", cleanup);
4154
- window.removeEventListener("focus", scheduleCleanup);
4155
- document.removeEventListener("visibilitychange", onVisibilityChange);
4156
- signal.removeEventListener("abort", cleanup);
4157
- browserPickerCleanups.delete(cleanup);
4158
- input.remove();
4159
- };
4160
- input.addEventListener("change", onChange);
4161
- input.addEventListener("cancel", cleanup);
4162
- window.addEventListener("focus", scheduleCleanup);
4163
- document.addEventListener("visibilitychange", onVisibilityChange);
4164
- signal.addEventListener("abort", cleanup, { once: true });
4165
- watchdogTimer = window.setTimeout(cleanup, 3e5);
4166
- browserPickerCleanups.add(cleanup);
4167
- if (signal.aborted) {
4168
- cleanup();
4169
- return;
4170
- }
4171
- try {
4172
- document.body.append(input);
4173
- input.click();
4174
- } catch {
4175
- cleanup();
4176
- if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aprire la fotocamera", "Could not open the camera", "无法打开相机"));
4177
- }
4306
+ pickBrowserFiles({
4307
+ accept: "image/*",
4308
+ capture: "environment",
4309
+ signal: mediaPickerAbortController.signal
4310
+ }).then((files) => {
4311
+ if (files !== void 0) deliverImages(files, context);
4312
+ }).catch((_error) => {
4313
+ if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aggiungere la foto", "Could not attach the photo", "无法附加照片"));
4314
+ });
4178
4315
  };
4179
4316
  const dismissCommandMenu = () => {
4180
4317
  (boundComposer?.querySelector("button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]"))?.click();
@@ -4632,7 +4769,6 @@ window.__ModuleLoader__.load({
4632
4769
  mediaRequestGeneration += 1;
4633
4770
  mediaPickerAbortController.abort();
4634
4771
  restoreLanguageMarker();
4635
- for (const cleanup of [...browserPickerCleanups]) cleanup();
4636
4772
  disposeEnterBinding?.();
4637
4773
  disposeBrowserComposerSoftEnter();
4638
4774
  observer.disconnect();
@@ -4778,6 +4914,63 @@ window.__ModuleLoader__.load({
4778
4914
  };
4779
4915
  }
4780
4916
  //#endregion
4917
+ //#region src/mobile-font-capabilities.ts
4918
+ const NO_ADVANCED_MOBILE_TYPOGRAPHY = Object.freeze({
4919
+ codeSize: false,
4920
+ textFamily: false,
4921
+ codeFamily: false
4922
+ });
4923
+ /**
4924
+ * Probe computed fonts derived from DSH's public Markdown tokens. Input values are
4925
+ * restored synchronously before painting; merely defining the input variable is not support.
4926
+ * @param document - Live page document in the current browsing context.
4927
+ * @returns Independently observed advanced typography support.
4928
+ */
4929
+ function readMobileTypographyCapabilities(document) {
4930
+ const style = document.body.style;
4931
+ const text = document.createElement("span");
4932
+ const code = document.createElement("span");
4933
+ for (const probe of [text, code]) {
4934
+ probe.setAttribute("aria-hidden", "true");
4935
+ probe.style.cssText = "position:fixed;visibility:hidden;pointer-events:none;contain:strict;width:0;height:0;overflow:hidden";
4936
+ }
4937
+ text.style.font = "var(--dsw-font-markdown-base, 14px sans-serif)";
4938
+ code.style.font = "var(--dsw-font-markdown-code-block, 11px monospace)";
4939
+ document.body.append(text, code);
4940
+ const previous = /* @__PURE__ */ new Map();
4941
+ const sample = (property, value) => {
4942
+ if (!previous.has(property)) previous.set(property, {
4943
+ value: style.getPropertyValue(property),
4944
+ priority: style.getPropertyPriority(property)
4945
+ });
4946
+ style.setProperty(property, value);
4947
+ };
4948
+ try {
4949
+ sample("--dsh-code-font-size", "23px");
4950
+ const firstSize = getComputedStyle(code).fontSize;
4951
+ sample("--dsh-code-font-size", "24px");
4952
+ const secondSize = getComputedStyle(code).fontSize;
4953
+ sample("--dsh-font-family-text", "\"dsh-mobile-text-capability-a\"");
4954
+ const firstTextFamily = getComputedStyle(text).fontFamily;
4955
+ sample("--dsh-font-family-text", "\"dsh-mobile-text-capability-b\"");
4956
+ const secondTextFamily = getComputedStyle(text).fontFamily;
4957
+ sample("--dsh-font-family-code", "\"dsh-mobile-code-capability-a\"");
4958
+ const firstCodeFamily = getComputedStyle(code).fontFamily;
4959
+ sample("--dsh-font-family-code", "\"dsh-mobile-code-capability-b\"");
4960
+ const secondCodeFamily = getComputedStyle(code).fontFamily;
4961
+ return Object.freeze({
4962
+ codeSize: firstSize === "23px" && secondSize === "24px",
4963
+ textFamily: firstTextFamily.includes("dsh-mobile-text-capability-a") && secondTextFamily.includes("dsh-mobile-text-capability-b"),
4964
+ codeFamily: firstCodeFamily.includes("dsh-mobile-code-capability-a") && secondCodeFamily.includes("dsh-mobile-code-capability-b")
4965
+ });
4966
+ } finally {
4967
+ for (const [property, value] of previous) if (value.value === "") style.removeProperty(property);
4968
+ else style.setProperty(property, value.value, value.priority);
4969
+ text.remove();
4970
+ code.remove();
4971
+ }
4972
+ }
4973
+ //#endregion
4781
4974
  //#region src/mobile-font.ts
4782
4975
  /** Device-local typography for authenticated mobile pages, without Host settings writes. */
4783
4976
  const MOBILE_FONT_STORAGE_KEY = "dsh-mobile.content-font-size.v1";
@@ -4878,6 +5071,8 @@ window.__ModuleLoader__.load({
4878
5071
  storage;
4879
5072
  typography = defaultTypography();
4880
5073
  listeners = /* @__PURE__ */ new Set();
5074
+ capabilities = NO_ADVANCED_MOBILE_TYPOGRAPHY;
5075
+ capabilityListeners = /* @__PURE__ */ new Set();
4881
5076
  constructor(storage) {
4882
5077
  this.storage = storage;
4883
5078
  let legacySize = 16;
@@ -4897,6 +5092,19 @@ window.__ModuleLoader__.load({
4897
5092
  this.listeners.delete(listener);
4898
5093
  };
4899
5094
  };
5095
+ getCapabilitiesSnapshot = () => this.capabilities;
5096
+ subscribeCapabilities = (listener) => {
5097
+ this.capabilityListeners.add(listener);
5098
+ return () => {
5099
+ this.capabilityListeners.delete(listener);
5100
+ };
5101
+ };
5102
+ /** Update observed theme support without changing stored values or Host settings. */
5103
+ updateCapabilities(capabilities) {
5104
+ if (this.capabilities.codeSize === capabilities.codeSize && this.capabilities.textFamily === capabilities.textFamily && this.capabilities.codeFamily === capabilities.codeFamily) return;
5105
+ this.capabilities = Object.freeze({ ...capabilities });
5106
+ for (const listener of this.capabilityListeners) listener();
5107
+ }
4900
5108
  /** Apply a user-selected size locally; never send it to DSH's settings RPC. */
4901
5109
  set(size) {
4902
5110
  this.setRoleSize("text", size);
@@ -4931,6 +5139,23 @@ window.__ModuleLoader__.load({
4931
5139
  if (MOBILE_FONT_ROLES.every((role) => this.typography.sizes[role] === next.sizes[role] && this.typography.families[role] === "")) return;
4932
5140
  this.persist(next);
4933
5141
  }
5142
+ /** Reset visible supported controls while retaining preferences an older theme cannot consume. */
5143
+ resetSupported() {
5144
+ const defaults = defaultTypography();
5145
+ const sizes = {
5146
+ text: defaults.sizes.text,
5147
+ code: this.capabilities.codeSize ? defaults.sizes.code : this.typography.sizes.code
5148
+ };
5149
+ const families = {
5150
+ text: this.capabilities.textFamily ? "" : this.typography.families.text,
5151
+ code: this.capabilities.codeFamily ? "" : this.typography.families.code
5152
+ };
5153
+ if (MOBILE_FONT_ROLES.every((role) => this.typography.sizes[role] === sizes[role] && this.typography.families[role] === families[role])) return;
5154
+ this.persist({
5155
+ sizes: Object.freeze(sizes),
5156
+ families: Object.freeze(families)
5157
+ });
5158
+ }
4934
5159
  persist(typography) {
4935
5160
  this.typography = Object.freeze(typography);
4936
5161
  try {
@@ -4942,7 +5167,11 @@ window.__ModuleLoader__.load({
4942
5167
  for (const listener of this.listeners) listener();
4943
5168
  }
4944
5169
  };
4945
- /** Keep mobile typography independent from Host theme changes, including body-level portals. */
5170
+ /**
5171
+ * Keep mobile typography independent from Host theme changes, including body-level portals.
5172
+ * Recheck head-owned stylesheets and body presentation attributes, not conversation DOM updates.
5173
+ * Arbitrary stylesheets inserted into body are not observed.
5174
+ */
4946
5175
  function installMobileFontPreference() {
4947
5176
  let storage = null;
4948
5177
  try {
@@ -4958,11 +5187,26 @@ window.__ModuleLoader__.load({
4958
5187
  const owned = /* @__PURE__ */ new Map();
4959
5188
  const apply = () => {
4960
5189
  const typography = preference.getTypographySnapshot();
5190
+ const capabilities = preference.getCapabilitiesSnapshot();
4961
5191
  for (const role of MOBILE_FONT_ROLES) for (const [property, value] of [[roleSpecs[role].variable, `${typography.sizes[role]}px`], [`--dsh-font-family-${role}`, typography.families[role]]]) {
4962
5192
  const current = style.getPropertyValue(property);
4963
5193
  const priority = style.getPropertyPriority(property);
4964
5194
  const last = owned.get(property);
4965
- if (last !== void 0 && (current !== last.value || priority !== last.priority)) previous.set(property, {
5195
+ if (!(property === roleSpecs[role].variable ? role === "text" || capabilities.codeSize : role === "text" ? capabilities.textFamily : capabilities.codeFamily)) {
5196
+ if (last !== void 0) {
5197
+ if (current === last.value && priority === last.priority) {
5198
+ const foreign = previous.get(property);
5199
+ if (foreign.value === "") style.removeProperty(property);
5200
+ else style.setProperty(property, foreign.value, foreign.priority);
5201
+ } else previous.set(property, {
5202
+ value: current,
5203
+ priority
5204
+ });
5205
+ owned.delete(property);
5206
+ }
5207
+ continue;
5208
+ }
5209
+ if (last === void 0 || current !== last.value || priority !== last.priority) previous.set(property, {
4966
5210
  value: current,
4967
5211
  priority
4968
5212
  });
@@ -4975,17 +5219,55 @@ window.__ModuleLoader__.load({
4975
5219
  else style.setProperty(property, value);
4976
5220
  }
4977
5221
  };
4978
- apply();
4979
- const unsubscribe = preference.subscribe(apply);
4980
- const observer = new MutationObserver(apply);
5222
+ let lastBodyState = "";
5223
+ const bodyState = () => `${style.cssText}|${document.body.className}|${document.body.getAttribute("data-ds-dark-theme")}`;
5224
+ const applyAndRemember = () => {
5225
+ apply();
5226
+ lastBodyState = bodyState();
5227
+ };
5228
+ const refresh = () => {
5229
+ preference.updateCapabilities(readMobileTypographyCapabilities(document));
5230
+ applyAndRemember();
5231
+ };
5232
+ refresh();
5233
+ const unsubscribe = preference.subscribe(applyAndRemember);
5234
+ const observer = new MutationObserver(() => {
5235
+ if (bodyState() !== lastBodyState) refresh();
5236
+ });
4981
5237
  observer.observe(document.body, {
4982
5238
  attributes: true,
4983
- attributeFilter: ["style"]
5239
+ attributeFilter: [
5240
+ "style",
5241
+ "class",
5242
+ "data-ds-dark-theme"
5243
+ ]
5244
+ });
5245
+ const themeObserver = new MutationObserver((records) => {
5246
+ const isSheet = (node) => node instanceof Element && (node.matches("style,link[rel=\"stylesheet\"]") || node.querySelector("style,link[rel=\"stylesheet\"]") !== null);
5247
+ if (records.some((record) => record.target instanceof Element && record.target.closest("style,link[rel=\"stylesheet\"]") !== null || [...record.addedNodes, ...record.removedNodes].some(isSheet))) refresh();
5248
+ });
5249
+ themeObserver.observe(document.head, {
5250
+ subtree: true,
5251
+ childList: true,
5252
+ characterData: true,
5253
+ attributes: true,
5254
+ attributeFilter: [
5255
+ "href",
5256
+ "media",
5257
+ "disabled",
5258
+ "rel"
5259
+ ]
4984
5260
  });
5261
+ const sheetLoaded = (event) => {
5262
+ if (event.target instanceof Element && event.target.matches("link[rel=\"stylesheet\"]")) refresh();
5263
+ };
5264
+ document.head.addEventListener("load", sheetLoaded, true);
4985
5265
  return {
4986
5266
  preference,
4987
5267
  dispose: () => {
4988
5268
  observer.disconnect();
5269
+ themeObserver.disconnect();
5270
+ document.head.removeEventListener("load", sheetLoaded, true);
4989
5271
  unsubscribe();
4990
5272
  for (const [property, value] of previous) {
4991
5273
  const last = owned.get(property);
@@ -5008,7 +5290,8 @@ window.__ModuleLoader__.load({
5008
5290
  familyHint: "填写设备已有的字体,多个名称用逗号分隔;留空使用默认字体。",
5009
5291
  codeSize: "代码字号",
5010
5292
  textFamily: "正文字体",
5011
- codeFamily: "代码字体"
5293
+ codeFamily: "代码字体",
5294
+ limited: "只显示当前 DSH 主题支持的字体选项;其他已保存的设置会保留。"
5012
5295
  },
5013
5296
  en: {
5014
5297
  title: "Mobile font size",
@@ -5021,7 +5304,8 @@ window.__ModuleLoader__.load({
5021
5304
  familyHint: "Use fonts already on your device, separated by commas. Leave empty for the default.",
5022
5305
  codeSize: "Code font size",
5023
5306
  textFamily: "Text font",
5024
- codeFamily: "Code font"
5307
+ codeFamily: "Code font",
5308
+ limited: "Only font options supported by the current DSH theme are shown. Other saved preferences are retained."
5025
5309
  },
5026
5310
  it: {
5027
5311
  title: "Dimensione testo mobile",
@@ -5034,7 +5318,8 @@ window.__ModuleLoader__.load({
5034
5318
  familyHint: "Usa caratteri presenti sul dispositivo, separati da virgole. Lascia vuoto per il predefinito.",
5035
5319
  codeSize: "Dimensione codice",
5036
5320
  textFamily: "Carattere testo",
5037
- codeFamily: "Carattere codice"
5321
+ codeFamily: "Carattere codice",
5322
+ limited: "Sono mostrate solo le opzioni supportate dal tema DSH corrente. Le altre preferenze salvate vengono mantenute."
5038
5323
  }
5039
5324
  };
5040
5325
  function FontFamilyInput({ preference, role, value, label, placeholder }) {
@@ -5062,6 +5347,7 @@ window.__ModuleLoader__.load({
5062
5347
  /** General settings row matching the existing Mobile controls and DSH theme tokens. */
5063
5348
  function MobileFontSizeRow({ preference, locale }) {
5064
5349
  const typography = (0, react.useSyncExternalStore)(preference.subscribe, preference.getTypographySnapshot);
5350
+ const capabilities = (0, react.useSyncExternalStore)(preference.subscribeCapabilities, preference.getCapabilitiesSnapshot);
5065
5351
  const copy = messages$1[locale];
5066
5352
  const controls = (role, title) => {
5067
5353
  const size = typography.sizes[role];
@@ -5092,18 +5378,22 @@ window.__ModuleLoader__.load({
5092
5378
  return (0, react.createElement)("div", {
5093
5379
  lang: locale,
5094
5380
  "data-mobile-font-setting": true
5095
- }, (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", { className: "dsh-mobile-settings_desc" }, copy.description)), controls("text", copy.title)), (0, react.createElement)("details", { className: "dsh-mobile-settings_fontDetails" }, (0, react.createElement)("summary", null, copy.more), (0, react.createElement)("p", { className: "dsh-mobile-settings_desc" }, copy.familyHint), (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("span", { className: "dsh-mobile-settings_rowText dsh-mobile-settings_title" }, copy.codeSize), controls("code", copy.codeSize)), ...MOBILE_FONT_ROLES.map((role) => (0, react.createElement)(FontFamilyInput, {
5381
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", { className: "dsh-mobile-settings_desc" }, copy.description)), controls("text", copy.title)), !Object.values(capabilities).every(Boolean) && (0, react.createElement)("p", {
5382
+ className: "dsh-mobile-settings_desc",
5383
+ "data-mobile-font-limited": true
5384
+ }, copy.limited), Object.values(capabilities).some(Boolean) && (0, react.createElement)("details", { className: "dsh-mobile-settings_fontDetails" }, (0, react.createElement)("summary", null, copy.more), (capabilities.textFamily || capabilities.codeFamily) && (0, react.createElement)("p", { className: "dsh-mobile-settings_desc" }, copy.familyHint), capabilities.codeSize && (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("span", { className: "dsh-mobile-settings_rowText dsh-mobile-settings_title" }, copy.codeSize), controls("code", copy.codeSize)), ...MOBILE_FONT_ROLES.filter((role) => role === "text" ? capabilities.textFamily : capabilities.codeFamily).map((role) => (0, react.createElement)(FontFamilyInput, {
5096
5385
  key: role,
5097
5386
  preference,
5098
5387
  role,
5099
5388
  value: typography.families[role],
5100
5389
  label: role === "text" ? copy.textFamily : copy.codeFamily,
5101
5390
  placeholder: copy.defaultFamily
5102
- })), (0, react.createElement)("button", {
5391
+ }))), (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("button", {
5103
5392
  type: "button",
5104
5393
  className: "dsh-mobile-settings_selector",
5394
+ "data-mobile-font-reset": true,
5105
5395
  onClick: () => {
5106
- preference.reset();
5396
+ preference.resetSupported();
5107
5397
  }
5108
5398
  }, copy.reset)));
5109
5399
  }
@@ -5326,6 +5616,116 @@ window.__ModuleLoader__.load({
5326
5616
  }, copy.reset)));
5327
5617
  }
5328
5618
  //#endregion
5619
+ //#region src/mobile-native-settings-action.ts
5620
+ /** Own capability discovery and explicit calls; native actions retain the App's own timeout. */
5621
+ var MobileNativeSettingsActionController = class {
5622
+ action;
5623
+ getBridge;
5624
+ snapshot = Object.freeze({
5625
+ supported: false,
5626
+ state: "idle"
5627
+ });
5628
+ listeners = /* @__PURE__ */ new Set();
5629
+ bridge;
5630
+ active = false;
5631
+ generation = 0;
5632
+ probe = 0;
5633
+ attempt = 0;
5634
+ constructor(action, getBridge) {
5635
+ this.action = action;
5636
+ this.getBridge = getBridge;
5637
+ }
5638
+ getSnapshot = () => this.snapshot;
5639
+ subscribe = (listener) => {
5640
+ this.listeners.add(listener);
5641
+ return () => {
5642
+ this.listeners.delete(listener);
5643
+ };
5644
+ };
5645
+ start() {
5646
+ this.active = true;
5647
+ this.publish({
5648
+ supported: false,
5649
+ state: "idle"
5650
+ });
5651
+ this.refresh();
5652
+ }
5653
+ stop() {
5654
+ this.active = false;
5655
+ this.generation += 1;
5656
+ this.probe += 1;
5657
+ this.attempt += 1;
5658
+ this.bridge = void 0;
5659
+ }
5660
+ /** Recheck support without replaying or invalidating an in-flight call to the same adapter. */
5661
+ refresh = () => {
5662
+ if (!this.active) return;
5663
+ const bridge = this.getBridge();
5664
+ const probe = ++this.probe;
5665
+ if (bridge !== this.bridge) {
5666
+ this.bridge = bridge;
5667
+ this.generation += 1;
5668
+ this.attempt += 1;
5669
+ this.publish({
5670
+ supported: false,
5671
+ state: "idle"
5672
+ });
5673
+ }
5674
+ if (bridge === void 0) return;
5675
+ const generation = this.generation;
5676
+ const settle = (supported) => {
5677
+ if (!this.current(bridge, generation) || probe !== this.probe) return;
5678
+ if (!supported) this.attempt += 1;
5679
+ this.publish({
5680
+ supported,
5681
+ state: supported ? this.snapshot.state : "idle"
5682
+ });
5683
+ };
5684
+ Promise.resolve().then(() => {
5685
+ if (!this.current(bridge, generation) || probe !== this.probe) return void 0;
5686
+ return bridge.capabilities();
5687
+ }).then((capabilities) => {
5688
+ settle(capabilities?.includes(this.action) === true);
5689
+ }, () => {
5690
+ settle(false);
5691
+ });
5692
+ };
5693
+ /** Send at most one explicit action; late replies cannot update a replaced or unmounted row. */
5694
+ invoke = () => {
5695
+ const bridge = this.bridge;
5696
+ if (bridge === void 0 || !this.snapshot.supported || !this.current(bridge, this.generation) || this.snapshot.state === "busy" || this.action === "mobile.switch-computer" && this.snapshot.state === "success") return;
5697
+ const generation = this.generation;
5698
+ const attempt = ++this.attempt;
5699
+ this.publish({
5700
+ supported: true,
5701
+ state: "busy"
5702
+ });
5703
+ const current = () => this.current(bridge, generation) && attempt === this.attempt && this.snapshot.supported;
5704
+ Promise.resolve().then(() => {
5705
+ if (!current()) return void 0;
5706
+ return bridge.invoke(this.action, {});
5707
+ }).then(() => {
5708
+ if (current()) this.publish({
5709
+ supported: true,
5710
+ state: "success"
5711
+ });
5712
+ }, () => {
5713
+ if (current()) this.publish({
5714
+ supported: true,
5715
+ state: "error"
5716
+ });
5717
+ });
5718
+ };
5719
+ current(bridge, generation) {
5720
+ return this.active && generation === this.generation && this.bridge === bridge && this.getBridge() === bridge;
5721
+ }
5722
+ publish(snapshot) {
5723
+ if (snapshot.supported === this.snapshot.supported && snapshot.state === this.snapshot.state) return;
5724
+ this.snapshot = Object.freeze(snapshot);
5725
+ for (const listener of this.listeners) listener();
5726
+ }
5727
+ };
5728
+ //#endregion
5329
5729
  //#region src/local-admin-host.ts
5330
5730
  /** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
5331
5731
  const DESKTOP_ADMIN_HEADER = "x-dsh-mobile-desktop-admin";
@@ -5386,65 +5786,282 @@ window.__ModuleLoader__.load({
5386
5786
  return (protocol === "dsh-app:" ? hostname === "app" : (protocol === "" || protocol === "http:" || protocol === "https:") && isLocalAdminHostname(hostname)) && frontend !== "dedicated" && !new URLSearchParams(query).has("dsh-mobile-preview");
5387
5787
  }
5388
5788
  //#endregion
5789
+ //#region src/client-module-presentation.ts
5790
+ /** Group boot-protected entries before considering their package namespace.
5791
+ * @param entry - Catalog entry with server-owned required and dependency fields.
5792
+ * @returns Display group, never an authorization or compatibility decision.
5793
+ */
5794
+ function clientModuleGroup(entry) {
5795
+ if (entry.required) return "required";
5796
+ return entry.id.startsWith("@deepseek-ai/") ? "advanced" : "extensions";
5797
+ }
5798
+ /** Format the package leaf mechanically; the catalog has no product titles.
5799
+ * @param id - Exact module ID from the server catalog.
5800
+ * @returns Unscoped package words without guessed function or plugin ownership.
5801
+ */
5802
+ function clientModuleDisplayName(id) {
5803
+ const leaf = id.startsWith("@") && id.includes("/") ? id.slice(id.indexOf("/") + 1) : id;
5804
+ return (leaf.replace(/^dsh-(?:client-(?:ui-)?)?/u, "") || leaf || id).replace(/[-_]+/gu, " ");
5805
+ }
5806
+ function matchesQuery(id, query) {
5807
+ const needle = query.trim().toLowerCase();
5808
+ return needle === "" || `${id}\n${clientModuleDisplayName(id)}`.toLowerCase().includes(needle);
5809
+ }
5810
+ /** Match the visible title or exact ID and the current draft loading choice.
5811
+ * @param entry - Catalog entry to consider.
5812
+ * @param excluded - Current draft exclusions, not a runtime loading observation.
5813
+ * @param query - User-entered search text.
5814
+ * @param filter - All, selected for loading, or selected for omission.
5815
+ * @returns Whether the row belongs in the current filtered list.
5816
+ */
5817
+ function clientModuleMatches(entry, excluded, query, filter) {
5818
+ const omitted = !entry.required && excluded.has(entry.id);
5819
+ return matchesQuery(entry.id, query) && (filter === "all" || (filter === "excluded" ? omitted : !omitted));
5820
+ }
5821
+ /** Editable selections retain unknown server exclusions and never cascade dependencies. */
5822
+ var ClientModuleDraft = class {
5823
+ entries;
5824
+ initial;
5825
+ excluded;
5826
+ entriesById;
5827
+ constructor(entries, excludedIds) {
5828
+ this.entries = entries;
5829
+ this.initial = new Set(excludedIds);
5830
+ this.excluded = new Set(excludedIds);
5831
+ this.entriesById = new Map(entries.map((entry) => [entry.id, entry]));
5832
+ }
5833
+ /** @returns Number of changed loading choices, including hidden filtered rows. */
5834
+ get changeCount() {
5835
+ let count = 0;
5836
+ for (const id of this.initial) if (!this.excluded.has(id)) count += 1;
5837
+ for (const id of this.excluded) if (!this.initial.has(id)) count += 1;
5838
+ return count;
5839
+ }
5840
+ /** @returns A snapshot of exclusions suitable for authenticated saving. */
5841
+ get excludedIds() {
5842
+ return [...this.excluded];
5843
+ }
5844
+ /** @returns Retained exclusions that cannot be saved against the current catalog. */
5845
+ get invalidExclusions() {
5846
+ return [...this.excluded].flatMap((id) => {
5847
+ const entry = this.entriesById.get(id);
5848
+ return entry === void 0 ? [{
5849
+ id,
5850
+ reason: "missing"
5851
+ }] : entry.required ? [{
5852
+ id,
5853
+ reason: "required"
5854
+ }] : [];
5855
+ });
5856
+ }
5857
+ /** Explicitly remove an old exclusion; ordinary optional choices use setLoaded.
5858
+ * @param id - Exact ID of a retained missing or now-required exclusion.
5859
+ * @returns Whether this draft changed; no server request is made.
5860
+ */
5861
+ clearInvalidExclusion(id) {
5862
+ const entry = this.entriesById.get(id);
5863
+ if (!this.initial.has(id) || entry !== void 0 && !entry.required) return false;
5864
+ return this.excluded.delete(id);
5865
+ }
5866
+ /** @returns Whether a catalog entry is selected for loading, not already loaded. */
5867
+ isLoaded(id) {
5868
+ return this.entriesById.get(id)?.required === true || !this.excluded.has(id);
5869
+ }
5870
+ /** Change one optional choice; required and unknown entries cannot be edited.
5871
+ * @param id - Exact catalog ID.
5872
+ * @param loaded - Whether to load the entry on the next open.
5873
+ */
5874
+ setLoaded(id, loaded) {
5875
+ const entry = this.entriesById.get(id);
5876
+ if (entry === void 0 || entry.required) return;
5877
+ if (loaded) this.excluded.delete(id);
5878
+ else this.excluded.add(id);
5879
+ }
5880
+ /** Filter a draft without changing hidden choices.
5881
+ * @param query - Search input.
5882
+ * @param filter - Requested loading selection.
5883
+ * @returns Catalog entries in their original stable order.
5884
+ */
5885
+ matchingEntries(query, filter) {
5886
+ return this.entries.filter((entry) => clientModuleMatches(entry, this.excluded, query, filter));
5887
+ }
5888
+ /** Find invalid old exclusions by name or ID, independently from the protected catalog row.
5889
+ * @param query - Search input.
5890
+ * @param filter - Old exclusions appear in all and excluded selections, not loaded.
5891
+ * @returns Invalid draft exclusions in their retained order.
5892
+ */
5893
+ matchingInvalidExclusions(query, filter) {
5894
+ return filter === "loaded" ? [] : this.invalidExclusions.filter((entry) => matchesQuery(entry.id, query));
5895
+ }
5896
+ };
5897
+ //#endregion
5389
5898
  //#region src/client-module-ui.ts
5390
5899
  /** Device-local module selection and the computer's defaults share a read-only catalog. */
5391
5900
  const CLIENT_MODULE_COPY = {
5392
5901
  zh: {
5393
5902
  title: "移动页面模块",
5394
- computer: "为移动访问设置默认加载模块,不卸载电脑上的插件。",
5395
- device: "只为本设备选择加载模块,不改变其他手机或电脑。",
5903
+ computer: "设置此电脑的移动访问默认项;电脑插件不变。",
5904
+ device: "仅本设备,下次打开生效;电脑插件不变。",
5396
5905
  open: "管理",
5906
+ help: "加载规则",
5397
5907
  loading: "正在读取模块…",
5398
- hint: "勾选表示加载。启动必需的模块不能取消;依赖冲突会在保存时说明。",
5399
- save: "保存,下次打开生效",
5908
+ hint: "勾选表示下次打开时加载。启动必需项受保护;依赖冲突会在保存时说明,不会自动取消其他模块。",
5909
+ save: "保存选择",
5400
5910
  reset: "恢复默认",
5401
5911
  close: "关闭",
5402
5912
  required: "启动必需",
5403
5913
  dependencies: "依赖:",
5404
- saved: "已保存,不会自动刷新当前页面。重新打开 DSH 后生效。",
5405
- failed: "暂时无法完成。请确认移动访问已启用,或检查下面的依赖冲突。",
5914
+ noDependencies: "无声明的前端依赖",
5915
+ details: "模块详情",
5916
+ moduleId: "模块 ID",
5917
+ saved: "已保存,下次打开 DSH 生效。不会自动刷新当前页面或丢弃草稿。",
5918
+ failed: "暂时无法读取模块。请确认移动访问已启用后重试。",
5919
+ saveFailed: "未确认保存成功,已保留你的选择。可重新保存,或重新读取已保存设置。",
5406
5920
  conflict: "无法保存:",
5407
- computerSource: "使用电脑默认设置",
5408
- deviceSource: "使用本设备设置",
5409
- resetConfirm: "恢复默认模块选择?不会重新加载当前会话。"
5921
+ computerSource: "当前来源:电脑默认设置",
5922
+ deviceSource: "当前来源:本设备设置",
5923
+ pluginSource: "当前来源:插件默认设置",
5924
+ resetConfirm: "恢复默认模块选择?仅下次打开生效,不会重新加载当前会话。",
5925
+ discardConfirm: "放弃未保存的模块选择并关闭?",
5926
+ reloadConfirm: "重新读取会替换未保存的选择。继续吗?",
5927
+ savingCloseConfirm: "保存结果尚未确认。现在关闭不会撤销可能已完成的保存,确定关闭吗?",
5928
+ search: "搜索模块",
5929
+ searchPlaceholder: "搜索名称或模块 ID",
5930
+ filter: "加载选择",
5931
+ all: "全部",
5932
+ loaded: "加载",
5933
+ excluded: "不加载",
5934
+ empty: "没有符合条件的模块。",
5935
+ clearFilters: "清除筛选",
5936
+ extensions: "可选扩展",
5937
+ extensionsHint: "可选择不加载的前端模块;一项模块不一定等于一个完整插件。",
5938
+ advanced: "DSH 可选组件(高级)",
5939
+ advancedHint: "DSH 的可选前端组件。只修改你了解的项目。",
5940
+ requiredGroup: "启动必需项",
5941
+ requiredHint: "这些模块保证页面启动,不能关闭。",
5942
+ changes: "未保存:{count} 项改动",
5943
+ unchanged: "没有未保存的改动",
5944
+ results: "匹配 {visible} / {total} 项",
5945
+ retry: "重新读取",
5946
+ saving: "正在保存…",
5947
+ readTimeout: "读取超时。当前页面和已保存设置未变,请重试。",
5948
+ writeTimeout: "保存超时,结果尚未确认。保留了你的选择;请重新读取确认,不会自动重试保存。",
5949
+ nextOpen: "仅下次打开生效",
5950
+ invalidSelections: "失效的旧选择",
5951
+ invalidHint: "这些旧排除已与当前模块目录不符。移除后仍需保存选择。",
5952
+ invalidMissing: "当前模块目录中不存在。",
5953
+ invalidRequired: "当前已是启动必需项,不能排除。",
5954
+ clearInvalid: "移除旧排除"
5410
5955
  },
5411
5956
  en: {
5412
5957
  title: "Mobile page modules",
5413
- computer: "Choose default modules for mobile access without uninstalling computer plugins.",
5414
- device: "Choose modules for this device only; other phones and computers are unchanged.",
5958
+ computer: "Mobile defaults for this computer. Desktop plugins are unchanged.",
5959
+ device: "For this device’s next open only. Computer plugins are unchanged.",
5415
5960
  open: "Manage",
5961
+ help: "Loading rules",
5416
5962
  loading: "Reading modules…",
5417
- hint: "Checked means loaded. Required boot modules cannot be unchecked; saving explains dependency conflicts.",
5418
- save: "Save for next open",
5963
+ hint: "Checked means load on the next open. Required entries are protected; saving explains dependency conflicts without unchecking other modules.",
5964
+ save: "Save selection",
5419
5965
  reset: "Restore defaults",
5420
5966
  close: "Close",
5421
5967
  required: "Required for boot",
5422
5968
  dependencies: "Depends on: ",
5423
- saved: "Saved. This page is not reloaded automatically. Reopen DSH to apply.",
5424
- failed: "Could not complete this operation. Enable mobile access or check the dependency conflict below.",
5969
+ noDependencies: "No declared frontend dependencies",
5970
+ details: "Module details",
5971
+ moduleId: "Module ID",
5972
+ saved: "Saved for the next DSH open. This page is not refreshed and its draft is kept.",
5973
+ failed: "Could not read modules. Enable mobile access and try again.",
5974
+ saveFailed: "Saving was not confirmed. Your choices are kept. Save again or reread the saved settings.",
5425
5975
  conflict: "Could not save: ",
5426
- computerSource: "Using computer defaults",
5427
- deviceSource: "Using this device’s settings",
5428
- resetConfirm: "Restore default module selection? The current conversation will not reload."
5976
+ computerSource: "Source: computer defaults",
5977
+ deviceSource: "Source: this device’s settings",
5978
+ pluginSource: "Source: plugin defaults",
5979
+ resetConfirm: "Restore default selections for the next open? The current conversation will not reload.",
5980
+ discardConfirm: "Discard unsaved module choices and close?",
5981
+ reloadConfirm: "Rereading replaces unsaved choices. Continue?",
5982
+ savingCloseConfirm: "Saving is not confirmed. Closing will not undo a save that may have completed. Close anyway?",
5983
+ search: "Search modules",
5984
+ searchPlaceholder: "Search name or module ID",
5985
+ filter: "Loading selection",
5986
+ all: "All",
5987
+ loaded: "Load",
5988
+ excluded: "Do not load",
5989
+ empty: "No modules match these filters.",
5990
+ clearFilters: "Clear filters",
5991
+ extensions: "Optional extensions",
5992
+ extensionsHint: "Optional frontend modules; one module is not necessarily a complete plugin.",
5993
+ advanced: "DSH optional components (advanced)",
5994
+ advancedHint: "Optional DSH frontend components. Change only entries you understand.",
5995
+ requiredGroup: "Required for boot",
5996
+ requiredHint: "These modules keep the page bootable and cannot be disabled.",
5997
+ changes: "Unsaved: {count} changes",
5998
+ unchanged: "No unsaved changes",
5999
+ results: "Matching {visible} / {total}",
6000
+ retry: "Reread settings",
6001
+ saving: "Saving…",
6002
+ readTimeout: "Reading timed out. This page and saved settings are unchanged; try again.",
6003
+ writeTimeout: "Saving timed out and its result is unconfirmed. Your choices are kept. Reread to confirm; saving is not retried automatically.",
6004
+ nextOpen: "Applies on next open only",
6005
+ invalidSelections: "Outdated selections",
6006
+ invalidHint: "These old exclusions no longer match the module catalog. Save your choices after removing them.",
6007
+ invalidMissing: "Not present in the current module catalog.",
6008
+ invalidRequired: "Now required for boot and cannot be excluded.",
6009
+ clearInvalid: "Remove old exclusion"
5429
6010
  },
5430
6011
  it: {
5431
6012
  title: "Moduli della pagina mobile",
5432
- computer: "Scegli i moduli predefiniti senza disinstallare i plugin del computer.",
5433
- device: "Scegli i moduli solo per questo dispositivo, senza cambiare gli altri.",
6013
+ computer: "Predefiniti mobile del computer; i plugin non cambiano.",
6014
+ device: "Solo alla prossima apertura su questo dispositivo; i plugin del computer non cambiano.",
5434
6015
  open: "Gestisci",
6016
+ help: "Regole di caricamento",
5435
6017
  loading: "Lettura dei moduli…",
5436
- hint: "Selezionato significa caricato. I moduli obbligatori non possono essere rimossi; il salvataggio mostra i conflitti.",
5437
- save: "Salva per la prossima apertura",
6018
+ hint: "Selezionato significa caricare alla prossima apertura. I moduli obbligatori sono protetti; il salvataggio mostra i conflitti senza deselezionare altri moduli.",
6019
+ save: "Salva selezione",
5438
6020
  reset: "Ripristina predefiniti",
5439
6021
  close: "Chiudi",
5440
6022
  required: "Obbligatorio",
5441
6023
  dependencies: "Dipende da: ",
5442
- saved: "Salvato. La pagina non viene ricaricata automaticamente. Riapri DSH per applicare.",
5443
- failed: "Operazione non completata. Abilita accesso mobile o controlla il conflitto sotto.",
6024
+ noDependencies: "Nessuna dipendenza frontend dichiarata",
6025
+ details: "Dettagli modulo",
6026
+ moduleId: "ID modulo",
6027
+ saved: "Salvato per la prossima apertura di DSH. La pagina non si aggiorna e la bozza resta.",
6028
+ failed: "Impossibile leggere i moduli. Abilita accesso mobile e riprova.",
6029
+ saveFailed: "Salvataggio non confermato. Le scelte restano; salva di nuovo o rileggi le impostazioni salvate.",
5444
6030
  conflict: "Impossibile salvare: ",
5445
- computerSource: "Predefiniti del computer",
5446
- deviceSource: "Impostazioni del dispositivo",
5447
- resetConfirm: "Ripristinare i moduli predefiniti? La conversazione attuale non verrà ricaricata."
6031
+ computerSource: "Origine: predefiniti del computer",
6032
+ deviceSource: "Origine: impostazioni del dispositivo",
6033
+ pluginSource: "Origine: predefiniti del plugin",
6034
+ resetConfirm: "Ripristinare i predefiniti per la prossima apertura? La conversazione attuale non verrà ricaricata.",
6035
+ discardConfirm: "Scartare le scelte non salvate e chiudere?",
6036
+ reloadConfirm: "Rileggere sostituisce le scelte non salvate. Continuare?",
6037
+ savingCloseConfirm: "Salvataggio non confermato. Chiudere non annulla un salvataggio già completato. Chiudere comunque?",
6038
+ search: "Cerca moduli",
6039
+ searchPlaceholder: "Cerca nome o ID modulo",
6040
+ filter: "Selezione caricamento",
6041
+ all: "Tutti",
6042
+ loaded: "Carica",
6043
+ excluded: "Non caricare",
6044
+ empty: "Nessun modulo corrisponde ai filtri.",
6045
+ clearFilters: "Cancella filtri",
6046
+ extensions: "Estensioni facoltative",
6047
+ extensionsHint: "Moduli frontend facoltativi; un modulo non è necessariamente un plugin completo.",
6048
+ advanced: "Componenti DSH facoltativi (avanzate)",
6049
+ advancedHint: "Componenti frontend DSH facoltativi. Modifica solo quelli che conosci.",
6050
+ requiredGroup: "Moduli obbligatori",
6051
+ requiredHint: "Questi moduli permettono l’avvio e non possono essere disattivati.",
6052
+ changes: "Non salvate: {count} modifiche",
6053
+ unchanged: "Nessuna modifica non salvata",
6054
+ results: "Corrispondenti {visible} / {total}",
6055
+ retry: "Rileggi impostazioni",
6056
+ saving: "Salvataggio…",
6057
+ readTimeout: "Lettura scaduta. Pagina e impostazioni salvate non cambiano; riprova.",
6058
+ writeTimeout: "Salvataggio scaduto e non confermato. Le scelte restano. Rileggi per confermare; nessun tentativo automatico.",
6059
+ nextOpen: "Solo alla prossima apertura",
6060
+ invalidSelections: "Scelte obsolete",
6061
+ invalidHint: "Queste esclusioni precedenti non corrispondono più al catalogo. Salva le scelte dopo averle rimosse.",
6062
+ invalidMissing: "Assente dal catalogo attuale dei moduli.",
6063
+ invalidRequired: "Ora obbligatorio per l’avvio e non può essere escluso.",
6064
+ clearInvalid: "Rimuovi esclusione precedente"
5448
6065
  }
5449
6066
  };
5450
6067
  var ClientModuleUiConflict = class extends Error {
@@ -5454,29 +6071,40 @@ window.__ModuleLoader__.load({
5454
6071
  this.detail = detail;
5455
6072
  }
5456
6073
  };
6074
+ /** A deadline does not imply that a POST was rolled back by the server. */
6075
+ var ClientModuleRequestTimeout = class extends Error {
6076
+ mutation;
6077
+ constructor(mutation) {
6078
+ super("client_module_request_timeout");
6079
+ this.mutation = mutation;
6080
+ }
6081
+ };
6082
+ const CLIENT_MODULE_REQUEST_TIMEOUT_MS = {
6083
+ read: 15e3,
6084
+ write: 2e4
6085
+ };
5457
6086
  /** Validate catalog response fields before creating interactive controls.
5458
6087
  * @param value - Gateway or computer-admin JSON response.
5459
6088
  * @returns The fields used by the module-selection dialog.
5460
6089
  */
5461
6090
  function parseClientModuleView(value) {
5462
- if (typeof value !== "object" || value === null || !("entries" in value) || !Array.isArray(value.entries) || !("excludedClientModules" in value) || !Array.isArray(value.excludedClientModules) || !("source" in value) || ![
5463
- "device",
5464
- "computer",
5465
- "plugin"
5466
- ].includes(String(value.source))) throw new Error("invalid_module_catalog");
6091
+ if (typeof value !== "object" || value === null || !("entries" in value) || !Array.isArray(value.entries) || !("excludedClientModules" in value) || !Array.isArray(value.excludedClientModules) || !("source" in value) || value.source !== "device" && value.source !== "computer" && value.source !== "plugin") throw new Error("invalid_module_catalog");
6092
+ const entries = value.entries.map((entry) => {
6093
+ if (typeof entry !== "object" || entry === null || !("id" in entry) || typeof entry.id !== "string" || !("required" in entry) || typeof entry.required !== "boolean" || !("dependencies" in entry) || !Array.isArray(entry.dependencies)) throw new Error("invalid_module_catalog");
6094
+ const dependencies = entry.dependencies.map((dependency) => {
6095
+ if (typeof dependency !== "string") throw new Error("invalid_module_catalog");
6096
+ return dependency;
6097
+ });
6098
+ if (entry.id.trim() === "") throw new Error("invalid_module_catalog");
6099
+ return {
6100
+ id: entry.id,
6101
+ required: entry.required,
6102
+ dependencies
6103
+ };
6104
+ });
6105
+ if (new Set(entries.map((entry) => entry.id)).size !== entries.length) throw new Error("invalid_module_catalog");
5467
6106
  return {
5468
- entries: value.entries.map((entry) => {
5469
- if (typeof entry !== "object" || entry === null || !("id" in entry) || typeof entry.id !== "string" || !("required" in entry) || typeof entry.required !== "boolean" || !("dependencies" in entry) || !Array.isArray(entry.dependencies)) throw new Error("invalid_module_catalog");
5470
- const dependencies = entry.dependencies.map((dependency) => {
5471
- if (typeof dependency !== "string") throw new Error("invalid_module_catalog");
5472
- return dependency;
5473
- });
5474
- return {
5475
- id: entry.id,
5476
- required: entry.required,
5477
- dependencies
5478
- };
5479
- }),
6107
+ entries,
5480
6108
  excludedClientModules: value.excludedClientModules.map((id) => {
5481
6109
  if (typeof id !== "string") throw new Error("invalid_module_catalog");
5482
6110
  return id;
@@ -5484,32 +6112,103 @@ window.__ModuleLoader__.load({
5484
6112
  source: value.source === "device" ? "device" : value.source === "computer" ? "computer" : "plugin"
5485
6113
  };
5486
6114
  }
6115
+ /** Read or explicitly write module choices with a bounded, cancellable operation.
6116
+ * @param scope - Computer-admin endpoint or authenticated device endpoint.
6117
+ * @param signal - Owning dialog lifetime; cancellation cannot undo a completed write.
6118
+ * @param body - Omit for a read; supplied fields are sent once without automatic replay.
6119
+ * @returns Validated catalog fields returned by the selected endpoint.
6120
+ */
6121
+ async function requestClientModuleView(scope, signal, body) {
6122
+ if (signal.aborted) throw new DOMException("Dialog closed", "AbortError");
6123
+ const controller = new AbortController();
6124
+ let rejectCancellation = () => {};
6125
+ const cancellation = new Promise((_, reject) => {
6126
+ rejectCancellation = reject;
6127
+ });
6128
+ const abort = () => {
6129
+ controller.abort();
6130
+ rejectCancellation(new DOMException("Dialog closed", "AbortError"));
6131
+ };
6132
+ signal.addEventListener("abort", abort, { once: true });
6133
+ const timer = setTimeout(() => {
6134
+ rejectCancellation(new ClientModuleRequestTimeout(body !== void 0));
6135
+ controller.abort();
6136
+ }, body === void 0 ? CLIENT_MODULE_REQUEST_TIMEOUT_MS.read : CLIENT_MODULE_REQUEST_TIMEOUT_MS.write);
6137
+ const operation = async () => {
6138
+ const init = {
6139
+ method: body === void 0 ? "GET" : "POST",
6140
+ signal: controller.signal,
6141
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
6142
+ };
6143
+ init.headers = scope === "computer" ? localAdminRequestHeaders(init, location) : { "content-type": "application/json" };
6144
+ if (scope === "device" && body !== void 0) {
6145
+ const cookies = document.cookie.split(";").map((value) => value.trim());
6146
+ const csrfCookie = cookies.find((value) => value.startsWith("__Host-dsh_ma_csrf=")) ?? cookies.find((value) => value.startsWith("dsh_ma_csrf="));
6147
+ if (csrfCookie !== void 0) {
6148
+ const headers = new Headers(init.headers);
6149
+ headers.set("x-dsh-mobile-csrf", csrfCookie.slice(csrfCookie.indexOf("=") + 1));
6150
+ init.headers = headers;
6151
+ }
6152
+ }
6153
+ const response = await fetch(scope === "computer" ? "/api/mobile-access/client-modules" : "/mobile-access/client-modules", init);
6154
+ const value = await response.json();
6155
+ if (!response.ok) {
6156
+ const detail = typeof value === "object" && value !== null && "detail" in value && typeof value.detail === "string" ? value.detail.slice(0, 512) : "";
6157
+ if (response.status === 409 && detail !== "" && typeof value === "object" && value !== null && "error" in value && value.error === "excluded_client_modules_invalid") throw new ClientModuleUiConflict(detail);
6158
+ throw new Error("client_module_request_failed");
6159
+ }
6160
+ return parseClientModuleView(value);
6161
+ };
6162
+ try {
6163
+ return await Promise.race([operation(), cancellation]);
6164
+ } finally {
6165
+ clearTimeout(timer);
6166
+ signal.removeEventListener("abort", abort);
6167
+ }
6168
+ }
5487
6169
  const CLIENT_MODULE_STYLES = `
5488
6170
  .dsh-module-row{display:flex;align-items:center;gap:8px;padding:16px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}
5489
6171
  .dsh-module-rowText{flex:1;min-width:0}.dsh-module-title{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}
5490
6172
  .dsh-module-description{margin-top:4px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
5491
6173
  .dsh-module-button{min-height:48px;padding:0 14px;border:0;border-radius:24px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;cursor:pointer}
5492
6174
  .dsh-module-button:focus-visible{outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb);outline-offset:2px}
5493
- .dsh-module-dialog{box-sizing:border-box;width:min(640px,calc(100vw - 24px));max-height:calc(100dvh - 32px);padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-panel,20px);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#171a21);font:16px/1.5 var(--dsw-font-family,sans-serif);overflow:auto}
5494
- .dsh-module-dialog::backdrop{background:rgb(15 23 42 / 40%)}.dsh-module-dialog h2{font-size:20px;margin:0 0 12px}
5495
- .dsh-module-dialog p{overflow-wrap:anywhere}.dsh-module-list{display:flex;flex-direction:column;gap:4px;margin:12px 0}
5496
- .dsh-module-choice{display:flex;align-items:flex-start;gap:12px;min-height:48px;padding:8px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
5497
- .dsh-module-choice input{width:24px;height:24px;flex:none;margin:2px 0}.dsh-module-choice span{min-width:0;overflow-wrap:anywhere}
5498
- .dsh-module-choice small{display:block;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:18px}.dsh-module-actions{display:flex;flex-wrap:wrap;gap:8px}
6175
+ .dsh-module-button:not(:disabled):active{background:var(--dsw-alias-interactive-bg-active)}
6176
+ .dsh-module-dialog{box-sizing:border-box;width:min(640px,calc(100vw - 24px));max-height:calc(100dvh - 32px);padding:0;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-panel,20px);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#171a21);font:16px/1.5 var(--dsw-font-family,sans-serif);overflow:hidden}
6177
+ .dsh-module-dialog[open]{display:flex;flex-direction:column}.dsh-module-dialog::backdrop{background:rgb(15 23 42 / 40%)}
6178
+ .dsh-module-header{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
6179
+ .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}
6180
+ .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}
6181
+ .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))}
6182
+ .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}
6183
+ .dsh-module-help>summary{box-sizing:border-box;min-height:48px;padding:12px 0;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-secondary)}
6184
+ .dsh-module-field input,.dsh-module-field select{box-sizing:border-box;width:100%;min-width:0;min-height:48px;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font:inherit;font-size:16px}
6185
+ .dsh-module-dialog :is(input,select,summary):focus-visible{outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb);outline-offset:2px}
6186
+ .dsh-module-list{display:flex;flex-direction:column;gap:16px;margin:12px 0}.dsh-module-group h3{margin:0 0 8px;font-size:16px;line-height:24px}
6187
+ .dsh-module-group>summary{min-height:48px;cursor:pointer;font-weight:600;overflow-wrap:anywhere;align-content:center}.dsh-module-group>summary+div{margin-top:8px}
6188
+ .dsh-module-entry{border-bottom:1px solid var(--dsw-alias-border-l2);padding:8px 0}.dsh-module-choice{display:flex;align-items:center;gap:12px;min-height:48px;cursor:pointer}
6189
+ .dsh-module-choice input{width:24px;height:24px;flex:none;margin:0}.dsh-module-choice span{min-width:0;overflow-wrap:anywhere}.dsh-module-choiceName{display:block;font-weight:500}
6190
+ .dsh-module-choice small{display:block}.dsh-module-entryDetails{margin-left:36px;min-width:0}.dsh-module-entryDetails summary{min-height:48px;align-content:center;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
6191
+ .dsh-module-invalid-entry code{display:block;min-width:0;overflow-wrap:anywhere;white-space:normal;font-size:13px}.dsh-module-invalid-entry .dsh-module-button{max-width:100%;white-space:normal;line-height:20px;padding:8px 14px}
6192
+ .dsh-module-entryDetails p,.dsh-module-entryDetails code{overflow-wrap:anywhere;font-size:13px}.dsh-module-entryDetails code{white-space:normal}
6193
+ .dsh-module-footer{flex:none;padding:12px 16px max(12px,env(safe-area-inset-bottom));border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1,#fff)}
6194
+ .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}
6195
+ .dsh-module-button:disabled{opacity:.5;cursor:default}.dsh-module-save{background:var(--dsw-alias-bg-module-platform);font-weight:600}
6196
+ .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}
6197
+ @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}}
5499
6198
  .dsh-module-error{color:var(--dsw-alias-state-error-primary,#b42318)}
5500
6199
  `;
5501
6200
  const openDialogs = /* @__PURE__ */ new Set();
5502
6201
  /** Cancel pending dialogs when the owning plugin generation is disposed. */
5503
6202
  function disposeClientModuleDialogs() {
5504
- for (const dispose of [...openDialogs]) dispose();
6203
+ for (const dialog of [...openDialogs]) dialog.dispose();
5505
6204
  }
5506
6205
  /** Close the foreground module modal before navigating its underlying DSH settings.
5507
6206
  * @returns Whether an open module modal consumed the return action.
5508
6207
  */
5509
6208
  function closeClientModuleDialog() {
5510
- const dispose = [...openDialogs].at(-1);
5511
- if (dispose === void 0) return false;
5512
- dispose();
6209
+ const dialog = [...openDialogs].at(-1);
6210
+ if (dialog === void 0) return false;
6211
+ dialog.requestClose();
5513
6212
  return true;
5514
6213
  }
5515
6214
  /** Open one modal; closing cancels reads and restores focus without reloading the page.
@@ -5519,143 +6218,338 @@ window.__ModuleLoader__.load({
5519
6218
  */
5520
6219
  function openClientModuleSettings(scope, locale, trigger) {
5521
6220
  const copy = CLIENT_MODULE_COPY[locale];
5522
- const endpoint = scope === "computer" ? "/api/mobile-access/client-modules" : "/mobile-access/client-modules";
5523
6221
  const controller = new AbortController();
5524
6222
  const dialog = document.createElement("dialog");
5525
6223
  dialog.className = "dsh-module-dialog";
5526
6224
  dialog.setAttribute("aria-label", copy.title);
6225
+ dialog.lang = locale;
6226
+ const button = (text) => {
6227
+ const result = document.createElement("button");
6228
+ result.type = "button";
6229
+ result.className = "dsh-module-button";
6230
+ result.textContent = text;
6231
+ return result;
6232
+ };
6233
+ const paragraph = (text, className = "") => {
6234
+ const result = document.createElement("p");
6235
+ result.textContent = text;
6236
+ result.className = className;
6237
+ return result;
6238
+ };
6239
+ const header = document.createElement("div");
6240
+ header.className = "dsh-module-header";
5527
6241
  const title = document.createElement("h2");
5528
6242
  title.textContent = copy.title;
5529
- const intro = document.createElement("p");
5530
- intro.textContent = `${copy[scope]} ${copy.hint}`;
5531
- const status = document.createElement("p");
6243
+ const close = button(copy.close);
6244
+ header.append(title, close);
6245
+ const body = document.createElement("div");
6246
+ body.className = "dsh-module-body";
6247
+ const intro = paragraph(copy[scope]);
6248
+ const hint = paragraph(copy.hint, "dsh-module-hint");
6249
+ const help = document.createElement("details");
6250
+ help.className = "dsh-module-help";
6251
+ const helpTitle = document.createElement("summary");
6252
+ helpTitle.textContent = copy.help;
6253
+ help.append(helpTitle, hint);
6254
+ const source = paragraph("", "dsh-module-source");
6255
+ const status = paragraph("", "dsh-module-notice");
5532
6256
  status.setAttribute("role", "status");
5533
6257
  status.setAttribute("aria-live", "polite");
5534
6258
  status.tabIndex = -1;
6259
+ const tools = document.createElement("div");
6260
+ tools.className = "dsh-module-tools";
6261
+ const searchLabel = document.createElement("label");
6262
+ searchLabel.className = "dsh-module-field";
6263
+ searchLabel.textContent = copy.search;
6264
+ const search = document.createElement("input");
6265
+ search.type = "search";
6266
+ search.placeholder = copy.searchPlaceholder;
6267
+ search.setAttribute("aria-label", copy.search);
6268
+ searchLabel.append(search);
6269
+ const filterLabel = document.createElement("label");
6270
+ filterLabel.className = "dsh-module-field";
6271
+ filterLabel.textContent = copy.filter;
6272
+ const filter = document.createElement("select");
6273
+ filter.dataset.clientModuleFilter = "true";
6274
+ filter.setAttribute("aria-label", copy.filter);
6275
+ for (const value of [
6276
+ "all",
6277
+ "loaded",
6278
+ "excluded"
6279
+ ]) {
6280
+ const option = document.createElement("option");
6281
+ option.value = value;
6282
+ option.textContent = copy[value];
6283
+ filter.append(option);
6284
+ }
6285
+ filterLabel.append(filter);
6286
+ tools.append(searchLabel, filterLabel);
6287
+ const results = paragraph("", "dsh-module-results");
6288
+ results.setAttribute("aria-live", "polite");
5535
6289
  const list = document.createElement("div");
5536
6290
  list.className = "dsh-module-list";
6291
+ const footer = document.createElement("div");
6292
+ footer.className = "dsh-module-footer";
6293
+ const changes = paragraph(copy.unchanged);
6294
+ changes.setAttribute("role", "status");
6295
+ changes.setAttribute("aria-live", "polite");
5537
6296
  const actions = document.createElement("div");
5538
6297
  actions.className = "dsh-module-actions";
5539
- const button = (text) => {
5540
- const result = document.createElement("button");
5541
- result.type = "button";
5542
- result.className = "dsh-module-button";
5543
- result.textContent = text;
5544
- return result;
5545
- };
5546
6298
  const save = button(copy.save);
6299
+ save.classList.add("dsh-module-save");
5547
6300
  const reset = button(copy.reset);
5548
- const close = button(copy.close);
5549
- actions.append(save, reset, close);
5550
- dialog.append(title, intro, status, list, actions);
6301
+ const reread = button(copy.retry);
6302
+ actions.append(save, reset, reread);
6303
+ footer.append(changes, status, actions);
6304
+ body.append(intro, source, tools, results, list, help);
6305
+ dialog.append(header, body, footer);
5551
6306
  let disposed = false;
5552
- let busy = false;
5553
- let loaded = false;
6307
+ let busy;
6308
+ let generation = 0;
6309
+ let draft;
6310
+ let unconfirmedWrite = false;
6311
+ const expansion = /* @__PURE__ */ new Map([
6312
+ ["extensions", true],
6313
+ ["advanced", false],
6314
+ ["required", false]
6315
+ ]);
5554
6316
  const choices = /* @__PURE__ */ new Map();
5555
- const setBusy = (value) => {
5556
- busy = value;
5557
- save.disabled = value || !loaded;
5558
- reset.disabled = value || !loaded;
5559
- for (const input of choices.values()) if (!input.dataset.required) input.disabled = value;
5560
- };
5561
- const request = async (body) => {
5562
- const init = {
5563
- method: body === void 0 ? "GET" : "POST",
5564
- signal: controller.signal,
5565
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
5566
- };
5567
- init.headers = scope === "computer" ? localAdminRequestHeaders(init, location) : { "content-type": "application/json" };
5568
- if (scope === "device" && body !== void 0) {
5569
- const csrfCookie = document.cookie.split(";").map((value) => value.trim()).find((value) => value.startsWith("__Host-dsh_ma_csrf=")) ?? document.cookie.split(";").map((value) => value.trim()).find((value) => value.startsWith("dsh_ma_csrf="));
5570
- if (csrfCookie !== void 0) {
5571
- const headers = new Headers(init.headers);
5572
- headers.set("x-dsh-mobile-csrf", csrfCookie.slice(csrfCookie.indexOf("=") + 1));
5573
- init.headers = headers;
6317
+ const invalidActions = /* @__PURE__ */ new Map();
6318
+ const filterValue = () => filter.value === "loaded" ? "loaded" : filter.value === "excluded" ? "excluded" : "all";
6319
+ const updateControls = () => {
6320
+ const changed = draft?.changeCount ?? 0;
6321
+ save.disabled = busy !== void 0 || changed === 0;
6322
+ reset.disabled = busy !== void 0 || draft === void 0;
6323
+ reread.disabled = busy !== void 0;
6324
+ search.disabled = busy !== void 0 || draft === void 0;
6325
+ filter.disabled = search.disabled;
6326
+ save.textContent = busy === "write" ? copy.saving : copy.save;
6327
+ changes.textContent = `${changed === 0 ? copy.unchanged : copy.changes.replace("{count}", String(changed))} · ${copy.nextOpen}`;
6328
+ list.setAttribute("aria-busy", String(busy !== void 0));
6329
+ for (const input of choices.values()) input.disabled = busy !== void 0 || input.dataset.required === "true";
6330
+ for (const action of invalidActions.values()) action.disabled = busy !== void 0;
6331
+ };
6332
+ const renderList = () => {
6333
+ choices.clear();
6334
+ invalidActions.clear();
6335
+ list.replaceChildren();
6336
+ if (draft === void 0) {
6337
+ results.textContent = "";
6338
+ return;
6339
+ }
6340
+ const visible = draft.matchingEntries(search.value, filterValue());
6341
+ const invalid = draft.matchingInvalidExclusions(search.value, filterValue());
6342
+ results.textContent = copy.results.replace("{visible}", String(visible.length + invalid.length)).replace("{total}", String(draft.entries.length + draft.invalidExclusions.length));
6343
+ const filtered = search.value.trim() !== "" || filterValue() !== "all";
6344
+ if (visible.length === 0 && invalid.length === 0) {
6345
+ const empty = document.createElement("div");
6346
+ empty.className = "dsh-module-empty";
6347
+ empty.append(paragraph(copy.empty));
6348
+ const clear = button(copy.clearFilters);
6349
+ clear.addEventListener("click", () => {
6350
+ search.value = "";
6351
+ filter.value = "all";
6352
+ renderList();
6353
+ search.focus({ preventScroll: true });
6354
+ });
6355
+ empty.append(clear);
6356
+ list.append(empty);
6357
+ }
6358
+ if (invalid.length > 0) {
6359
+ const section = document.createElement("section");
6360
+ section.className = "dsh-module-group";
6361
+ section.dataset.clientModuleGroup = "invalid";
6362
+ const heading = document.createElement("h3");
6363
+ heading.textContent = `${copy.invalidSelections} (${invalid.length})`;
6364
+ section.append(heading, paragraph(copy.invalidHint, "dsh-module-groupHint"));
6365
+ for (const entry of invalid) {
6366
+ const row = document.createElement("div");
6367
+ row.className = "dsh-module-entry dsh-module-invalid-entry";
6368
+ row.dataset.invalidClientModuleId = entry.id;
6369
+ const id = document.createElement("code");
6370
+ id.textContent = entry.id;
6371
+ const remove = button(copy.clearInvalid);
6372
+ remove.setAttribute("aria-label", `${copy.clearInvalid}: ${entry.id}`);
6373
+ remove.addEventListener("click", () => {
6374
+ if (disposed || busy !== void 0 || draft?.clearInvalidExclusion(entry.id) !== true) return;
6375
+ status.textContent = "";
6376
+ status.classList.remove("dsh-module-error");
6377
+ renderList();
6378
+ (invalidActions.values().next().value ?? save).focus({ preventScroll: true });
6379
+ });
6380
+ row.append(id, paragraph(entry.reason === "missing" ? copy.invalidMissing : copy.invalidRequired), remove);
6381
+ invalidActions.set(entry.id, remove);
6382
+ section.append(row);
5574
6383
  }
6384
+ list.append(section);
5575
6385
  }
5576
- const response = await fetch(endpoint, init);
5577
- const value = await response.json();
5578
- if (!response.ok) {
5579
- const detail = typeof value === "object" && value !== null && "detail" in value && typeof value.detail === "string" ? value.detail.slice(0, 512) : "";
5580
- if (response.status === 409 && detail !== "" && typeof value === "object" && value !== null && "error" in value && value.error === "excluded_client_modules_invalid") throw new ClientModuleUiConflict(detail);
5581
- throw new Error("client_module_request_failed");
6386
+ for (const group of [
6387
+ "extensions",
6388
+ "advanced",
6389
+ "required"
6390
+ ]) {
6391
+ const entries = visible.filter((entry) => clientModuleGroup(entry) === group);
6392
+ if (entries.length === 0) continue;
6393
+ const section = document.createElement(group === "extensions" ? "section" : "details");
6394
+ section.className = "dsh-module-group";
6395
+ section.dataset.group = group;
6396
+ section.dataset.clientModuleGroup = group === "advanced" ? "system" : group;
6397
+ const headingText = `${group === "required" ? copy.requiredGroup : copy[group]} (${entries.length})`;
6398
+ if (section instanceof HTMLDetailsElement) {
6399
+ section.open = filtered || expansion.get(group) === true;
6400
+ const heading = document.createElement("summary");
6401
+ heading.textContent = headingText;
6402
+ section.append(heading);
6403
+ section.addEventListener("toggle", () => {
6404
+ if (!filtered) expansion.set(group, section.open);
6405
+ });
6406
+ } else {
6407
+ const heading = document.createElement("h3");
6408
+ heading.textContent = headingText;
6409
+ section.append(heading);
6410
+ }
6411
+ const content = document.createElement("div");
6412
+ content.append(paragraph(group === "required" ? copy.requiredHint : group === "advanced" ? copy.advancedHint : copy.extensionsHint, "dsh-module-groupHint"));
6413
+ for (const entry of entries) {
6414
+ const row = document.createElement("div");
6415
+ row.className = "dsh-module-entry";
6416
+ row.dataset.clientModuleId = entry.id;
6417
+ const label = document.createElement("label");
6418
+ label.className = "dsh-module-choice";
6419
+ const input = document.createElement("input");
6420
+ input.type = "checkbox";
6421
+ input.checked = draft.isLoaded(entry.id);
6422
+ if (entry.required) input.dataset.required = "true";
6423
+ const name = document.createElement("span");
6424
+ const shortName = document.createElement("span");
6425
+ shortName.className = "dsh-module-choiceName";
6426
+ shortName.textContent = clientModuleDisplayName(entry.id);
6427
+ const selected = document.createElement("small");
6428
+ selected.textContent = entry.required ? copy.required : input.checked ? copy.loaded : copy.excluded;
6429
+ name.append(shortName, selected);
6430
+ label.append(input, name);
6431
+ row.append(label);
6432
+ const detail = document.createElement("details");
6433
+ detail.className = "dsh-module-entryDetails";
6434
+ const detailSummary = document.createElement("summary");
6435
+ detailSummary.textContent = copy.details;
6436
+ const id = paragraph(`${copy.moduleId}: `);
6437
+ const code = document.createElement("code");
6438
+ code.textContent = entry.id;
6439
+ id.append(code);
6440
+ detail.append(detailSummary, id, paragraph(entry.dependencies.length === 0 ? copy.noDependencies : `${copy.dependencies}${entry.dependencies.join(", ")}`));
6441
+ row.append(detail);
6442
+ input.addEventListener("change", () => {
6443
+ if (busy !== void 0 || draft === void 0) return;
6444
+ draft.setLoaded(entry.id, input.checked);
6445
+ input.checked = draft.isLoaded(entry.id);
6446
+ selected.textContent = entry.required ? copy.required : input.checked ? copy.loaded : copy.excluded;
6447
+ status.textContent = "";
6448
+ status.classList.remove("dsh-module-error");
6449
+ if (filterValue() !== "all") {
6450
+ renderList();
6451
+ filter.focus({ preventScroll: true });
6452
+ }
6453
+ updateControls();
6454
+ });
6455
+ choices.set(entry.id, input);
6456
+ content.append(row);
6457
+ }
6458
+ section.append(content);
6459
+ list.append(section);
5582
6460
  }
5583
- return parseClientModuleView(value);
6461
+ updateControls();
5584
6462
  };
5585
6463
  const render = (view) => {
5586
- if (disposed) return;
5587
- loaded = true;
5588
- choices.clear();
5589
- list.replaceChildren();
5590
- for (const entry of view.entries) {
5591
- const label = document.createElement("label");
5592
- label.className = "dsh-module-choice";
5593
- const input = document.createElement("input");
5594
- input.type = "checkbox";
5595
- input.checked = !view.excludedClientModules.includes(entry.id);
5596
- input.disabled = entry.required;
5597
- if (entry.required) input.dataset.required = "true";
5598
- const name = document.createElement("span");
5599
- name.textContent = entry.id;
5600
- const hint = document.createElement("small");
5601
- hint.textContent = entry.required ? copy.required : entry.dependencies.length === 0 ? "" : `${copy.dependencies}${entry.dependencies.join(", ")}`;
5602
- name.append(hint);
5603
- label.append(input, name);
5604
- list.append(label);
5605
- choices.set(entry.id, input);
5606
- }
5607
- status.classList.remove("dsh-module-error");
5608
- status.textContent = view.source === "device" ? copy.deviceSource : copy.computerSource;
6464
+ draft = new ClientModuleDraft(view.entries, view.excludedClientModules);
6465
+ source.textContent = view.source === "device" ? copy.deviceSource : view.source === "computer" ? copy.computerSource : copy.pluginSource;
6466
+ renderList();
5609
6467
  };
5610
- const failed = (error) => {
5611
- if (disposed || controller.signal.aborted) return;
6468
+ const failed = (error, mutation) => {
5612
6469
  status.classList.add("dsh-module-error");
5613
- status.textContent = error instanceof ClientModuleUiConflict ? `${copy.conflict}${error.detail}` : copy.failed;
6470
+ status.textContent = error instanceof ClientModuleUiConflict ? `${copy.conflict}${error.detail}` : error instanceof ClientModuleRequestTimeout ? error.mutation ? copy.writeTimeout : copy.readTimeout : mutation ? copy.saveFailed : copy.failed;
5614
6471
  status.focus({ preventScroll: true });
5615
6472
  };
5616
- const mutate = (body) => {
5617
- if (busy || !loaded) return;
5618
- setBusy(true);
5619
- request(body).then((view) => {
6473
+ const runRequest = (payload) => {
6474
+ if (disposed || busy !== void 0 || payload !== void 0 && draft === void 0) return;
6475
+ const ownGeneration = ++generation;
6476
+ busy = payload === void 0 ? "read" : "write";
6477
+ if (payload !== void 0) unconfirmedWrite = true;
6478
+ status.classList.remove("dsh-module-error");
6479
+ status.textContent = payload === void 0 ? copy.loading : copy.saving;
6480
+ updateControls();
6481
+ requestClientModuleView(scope, controller.signal, payload).then((view) => {
6482
+ if (disposed || ownGeneration !== generation) return;
6483
+ unconfirmedWrite = false;
5620
6484
  render(view);
5621
- if (!disposed) status.textContent = copy.saved;
5622
- }, failed).finally(() => {
5623
- if (!disposed) setBusy(false);
6485
+ status.textContent = payload === void 0 ? "" : copy.saved;
6486
+ }, (error) => {
6487
+ if (disposed || ownGeneration !== generation) return;
6488
+ if (error instanceof ClientModuleUiConflict) unconfirmedWrite = false;
6489
+ failed(error, payload !== void 0);
6490
+ }).finally(() => {
6491
+ if (disposed || ownGeneration !== generation) return;
6492
+ busy = void 0;
6493
+ updateControls();
5624
6494
  });
5625
6495
  };
6496
+ search.addEventListener("input", renderList);
6497
+ filter.addEventListener("change", renderList);
5626
6498
  save.addEventListener("click", () => {
5627
- mutate({ excludedClientModules: [...choices].filter(([, input]) => !input.checked).map(([id]) => id) });
6499
+ if ((draft?.changeCount ?? 0) > 0) runRequest({ excludedClientModules: draft?.excludedIds });
5628
6500
  });
5629
6501
  reset.addEventListener("click", () => {
5630
- if (window.confirm(copy.resetConfirm)) mutate({ reset: true });
6502
+ if (busy === void 0 && draft !== void 0 && window.confirm(copy.resetConfirm)) runRequest({ reset: true });
6503
+ });
6504
+ reread.addEventListener("click", () => {
6505
+ if (busy === void 0 && ((draft?.changeCount ?? 0) === 0 || window.confirm(copy.reloadConfirm))) runRequest();
5631
6506
  });
5632
- const onBack = (event) => {
5633
- if (!event.cancelable || !dialog.open || event.defaultPrevented) return;
5634
- event.preventDefault();
5635
- event.stopImmediatePropagation();
5636
- dispose();
5637
- };
5638
- window.addEventListener("dsh-mobile:native-back", onBack, true);
5639
6507
  const dispose = () => {
5640
6508
  if (disposed) return;
5641
6509
  disposed = true;
6510
+ ++generation;
5642
6511
  controller.abort();
5643
- openDialogs.delete(dispose);
6512
+ openDialogs.delete(openDialog);
5644
6513
  window.removeEventListener("dsh-mobile:native-back", onBack, true);
6514
+ window.removeEventListener("keydown", onEscape, true);
5645
6515
  if (dialog.open) dialog.close();
5646
6516
  dialog.remove();
5647
6517
  if (trigger?.isConnected) trigger.focus({ preventScroll: true });
5648
6518
  };
5649
- openDialogs.add(dispose);
6519
+ const requestClose = () => {
6520
+ if (disposed) return;
6521
+ if (unconfirmedWrite && !window.confirm(copy.savingCloseConfirm)) return;
6522
+ if (!unconfirmedWrite && (draft?.changeCount ?? 0) > 0 && !window.confirm(copy.discardConfirm)) return;
6523
+ dispose();
6524
+ };
6525
+ const onBack = (event) => {
6526
+ if (!event.cancelable || !dialog.open || event.defaultPrevented || [...openDialogs].at(-1) !== openDialog) return;
6527
+ event.preventDefault();
6528
+ event.stopImmediatePropagation();
6529
+ requestClose();
6530
+ };
6531
+ const onEscape = (event) => {
6532
+ if (event.key !== "Escape" || event.isComposing || event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented || !dialog.open || [...openDialogs].at(-1) !== openDialog) return;
6533
+ event.preventDefault();
6534
+ event.stopImmediatePropagation();
6535
+ requestClose();
6536
+ };
6537
+ const openDialog = {
6538
+ dispose,
6539
+ requestClose
6540
+ };
6541
+ openDialogs.add(openDialog);
6542
+ window.addEventListener("dsh-mobile:native-back", onBack, true);
6543
+ window.addEventListener("keydown", onEscape, true);
6544
+ dialog.addEventListener("cancel", (event) => {
6545
+ event.preventDefault();
6546
+ requestClose();
6547
+ });
5650
6548
  dialog.addEventListener("close", dispose, { once: true });
5651
- close.addEventListener("click", dispose);
6549
+ close.addEventListener("click", requestClose);
5652
6550
  document.body.append(dialog);
5653
6551
  dialog.showModal();
5654
- setBusy(true);
5655
- status.textContent = copy.loading;
5656
- request().then(render, failed).finally(() => {
5657
- if (!disposed) setBusy(false);
5658
- });
6552
+ runRequest();
5659
6553
  }
5660
6554
  /** Settings row used on mobile pages and on the computer's General settings.
5661
6555
  * @param props - Selection scope and current locale.
@@ -6161,42 +7055,29 @@ window.__ModuleLoader__.load({
6161
7055
  error: "Could not switch computers. Try again."
6162
7056
  };
6163
7057
  }
6164
- /** Render the native-app-only action in DSH's General settings row style. */
6165
- function MobileSwitchComputerRow() {
6166
- const [nativeReady, setNativeReady] = (0, react.useState)(false);
6167
- const [state, setState] = (0, react.useState)("idle");
7058
+ function useMobileNativeSettingsAction(action) {
7059
+ const [controller] = (0, react.useState)(() => new MobileNativeSettingsActionController(action, () => window.__DSH_MOBILE_NATIVE__));
7060
+ const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
6168
7061
  (0, react.useEffect)(() => {
6169
- let disposed = false;
6170
- const checkNativeAction = () => {
6171
- const bridge = window.__DSH_MOBILE_NATIVE__;
6172
- if (bridge === void 0) {
6173
- setNativeReady(false);
6174
- return;
6175
- }
6176
- Promise.resolve().then(() => bridge.capabilities()).then((capabilities) => {
6177
- if (!disposed) setNativeReady(capabilities.includes("mobile.switch-computer"));
6178
- }, () => {
6179
- if (!disposed) setNativeReady(false);
6180
- });
6181
- };
6182
- checkNativeAction();
6183
- window.addEventListener("dsh-mobile-native-ready", checkNativeAction);
7062
+ controller.start();
7063
+ window.addEventListener("dsh-mobile-native-ready", controller.refresh);
6184
7064
  return () => {
6185
- disposed = true;
6186
- window.removeEventListener("dsh-mobile-native-ready", checkNativeAction);
7065
+ window.removeEventListener("dsh-mobile-native-ready", controller.refresh);
7066
+ controller.stop();
6187
7067
  };
6188
- }, []);
6189
- if (!nativeReady) return null;
7068
+ }, [controller]);
7069
+ return {
7070
+ ...snapshot,
7071
+ invoke: controller.invoke
7072
+ };
7073
+ }
7074
+ /** Render the native-app-only action in DSH's General settings row style. */
7075
+ function MobileSwitchComputerRow() {
7076
+ const { supported, state, invoke } = useMobileNativeSettingsAction("mobile.switch-computer");
7077
+ if (!supported) return null;
6190
7078
  const locale = selectedMobileControlLocale();
6191
7079
  const copy = mobileSwitchComputerCopy(locale);
6192
- const invoke = () => {
6193
- const bridge = window.__DSH_MOBILE_NATIVE__;
6194
- if (bridge === void 0 || state === "busy") return;
6195
- setState("busy");
6196
- bridge.invoke("mobile.switch-computer", {}).catch(() => {
6197
- setState("error");
6198
- });
6199
- };
7080
+ const busy = state === "busy" || state === "success";
6200
7081
  return (0, react.createElement)("div", {
6201
7082
  className: "dsh-mobile-settings_row",
6202
7083
  lang: locale
@@ -6206,10 +7087,10 @@ window.__ModuleLoader__.load({
6206
7087
  }, state === "error" ? copy.error : copy.description)), (0, react.createElement)("button", {
6207
7088
  type: "button",
6208
7089
  className: "dsh-mobile-settings_selector",
6209
- disabled: state === "busy",
6210
- "aria-busy": state === "busy" ? "true" : void 0,
7090
+ disabled: busy,
7091
+ "aria-busy": busy ? "true" : void 0,
6211
7092
  onClick: invoke
6212
- }, state === "busy" ? copy.busy : state === "error" ? copy.retry : copy.action));
7093
+ }, busy ? copy.busy : state === "error" ? copy.retry : copy.action));
6213
7094
  }
6214
7095
  function mobileTaskNotificationCopy(locale) {
6215
7096
  if (locale === "zh") return {
@@ -6245,42 +7126,10 @@ window.__ModuleLoader__.load({
6245
7126
  }
6246
7127
  /** Render the Android-only notification permission entry in DSH General settings. */
6247
7128
  function MobileTaskNotificationRow() {
6248
- const [nativeReady, setNativeReady] = (0, react.useState)(false);
6249
- const [state, setState] = (0, react.useState)("idle");
6250
- (0, react.useEffect)(() => {
6251
- let disposed = false;
6252
- const checkNativeAction = () => {
6253
- const bridge = window.__DSH_MOBILE_NATIVE__;
6254
- if (bridge === void 0) {
6255
- setNativeReady(false);
6256
- return;
6257
- }
6258
- Promise.resolve().then(() => bridge.capabilities()).then((capabilities) => {
6259
- if (!disposed) setNativeReady(capabilities.includes("notification.settings"));
6260
- }, () => {
6261
- if (!disposed) setNativeReady(false);
6262
- });
6263
- };
6264
- checkNativeAction();
6265
- window.addEventListener("dsh-mobile-native-ready", checkNativeAction);
6266
- return () => {
6267
- disposed = true;
6268
- window.removeEventListener("dsh-mobile-native-ready", checkNativeAction);
6269
- };
6270
- }, []);
6271
- if (!nativeReady) return null;
7129
+ const { supported, state, invoke } = useMobileNativeSettingsAction("notification.settings");
7130
+ if (!supported) return null;
6272
7131
  const locale = selectedMobileControlLocale();
6273
7132
  const copy = mobileTaskNotificationCopy(locale);
6274
- const invoke = () => {
6275
- const bridge = window.__DSH_MOBILE_NATIVE__;
6276
- if (bridge === void 0 || state === "busy") return;
6277
- setState("busy");
6278
- bridge.invoke("notification.settings", {}).then(() => {
6279
- setState("success");
6280
- }, () => {
6281
- setState("error");
6282
- });
6283
- };
6284
7133
  return (0, react.createElement)("div", {
6285
7134
  className: "dsh-mobile-settings_row",
6286
7135
  lang: locale
@@ -6450,6 +7299,11 @@ window.__ModuleLoader__.load({
6450
7299
  cpolar_installer_missing: "cpolarExtractFailed",
6451
7300
  cpolar_executable_missing: "cpolarExtractFailed",
6452
7301
  cpolar_extract_failed: "cpolarExtractFailed",
7302
+ cpolar_extract_tools_failed: "cpolarExtractToolsFailed",
7303
+ cpolar_extract_archive_failed: "cpolarExtractArchiveFailed",
7304
+ cpolar_extract_database_failed: "cpolarExtractDatabaseFailed",
7305
+ cpolar_extract_cabinet_failed: "cpolarExtractCabinetFailed",
7306
+ cpolar_extract_file_failed: "cpolarExtractFileFailed",
6453
7307
  cpolar_storage_failed: "cpolarStorageFailed",
6454
7308
  cpolar_config_missing: "cpolarConfigMissing",
6455
7309
  cpolar_config_invalid: "cpolarConfigInvalid",
@@ -6531,6 +7385,11 @@ window.__ModuleLoader__.load({
6531
7385
  "cpolar_component_invalid",
6532
7386
  "cpolar_config_missing",
6533
7387
  "cpolar_config_invalid",
7388
+ "cpolar_extract_tools_failed",
7389
+ "cpolar_extract_archive_failed",
7390
+ "cpolar_extract_database_failed",
7391
+ "cpolar_extract_cabinet_failed",
7392
+ "cpolar_extract_file_failed",
6534
7393
  "funnel_state_invalid",
6535
7394
  "cpolar_start_timeout",
6536
7395
  "cpolar_stopped",
@@ -10588,73 +11447,12 @@ window.__ModuleLoader__.load({
10588
11447
  await navigator.clipboard.writeText(typeof input === "object" && input !== null && "text" in input ? String(input.text) : "");
10589
11448
  return { ok: true };
10590
11449
  }
10591
- if (action === "files.pick" || action === "camera.capture") {
10592
- const inputElement = element("input");
10593
- inputElement.type = "file";
10594
- inputElement.accept = action === "camera.capture" ? "image/*" : "*/*";
10595
- if (action === "camera.capture") inputElement.capture = "environment";
10596
- inputElement.hidden = true;
10597
- return new Promise((resolve, reject) => {
10598
- let settled = false;
10599
- let cleaned = false;
10600
- let cleanupTimer = 0;
10601
- let watchdogTimer = 0;
10602
- const cleanup = () => {
10603
- if (cleaned) return;
10604
- cleaned = true;
10605
- if (cleanupTimer !== 0) window.clearTimeout(cleanupTimer);
10606
- if (watchdogTimer !== 0) window.clearTimeout(watchdogTimer);
10607
- window.removeEventListener("focus", scheduleCancel);
10608
- document.removeEventListener("visibilitychange", onVisibilityChange);
10609
- signal.removeEventListener("abort", onAbort);
10610
- inputElement.removeEventListener("change", onChange);
10611
- inputElement.removeEventListener("cancel", onCancel);
10612
- inputElement.remove();
10613
- };
10614
- const finish = (callback) => {
10615
- if (settled) return;
10616
- settled = true;
10617
- cleanup();
10618
- callback();
10619
- };
10620
- const onChange = () => {
10621
- const file = inputElement.files?.[0];
10622
- finish(() => {
10623
- resolve(file);
10624
- });
10625
- };
10626
- const onCancel = () => {
10627
- finish(() => {
10628
- resolve(void 0);
10629
- });
10630
- };
10631
- const onAbort = () => {
10632
- finish(() => {
10633
- reject(abortReason());
10634
- });
10635
- };
10636
- const scheduleCancel = () => {
10637
- if (!settled && cleanupTimer === 0) cleanupTimer = window.setTimeout(onCancel, 1e3);
10638
- };
10639
- const onVisibilityChange = () => {
10640
- if (document.visibilityState === "visible") scheduleCancel();
10641
- };
10642
- inputElement.addEventListener("change", onChange, { once: true });
10643
- inputElement.addEventListener("cancel", onCancel, { once: true });
10644
- window.addEventListener("focus", scheduleCancel);
10645
- document.addEventListener("visibilitychange", onVisibilityChange);
10646
- signal.addEventListener("abort", onAbort, { once: true });
10647
- watchdogTimer = window.setTimeout(onCancel, 3e5);
10648
- try {
10649
- document.body.append(inputElement);
10650
- inputElement.click();
10651
- } catch (error) {
10652
- finish(() => {
10653
- reject(error);
10654
- });
10655
- }
10656
- });
10657
- }
11450
+ if (action === "files.pick" || action === "camera.capture") return pickBrowserFiles({
11451
+ signal,
11452
+ accept: action === "camera.capture" ? "image/*" : "*/*",
11453
+ ...action === "camera.capture" ? { capture: "environment" } : {},
11454
+ abortReason
11455
+ }).then((files) => files?.[0]);
10658
11456
  throw new Error("native capability is unavailable");
10659
11457
  };
10660
11458
  const makeApi = (id, hostGeneration, controller, surfaces, surfaceIds) => {