dsh-mobile 0.6.4 → 0.6.5

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, {
@@ -3236,6 +3251,8 @@ window.__ModuleLoader__.load({
3236
3251
  .dsh-mobile-settings_fontFamily > span { flex:1 1 100px; }
3237
3252
  .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
3253
  .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; }
3254
+ 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; }
3255
+ 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
3256
  /* The mobile row replaces the Host-backed font control, not the theme selector. */
3240
3257
  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
3258
  @media ${NATIVE_MOBILE_OVERLAY_QUERY} {
@@ -3305,8 +3322,6 @@ window.__ModuleLoader__.load({
3305
3322
  [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
3323
  [data-dsh-mobile-settings-list] [aria-current="true"] { border-color:transparent !important; outline:0 !important; box-shadow:none !important; }
3307
3324
  [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
3325
  [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
3326
  [data-dsh-mobile-settings-options] > * { width:100% !important; min-width:0 !important; }
3312
3327
  [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] > [class*="_row"] { flex-direction:column !important; align-items:stretch !important; gap:12px !important; }
@@ -4778,6 +4793,63 @@ window.__ModuleLoader__.load({
4778
4793
  };
4779
4794
  }
4780
4795
  //#endregion
4796
+ //#region src/mobile-font-capabilities.ts
4797
+ const NO_ADVANCED_MOBILE_TYPOGRAPHY = Object.freeze({
4798
+ codeSize: false,
4799
+ textFamily: false,
4800
+ codeFamily: false
4801
+ });
4802
+ /**
4803
+ * Probe computed fonts derived from DSH's public Markdown tokens. Input values are
4804
+ * restored synchronously before painting; merely defining the input variable is not support.
4805
+ * @param document - Live page document in the current browsing context.
4806
+ * @returns Independently observed advanced typography support.
4807
+ */
4808
+ function readMobileTypographyCapabilities(document) {
4809
+ const style = document.body.style;
4810
+ const text = document.createElement("span");
4811
+ const code = document.createElement("span");
4812
+ for (const probe of [text, code]) {
4813
+ probe.setAttribute("aria-hidden", "true");
4814
+ probe.style.cssText = "position:fixed;visibility:hidden;pointer-events:none;contain:strict;width:0;height:0;overflow:hidden";
4815
+ }
4816
+ text.style.font = "var(--dsw-font-markdown-base, 14px sans-serif)";
4817
+ code.style.font = "var(--dsw-font-markdown-code-block, 11px monospace)";
4818
+ document.body.append(text, code);
4819
+ const previous = /* @__PURE__ */ new Map();
4820
+ const sample = (property, value) => {
4821
+ if (!previous.has(property)) previous.set(property, {
4822
+ value: style.getPropertyValue(property),
4823
+ priority: style.getPropertyPriority(property)
4824
+ });
4825
+ style.setProperty(property, value);
4826
+ };
4827
+ try {
4828
+ sample("--dsh-code-font-size", "23px");
4829
+ const firstSize = getComputedStyle(code).fontSize;
4830
+ sample("--dsh-code-font-size", "24px");
4831
+ const secondSize = getComputedStyle(code).fontSize;
4832
+ sample("--dsh-font-family-text", "\"dsh-mobile-text-capability-a\"");
4833
+ const firstTextFamily = getComputedStyle(text).fontFamily;
4834
+ sample("--dsh-font-family-text", "\"dsh-mobile-text-capability-b\"");
4835
+ const secondTextFamily = getComputedStyle(text).fontFamily;
4836
+ sample("--dsh-font-family-code", "\"dsh-mobile-code-capability-a\"");
4837
+ const firstCodeFamily = getComputedStyle(code).fontFamily;
4838
+ sample("--dsh-font-family-code", "\"dsh-mobile-code-capability-b\"");
4839
+ const secondCodeFamily = getComputedStyle(code).fontFamily;
4840
+ return Object.freeze({
4841
+ codeSize: firstSize === "23px" && secondSize === "24px",
4842
+ textFamily: firstTextFamily.includes("dsh-mobile-text-capability-a") && secondTextFamily.includes("dsh-mobile-text-capability-b"),
4843
+ codeFamily: firstCodeFamily.includes("dsh-mobile-code-capability-a") && secondCodeFamily.includes("dsh-mobile-code-capability-b")
4844
+ });
4845
+ } finally {
4846
+ for (const [property, value] of previous) if (value.value === "") style.removeProperty(property);
4847
+ else style.setProperty(property, value.value, value.priority);
4848
+ text.remove();
4849
+ code.remove();
4850
+ }
4851
+ }
4852
+ //#endregion
4781
4853
  //#region src/mobile-font.ts
4782
4854
  /** Device-local typography for authenticated mobile pages, without Host settings writes. */
4783
4855
  const MOBILE_FONT_STORAGE_KEY = "dsh-mobile.content-font-size.v1";
@@ -4878,6 +4950,8 @@ window.__ModuleLoader__.load({
4878
4950
  storage;
4879
4951
  typography = defaultTypography();
4880
4952
  listeners = /* @__PURE__ */ new Set();
4953
+ capabilities = NO_ADVANCED_MOBILE_TYPOGRAPHY;
4954
+ capabilityListeners = /* @__PURE__ */ new Set();
4881
4955
  constructor(storage) {
4882
4956
  this.storage = storage;
4883
4957
  let legacySize = 16;
@@ -4897,6 +4971,19 @@ window.__ModuleLoader__.load({
4897
4971
  this.listeners.delete(listener);
4898
4972
  };
4899
4973
  };
4974
+ getCapabilitiesSnapshot = () => this.capabilities;
4975
+ subscribeCapabilities = (listener) => {
4976
+ this.capabilityListeners.add(listener);
4977
+ return () => {
4978
+ this.capabilityListeners.delete(listener);
4979
+ };
4980
+ };
4981
+ /** Update observed theme support without changing stored values or Host settings. */
4982
+ updateCapabilities(capabilities) {
4983
+ if (this.capabilities.codeSize === capabilities.codeSize && this.capabilities.textFamily === capabilities.textFamily && this.capabilities.codeFamily === capabilities.codeFamily) return;
4984
+ this.capabilities = Object.freeze({ ...capabilities });
4985
+ for (const listener of this.capabilityListeners) listener();
4986
+ }
4900
4987
  /** Apply a user-selected size locally; never send it to DSH's settings RPC. */
4901
4988
  set(size) {
4902
4989
  this.setRoleSize("text", size);
@@ -4931,6 +5018,23 @@ window.__ModuleLoader__.load({
4931
5018
  if (MOBILE_FONT_ROLES.every((role) => this.typography.sizes[role] === next.sizes[role] && this.typography.families[role] === "")) return;
4932
5019
  this.persist(next);
4933
5020
  }
5021
+ /** Reset visible supported controls while retaining preferences an older theme cannot consume. */
5022
+ resetSupported() {
5023
+ const defaults = defaultTypography();
5024
+ const sizes = {
5025
+ text: defaults.sizes.text,
5026
+ code: this.capabilities.codeSize ? defaults.sizes.code : this.typography.sizes.code
5027
+ };
5028
+ const families = {
5029
+ text: this.capabilities.textFamily ? "" : this.typography.families.text,
5030
+ code: this.capabilities.codeFamily ? "" : this.typography.families.code
5031
+ };
5032
+ if (MOBILE_FONT_ROLES.every((role) => this.typography.sizes[role] === sizes[role] && this.typography.families[role] === families[role])) return;
5033
+ this.persist({
5034
+ sizes: Object.freeze(sizes),
5035
+ families: Object.freeze(families)
5036
+ });
5037
+ }
4934
5038
  persist(typography) {
4935
5039
  this.typography = Object.freeze(typography);
4936
5040
  try {
@@ -4942,7 +5046,11 @@ window.__ModuleLoader__.load({
4942
5046
  for (const listener of this.listeners) listener();
4943
5047
  }
4944
5048
  };
4945
- /** Keep mobile typography independent from Host theme changes, including body-level portals. */
5049
+ /**
5050
+ * Keep mobile typography independent from Host theme changes, including body-level portals.
5051
+ * Recheck head-owned stylesheets and body presentation attributes, not conversation DOM updates.
5052
+ * Arbitrary stylesheets inserted into body are not observed.
5053
+ */
4946
5054
  function installMobileFontPreference() {
4947
5055
  let storage = null;
4948
5056
  try {
@@ -4958,11 +5066,26 @@ window.__ModuleLoader__.load({
4958
5066
  const owned = /* @__PURE__ */ new Map();
4959
5067
  const apply = () => {
4960
5068
  const typography = preference.getTypographySnapshot();
5069
+ const capabilities = preference.getCapabilitiesSnapshot();
4961
5070
  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
5071
  const current = style.getPropertyValue(property);
4963
5072
  const priority = style.getPropertyPriority(property);
4964
5073
  const last = owned.get(property);
4965
- if (last !== void 0 && (current !== last.value || priority !== last.priority)) previous.set(property, {
5074
+ if (!(property === roleSpecs[role].variable ? role === "text" || capabilities.codeSize : role === "text" ? capabilities.textFamily : capabilities.codeFamily)) {
5075
+ if (last !== void 0) {
5076
+ if (current === last.value && priority === last.priority) {
5077
+ const foreign = previous.get(property);
5078
+ if (foreign.value === "") style.removeProperty(property);
5079
+ else style.setProperty(property, foreign.value, foreign.priority);
5080
+ } else previous.set(property, {
5081
+ value: current,
5082
+ priority
5083
+ });
5084
+ owned.delete(property);
5085
+ }
5086
+ continue;
5087
+ }
5088
+ if (last === void 0 || current !== last.value || priority !== last.priority) previous.set(property, {
4966
5089
  value: current,
4967
5090
  priority
4968
5091
  });
@@ -4975,17 +5098,55 @@ window.__ModuleLoader__.load({
4975
5098
  else style.setProperty(property, value);
4976
5099
  }
4977
5100
  };
4978
- apply();
4979
- const unsubscribe = preference.subscribe(apply);
4980
- const observer = new MutationObserver(apply);
5101
+ let lastBodyState = "";
5102
+ const bodyState = () => `${style.cssText}|${document.body.className}|${document.body.getAttribute("data-ds-dark-theme")}`;
5103
+ const applyAndRemember = () => {
5104
+ apply();
5105
+ lastBodyState = bodyState();
5106
+ };
5107
+ const refresh = () => {
5108
+ preference.updateCapabilities(readMobileTypographyCapabilities(document));
5109
+ applyAndRemember();
5110
+ };
5111
+ refresh();
5112
+ const unsubscribe = preference.subscribe(applyAndRemember);
5113
+ const observer = new MutationObserver(() => {
5114
+ if (bodyState() !== lastBodyState) refresh();
5115
+ });
4981
5116
  observer.observe(document.body, {
4982
5117
  attributes: true,
4983
- attributeFilter: ["style"]
5118
+ attributeFilter: [
5119
+ "style",
5120
+ "class",
5121
+ "data-ds-dark-theme"
5122
+ ]
5123
+ });
5124
+ const themeObserver = new MutationObserver((records) => {
5125
+ const isSheet = (node) => node instanceof Element && (node.matches("style,link[rel=\"stylesheet\"]") || node.querySelector("style,link[rel=\"stylesheet\"]") !== null);
5126
+ if (records.some((record) => record.target instanceof Element && record.target.closest("style,link[rel=\"stylesheet\"]") !== null || [...record.addedNodes, ...record.removedNodes].some(isSheet))) refresh();
5127
+ });
5128
+ themeObserver.observe(document.head, {
5129
+ subtree: true,
5130
+ childList: true,
5131
+ characterData: true,
5132
+ attributes: true,
5133
+ attributeFilter: [
5134
+ "href",
5135
+ "media",
5136
+ "disabled",
5137
+ "rel"
5138
+ ]
4984
5139
  });
5140
+ const sheetLoaded = (event) => {
5141
+ if (event.target instanceof Element && event.target.matches("link[rel=\"stylesheet\"]")) refresh();
5142
+ };
5143
+ document.head.addEventListener("load", sheetLoaded, true);
4985
5144
  return {
4986
5145
  preference,
4987
5146
  dispose: () => {
4988
5147
  observer.disconnect();
5148
+ themeObserver.disconnect();
5149
+ document.head.removeEventListener("load", sheetLoaded, true);
4989
5150
  unsubscribe();
4990
5151
  for (const [property, value] of previous) {
4991
5152
  const last = owned.get(property);
@@ -5008,7 +5169,8 @@ window.__ModuleLoader__.load({
5008
5169
  familyHint: "填写设备已有的字体,多个名称用逗号分隔;留空使用默认字体。",
5009
5170
  codeSize: "代码字号",
5010
5171
  textFamily: "正文字体",
5011
- codeFamily: "代码字体"
5172
+ codeFamily: "代码字体",
5173
+ limited: "只显示当前 DSH 主题支持的字体选项;其他已保存的设置会保留。"
5012
5174
  },
5013
5175
  en: {
5014
5176
  title: "Mobile font size",
@@ -5021,7 +5183,8 @@ window.__ModuleLoader__.load({
5021
5183
  familyHint: "Use fonts already on your device, separated by commas. Leave empty for the default.",
5022
5184
  codeSize: "Code font size",
5023
5185
  textFamily: "Text font",
5024
- codeFamily: "Code font"
5186
+ codeFamily: "Code font",
5187
+ limited: "Only font options supported by the current DSH theme are shown. Other saved preferences are retained."
5025
5188
  },
5026
5189
  it: {
5027
5190
  title: "Dimensione testo mobile",
@@ -5034,7 +5197,8 @@ window.__ModuleLoader__.load({
5034
5197
  familyHint: "Usa caratteri presenti sul dispositivo, separati da virgole. Lascia vuoto per il predefinito.",
5035
5198
  codeSize: "Dimensione codice",
5036
5199
  textFamily: "Carattere testo",
5037
- codeFamily: "Carattere codice"
5200
+ codeFamily: "Carattere codice",
5201
+ limited: "Sono mostrate solo le opzioni supportate dal tema DSH corrente. Le altre preferenze salvate vengono mantenute."
5038
5202
  }
5039
5203
  };
5040
5204
  function FontFamilyInput({ preference, role, value, label, placeholder }) {
@@ -5062,6 +5226,7 @@ window.__ModuleLoader__.load({
5062
5226
  /** General settings row matching the existing Mobile controls and DSH theme tokens. */
5063
5227
  function MobileFontSizeRow({ preference, locale }) {
5064
5228
  const typography = (0, react.useSyncExternalStore)(preference.subscribe, preference.getTypographySnapshot);
5229
+ const capabilities = (0, react.useSyncExternalStore)(preference.subscribeCapabilities, preference.getCapabilitiesSnapshot);
5065
5230
  const copy = messages$1[locale];
5066
5231
  const controls = (role, title) => {
5067
5232
  const size = typography.sizes[role];
@@ -5092,18 +5257,22 @@ window.__ModuleLoader__.load({
5092
5257
  return (0, react.createElement)("div", {
5093
5258
  lang: locale,
5094
5259
  "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, {
5260
+ }, (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", {
5261
+ className: "dsh-mobile-settings_desc",
5262
+ "data-mobile-font-limited": true
5263
+ }, 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
5264
  key: role,
5097
5265
  preference,
5098
5266
  role,
5099
5267
  value: typography.families[role],
5100
5268
  label: role === "text" ? copy.textFamily : copy.codeFamily,
5101
5269
  placeholder: copy.defaultFamily
5102
- })), (0, react.createElement)("button", {
5270
+ }))), (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("button", {
5103
5271
  type: "button",
5104
5272
  className: "dsh-mobile-settings_selector",
5273
+ "data-mobile-font-reset": true,
5105
5274
  onClick: () => {
5106
- preference.reset();
5275
+ preference.resetSupported();
5107
5276
  }
5108
5277
  }, copy.reset)));
5109
5278
  }
@@ -5326,6 +5495,116 @@ window.__ModuleLoader__.load({
5326
5495
  }, copy.reset)));
5327
5496
  }
5328
5497
  //#endregion
5498
+ //#region src/mobile-native-settings-action.ts
5499
+ /** Own capability discovery and explicit calls; native actions retain the App's own timeout. */
5500
+ var MobileNativeSettingsActionController = class {
5501
+ action;
5502
+ getBridge;
5503
+ snapshot = Object.freeze({
5504
+ supported: false,
5505
+ state: "idle"
5506
+ });
5507
+ listeners = /* @__PURE__ */ new Set();
5508
+ bridge;
5509
+ active = false;
5510
+ generation = 0;
5511
+ probe = 0;
5512
+ attempt = 0;
5513
+ constructor(action, getBridge) {
5514
+ this.action = action;
5515
+ this.getBridge = getBridge;
5516
+ }
5517
+ getSnapshot = () => this.snapshot;
5518
+ subscribe = (listener) => {
5519
+ this.listeners.add(listener);
5520
+ return () => {
5521
+ this.listeners.delete(listener);
5522
+ };
5523
+ };
5524
+ start() {
5525
+ this.active = true;
5526
+ this.publish({
5527
+ supported: false,
5528
+ state: "idle"
5529
+ });
5530
+ this.refresh();
5531
+ }
5532
+ stop() {
5533
+ this.active = false;
5534
+ this.generation += 1;
5535
+ this.probe += 1;
5536
+ this.attempt += 1;
5537
+ this.bridge = void 0;
5538
+ }
5539
+ /** Recheck support without replaying or invalidating an in-flight call to the same adapter. */
5540
+ refresh = () => {
5541
+ if (!this.active) return;
5542
+ const bridge = this.getBridge();
5543
+ const probe = ++this.probe;
5544
+ if (bridge !== this.bridge) {
5545
+ this.bridge = bridge;
5546
+ this.generation += 1;
5547
+ this.attempt += 1;
5548
+ this.publish({
5549
+ supported: false,
5550
+ state: "idle"
5551
+ });
5552
+ }
5553
+ if (bridge === void 0) return;
5554
+ const generation = this.generation;
5555
+ const settle = (supported) => {
5556
+ if (!this.current(bridge, generation) || probe !== this.probe) return;
5557
+ if (!supported) this.attempt += 1;
5558
+ this.publish({
5559
+ supported,
5560
+ state: supported ? this.snapshot.state : "idle"
5561
+ });
5562
+ };
5563
+ Promise.resolve().then(() => {
5564
+ if (!this.current(bridge, generation) || probe !== this.probe) return void 0;
5565
+ return bridge.capabilities();
5566
+ }).then((capabilities) => {
5567
+ settle(capabilities?.includes(this.action) === true);
5568
+ }, () => {
5569
+ settle(false);
5570
+ });
5571
+ };
5572
+ /** Send at most one explicit action; late replies cannot update a replaced or unmounted row. */
5573
+ invoke = () => {
5574
+ const bridge = this.bridge;
5575
+ 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;
5576
+ const generation = this.generation;
5577
+ const attempt = ++this.attempt;
5578
+ this.publish({
5579
+ supported: true,
5580
+ state: "busy"
5581
+ });
5582
+ const current = () => this.current(bridge, generation) && attempt === this.attempt && this.snapshot.supported;
5583
+ Promise.resolve().then(() => {
5584
+ if (!current()) return void 0;
5585
+ return bridge.invoke(this.action, {});
5586
+ }).then(() => {
5587
+ if (current()) this.publish({
5588
+ supported: true,
5589
+ state: "success"
5590
+ });
5591
+ }, () => {
5592
+ if (current()) this.publish({
5593
+ supported: true,
5594
+ state: "error"
5595
+ });
5596
+ });
5597
+ };
5598
+ current(bridge, generation) {
5599
+ return this.active && generation === this.generation && this.bridge === bridge && this.getBridge() === bridge;
5600
+ }
5601
+ publish(snapshot) {
5602
+ if (snapshot.supported === this.snapshot.supported && snapshot.state === this.snapshot.state) return;
5603
+ this.snapshot = Object.freeze(snapshot);
5604
+ for (const listener of this.listeners) listener();
5605
+ }
5606
+ };
5607
+ //#endregion
5329
5608
  //#region src/local-admin-host.ts
5330
5609
  /** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
5331
5610
  const DESKTOP_ADMIN_HEADER = "x-dsh-mobile-desktop-admin";
@@ -5386,65 +5665,282 @@ window.__ModuleLoader__.load({
5386
5665
  return (protocol === "dsh-app:" ? hostname === "app" : (protocol === "" || protocol === "http:" || protocol === "https:") && isLocalAdminHostname(hostname)) && frontend !== "dedicated" && !new URLSearchParams(query).has("dsh-mobile-preview");
5387
5666
  }
5388
5667
  //#endregion
5668
+ //#region src/client-module-presentation.ts
5669
+ /** Group boot-protected entries before considering their package namespace.
5670
+ * @param entry - Catalog entry with server-owned required and dependency fields.
5671
+ * @returns Display group, never an authorization or compatibility decision.
5672
+ */
5673
+ function clientModuleGroup(entry) {
5674
+ if (entry.required) return "required";
5675
+ return entry.id.startsWith("@deepseek-ai/") ? "advanced" : "extensions";
5676
+ }
5677
+ /** Format the package leaf mechanically; the catalog has no product titles.
5678
+ * @param id - Exact module ID from the server catalog.
5679
+ * @returns Unscoped package words without guessed function or plugin ownership.
5680
+ */
5681
+ function clientModuleDisplayName(id) {
5682
+ const leaf = id.startsWith("@") && id.includes("/") ? id.slice(id.indexOf("/") + 1) : id;
5683
+ return (leaf.replace(/^dsh-(?:client-(?:ui-)?)?/u, "") || leaf || id).replace(/[-_]+/gu, " ");
5684
+ }
5685
+ function matchesQuery(id, query) {
5686
+ const needle = query.trim().toLowerCase();
5687
+ return needle === "" || `${id}\n${clientModuleDisplayName(id)}`.toLowerCase().includes(needle);
5688
+ }
5689
+ /** Match the visible title or exact ID and the current draft loading choice.
5690
+ * @param entry - Catalog entry to consider.
5691
+ * @param excluded - Current draft exclusions, not a runtime loading observation.
5692
+ * @param query - User-entered search text.
5693
+ * @param filter - All, selected for loading, or selected for omission.
5694
+ * @returns Whether the row belongs in the current filtered list.
5695
+ */
5696
+ function clientModuleMatches(entry, excluded, query, filter) {
5697
+ const omitted = !entry.required && excluded.has(entry.id);
5698
+ return matchesQuery(entry.id, query) && (filter === "all" || (filter === "excluded" ? omitted : !omitted));
5699
+ }
5700
+ /** Editable selections retain unknown server exclusions and never cascade dependencies. */
5701
+ var ClientModuleDraft = class {
5702
+ entries;
5703
+ initial;
5704
+ excluded;
5705
+ entriesById;
5706
+ constructor(entries, excludedIds) {
5707
+ this.entries = entries;
5708
+ this.initial = new Set(excludedIds);
5709
+ this.excluded = new Set(excludedIds);
5710
+ this.entriesById = new Map(entries.map((entry) => [entry.id, entry]));
5711
+ }
5712
+ /** @returns Number of changed loading choices, including hidden filtered rows. */
5713
+ get changeCount() {
5714
+ let count = 0;
5715
+ for (const id of this.initial) if (!this.excluded.has(id)) count += 1;
5716
+ for (const id of this.excluded) if (!this.initial.has(id)) count += 1;
5717
+ return count;
5718
+ }
5719
+ /** @returns A snapshot of exclusions suitable for authenticated saving. */
5720
+ get excludedIds() {
5721
+ return [...this.excluded];
5722
+ }
5723
+ /** @returns Retained exclusions that cannot be saved against the current catalog. */
5724
+ get invalidExclusions() {
5725
+ return [...this.excluded].flatMap((id) => {
5726
+ const entry = this.entriesById.get(id);
5727
+ return entry === void 0 ? [{
5728
+ id,
5729
+ reason: "missing"
5730
+ }] : entry.required ? [{
5731
+ id,
5732
+ reason: "required"
5733
+ }] : [];
5734
+ });
5735
+ }
5736
+ /** Explicitly remove an old exclusion; ordinary optional choices use setLoaded.
5737
+ * @param id - Exact ID of a retained missing or now-required exclusion.
5738
+ * @returns Whether this draft changed; no server request is made.
5739
+ */
5740
+ clearInvalidExclusion(id) {
5741
+ const entry = this.entriesById.get(id);
5742
+ if (!this.initial.has(id) || entry !== void 0 && !entry.required) return false;
5743
+ return this.excluded.delete(id);
5744
+ }
5745
+ /** @returns Whether a catalog entry is selected for loading, not already loaded. */
5746
+ isLoaded(id) {
5747
+ return this.entriesById.get(id)?.required === true || !this.excluded.has(id);
5748
+ }
5749
+ /** Change one optional choice; required and unknown entries cannot be edited.
5750
+ * @param id - Exact catalog ID.
5751
+ * @param loaded - Whether to load the entry on the next open.
5752
+ */
5753
+ setLoaded(id, loaded) {
5754
+ const entry = this.entriesById.get(id);
5755
+ if (entry === void 0 || entry.required) return;
5756
+ if (loaded) this.excluded.delete(id);
5757
+ else this.excluded.add(id);
5758
+ }
5759
+ /** Filter a draft without changing hidden choices.
5760
+ * @param query - Search input.
5761
+ * @param filter - Requested loading selection.
5762
+ * @returns Catalog entries in their original stable order.
5763
+ */
5764
+ matchingEntries(query, filter) {
5765
+ return this.entries.filter((entry) => clientModuleMatches(entry, this.excluded, query, filter));
5766
+ }
5767
+ /** Find invalid old exclusions by name or ID, independently from the protected catalog row.
5768
+ * @param query - Search input.
5769
+ * @param filter - Old exclusions appear in all and excluded selections, not loaded.
5770
+ * @returns Invalid draft exclusions in their retained order.
5771
+ */
5772
+ matchingInvalidExclusions(query, filter) {
5773
+ return filter === "loaded" ? [] : this.invalidExclusions.filter((entry) => matchesQuery(entry.id, query));
5774
+ }
5775
+ };
5776
+ //#endregion
5389
5777
  //#region src/client-module-ui.ts
5390
5778
  /** Device-local module selection and the computer's defaults share a read-only catalog. */
5391
5779
  const CLIENT_MODULE_COPY = {
5392
5780
  zh: {
5393
5781
  title: "移动页面模块",
5394
- computer: "为移动访问设置默认加载模块,不卸载电脑上的插件。",
5395
- device: "只为本设备选择加载模块,不改变其他手机或电脑。",
5782
+ computer: "设置此电脑的移动访问默认项;电脑插件不变。",
5783
+ device: "仅本设备,下次打开生效;电脑插件不变。",
5396
5784
  open: "管理",
5785
+ help: "加载规则",
5397
5786
  loading: "正在读取模块…",
5398
- hint: "勾选表示加载。启动必需的模块不能取消;依赖冲突会在保存时说明。",
5399
- save: "保存,下次打开生效",
5787
+ hint: "勾选表示下次打开时加载。启动必需项受保护;依赖冲突会在保存时说明,不会自动取消其他模块。",
5788
+ save: "保存选择",
5400
5789
  reset: "恢复默认",
5401
5790
  close: "关闭",
5402
5791
  required: "启动必需",
5403
5792
  dependencies: "依赖:",
5404
- saved: "已保存,不会自动刷新当前页面。重新打开 DSH 后生效。",
5405
- failed: "暂时无法完成。请确认移动访问已启用,或检查下面的依赖冲突。",
5793
+ noDependencies: "无声明的前端依赖",
5794
+ details: "模块详情",
5795
+ moduleId: "模块 ID",
5796
+ saved: "已保存,下次打开 DSH 生效。不会自动刷新当前页面或丢弃草稿。",
5797
+ failed: "暂时无法读取模块。请确认移动访问已启用后重试。",
5798
+ saveFailed: "未确认保存成功,已保留你的选择。可重新保存,或重新读取已保存设置。",
5406
5799
  conflict: "无法保存:",
5407
- computerSource: "使用电脑默认设置",
5408
- deviceSource: "使用本设备设置",
5409
- resetConfirm: "恢复默认模块选择?不会重新加载当前会话。"
5800
+ computerSource: "当前来源:电脑默认设置",
5801
+ deviceSource: "当前来源:本设备设置",
5802
+ pluginSource: "当前来源:插件默认设置",
5803
+ resetConfirm: "恢复默认模块选择?仅下次打开生效,不会重新加载当前会话。",
5804
+ discardConfirm: "放弃未保存的模块选择并关闭?",
5805
+ reloadConfirm: "重新读取会替换未保存的选择。继续吗?",
5806
+ savingCloseConfirm: "保存结果尚未确认。现在关闭不会撤销可能已完成的保存,确定关闭吗?",
5807
+ search: "搜索模块",
5808
+ searchPlaceholder: "搜索名称或模块 ID",
5809
+ filter: "加载选择",
5810
+ all: "全部",
5811
+ loaded: "加载",
5812
+ excluded: "不加载",
5813
+ empty: "没有符合条件的模块。",
5814
+ clearFilters: "清除筛选",
5815
+ extensions: "可选扩展",
5816
+ extensionsHint: "可选择不加载的前端模块;一项模块不一定等于一个完整插件。",
5817
+ advanced: "DSH 可选组件(高级)",
5818
+ advancedHint: "DSH 的可选前端组件。只修改你了解的项目。",
5819
+ requiredGroup: "启动必需项",
5820
+ requiredHint: "这些模块保证页面启动,不能关闭。",
5821
+ changes: "未保存:{count} 项改动",
5822
+ unchanged: "没有未保存的改动",
5823
+ results: "匹配 {visible} / {total} 项",
5824
+ retry: "重新读取",
5825
+ saving: "正在保存…",
5826
+ readTimeout: "读取超时。当前页面和已保存设置未变,请重试。",
5827
+ writeTimeout: "保存超时,结果尚未确认。保留了你的选择;请重新读取确认,不会自动重试保存。",
5828
+ nextOpen: "仅下次打开生效",
5829
+ invalidSelections: "失效的旧选择",
5830
+ invalidHint: "这些旧排除已与当前模块目录不符。移除后仍需保存选择。",
5831
+ invalidMissing: "当前模块目录中不存在。",
5832
+ invalidRequired: "当前已是启动必需项,不能排除。",
5833
+ clearInvalid: "移除旧排除"
5410
5834
  },
5411
5835
  en: {
5412
5836
  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.",
5837
+ computer: "Mobile defaults for this computer. Desktop plugins are unchanged.",
5838
+ device: "For this device’s next open only. Computer plugins are unchanged.",
5415
5839
  open: "Manage",
5840
+ help: "Loading rules",
5416
5841
  loading: "Reading modules…",
5417
- hint: "Checked means loaded. Required boot modules cannot be unchecked; saving explains dependency conflicts.",
5418
- save: "Save for next open",
5842
+ hint: "Checked means load on the next open. Required entries are protected; saving explains dependency conflicts without unchecking other modules.",
5843
+ save: "Save selection",
5419
5844
  reset: "Restore defaults",
5420
5845
  close: "Close",
5421
5846
  required: "Required for boot",
5422
5847
  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.",
5848
+ noDependencies: "No declared frontend dependencies",
5849
+ details: "Module details",
5850
+ moduleId: "Module ID",
5851
+ saved: "Saved for the next DSH open. This page is not refreshed and its draft is kept.",
5852
+ failed: "Could not read modules. Enable mobile access and try again.",
5853
+ saveFailed: "Saving was not confirmed. Your choices are kept. Save again or reread the saved settings.",
5425
5854
  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."
5855
+ computerSource: "Source: computer defaults",
5856
+ deviceSource: "Source: this device’s settings",
5857
+ pluginSource: "Source: plugin defaults",
5858
+ resetConfirm: "Restore default selections for the next open? The current conversation will not reload.",
5859
+ discardConfirm: "Discard unsaved module choices and close?",
5860
+ reloadConfirm: "Rereading replaces unsaved choices. Continue?",
5861
+ savingCloseConfirm: "Saving is not confirmed. Closing will not undo a save that may have completed. Close anyway?",
5862
+ search: "Search modules",
5863
+ searchPlaceholder: "Search name or module ID",
5864
+ filter: "Loading selection",
5865
+ all: "All",
5866
+ loaded: "Load",
5867
+ excluded: "Do not load",
5868
+ empty: "No modules match these filters.",
5869
+ clearFilters: "Clear filters",
5870
+ extensions: "Optional extensions",
5871
+ extensionsHint: "Optional frontend modules; one module is not necessarily a complete plugin.",
5872
+ advanced: "DSH optional components (advanced)",
5873
+ advancedHint: "Optional DSH frontend components. Change only entries you understand.",
5874
+ requiredGroup: "Required for boot",
5875
+ requiredHint: "These modules keep the page bootable and cannot be disabled.",
5876
+ changes: "Unsaved: {count} changes",
5877
+ unchanged: "No unsaved changes",
5878
+ results: "Matching {visible} / {total}",
5879
+ retry: "Reread settings",
5880
+ saving: "Saving…",
5881
+ readTimeout: "Reading timed out. This page and saved settings are unchanged; try again.",
5882
+ writeTimeout: "Saving timed out and its result is unconfirmed. Your choices are kept. Reread to confirm; saving is not retried automatically.",
5883
+ nextOpen: "Applies on next open only",
5884
+ invalidSelections: "Outdated selections",
5885
+ invalidHint: "These old exclusions no longer match the module catalog. Save your choices after removing them.",
5886
+ invalidMissing: "Not present in the current module catalog.",
5887
+ invalidRequired: "Now required for boot and cannot be excluded.",
5888
+ clearInvalid: "Remove old exclusion"
5429
5889
  },
5430
5890
  it: {
5431
5891
  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.",
5892
+ computer: "Predefiniti mobile del computer; i plugin non cambiano.",
5893
+ device: "Solo alla prossima apertura su questo dispositivo; i plugin del computer non cambiano.",
5434
5894
  open: "Gestisci",
5895
+ help: "Regole di caricamento",
5435
5896
  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",
5897
+ hint: "Selezionato significa caricare alla prossima apertura. I moduli obbligatori sono protetti; il salvataggio mostra i conflitti senza deselezionare altri moduli.",
5898
+ save: "Salva selezione",
5438
5899
  reset: "Ripristina predefiniti",
5439
5900
  close: "Chiudi",
5440
5901
  required: "Obbligatorio",
5441
5902
  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.",
5903
+ noDependencies: "Nessuna dipendenza frontend dichiarata",
5904
+ details: "Dettagli modulo",
5905
+ moduleId: "ID modulo",
5906
+ saved: "Salvato per la prossima apertura di DSH. La pagina non si aggiorna e la bozza resta.",
5907
+ failed: "Impossibile leggere i moduli. Abilita accesso mobile e riprova.",
5908
+ saveFailed: "Salvataggio non confermato. Le scelte restano; salva di nuovo o rileggi le impostazioni salvate.",
5444
5909
  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."
5910
+ computerSource: "Origine: predefiniti del computer",
5911
+ deviceSource: "Origine: impostazioni del dispositivo",
5912
+ pluginSource: "Origine: predefiniti del plugin",
5913
+ resetConfirm: "Ripristinare i predefiniti per la prossima apertura? La conversazione attuale non verrà ricaricata.",
5914
+ discardConfirm: "Scartare le scelte non salvate e chiudere?",
5915
+ reloadConfirm: "Rileggere sostituisce le scelte non salvate. Continuare?",
5916
+ savingCloseConfirm: "Salvataggio non confermato. Chiudere non annulla un salvataggio già completato. Chiudere comunque?",
5917
+ search: "Cerca moduli",
5918
+ searchPlaceholder: "Cerca nome o ID modulo",
5919
+ filter: "Selezione caricamento",
5920
+ all: "Tutti",
5921
+ loaded: "Carica",
5922
+ excluded: "Non caricare",
5923
+ empty: "Nessun modulo corrisponde ai filtri.",
5924
+ clearFilters: "Cancella filtri",
5925
+ extensions: "Estensioni facoltative",
5926
+ extensionsHint: "Moduli frontend facoltativi; un modulo non è necessariamente un plugin completo.",
5927
+ advanced: "Componenti DSH facoltativi (avanzate)",
5928
+ advancedHint: "Componenti frontend DSH facoltativi. Modifica solo quelli che conosci.",
5929
+ requiredGroup: "Moduli obbligatori",
5930
+ requiredHint: "Questi moduli permettono l’avvio e non possono essere disattivati.",
5931
+ changes: "Non salvate: {count} modifiche",
5932
+ unchanged: "Nessuna modifica non salvata",
5933
+ results: "Corrispondenti {visible} / {total}",
5934
+ retry: "Rileggi impostazioni",
5935
+ saving: "Salvataggio…",
5936
+ readTimeout: "Lettura scaduta. Pagina e impostazioni salvate non cambiano; riprova.",
5937
+ writeTimeout: "Salvataggio scaduto e non confermato. Le scelte restano. Rileggi per confermare; nessun tentativo automatico.",
5938
+ nextOpen: "Solo alla prossima apertura",
5939
+ invalidSelections: "Scelte obsolete",
5940
+ invalidHint: "Queste esclusioni precedenti non corrispondono più al catalogo. Salva le scelte dopo averle rimosse.",
5941
+ invalidMissing: "Assente dal catalogo attuale dei moduli.",
5942
+ invalidRequired: "Ora obbligatorio per l’avvio e non può essere escluso.",
5943
+ clearInvalid: "Rimuovi esclusione precedente"
5448
5944
  }
5449
5945
  };
5450
5946
  var ClientModuleUiConflict = class extends Error {
@@ -5454,29 +5950,40 @@ window.__ModuleLoader__.load({
5454
5950
  this.detail = detail;
5455
5951
  }
5456
5952
  };
5953
+ /** A deadline does not imply that a POST was rolled back by the server. */
5954
+ var ClientModuleRequestTimeout = class extends Error {
5955
+ mutation;
5956
+ constructor(mutation) {
5957
+ super("client_module_request_timeout");
5958
+ this.mutation = mutation;
5959
+ }
5960
+ };
5961
+ const CLIENT_MODULE_REQUEST_TIMEOUT_MS = {
5962
+ read: 15e3,
5963
+ write: 2e4
5964
+ };
5457
5965
  /** Validate catalog response fields before creating interactive controls.
5458
5966
  * @param value - Gateway or computer-admin JSON response.
5459
5967
  * @returns The fields used by the module-selection dialog.
5460
5968
  */
5461
5969
  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");
5970
+ 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");
5971
+ const entries = value.entries.map((entry) => {
5972
+ 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");
5973
+ const dependencies = entry.dependencies.map((dependency) => {
5974
+ if (typeof dependency !== "string") throw new Error("invalid_module_catalog");
5975
+ return dependency;
5976
+ });
5977
+ if (entry.id.trim() === "") throw new Error("invalid_module_catalog");
5978
+ return {
5979
+ id: entry.id,
5980
+ required: entry.required,
5981
+ dependencies
5982
+ };
5983
+ });
5984
+ if (new Set(entries.map((entry) => entry.id)).size !== entries.length) throw new Error("invalid_module_catalog");
5467
5985
  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
- }),
5986
+ entries,
5480
5987
  excludedClientModules: value.excludedClientModules.map((id) => {
5481
5988
  if (typeof id !== "string") throw new Error("invalid_module_catalog");
5482
5989
  return id;
@@ -5484,32 +5991,103 @@ window.__ModuleLoader__.load({
5484
5991
  source: value.source === "device" ? "device" : value.source === "computer" ? "computer" : "plugin"
5485
5992
  };
5486
5993
  }
5994
+ /** Read or explicitly write module choices with a bounded, cancellable operation.
5995
+ * @param scope - Computer-admin endpoint or authenticated device endpoint.
5996
+ * @param signal - Owning dialog lifetime; cancellation cannot undo a completed write.
5997
+ * @param body - Omit for a read; supplied fields are sent once without automatic replay.
5998
+ * @returns Validated catalog fields returned by the selected endpoint.
5999
+ */
6000
+ async function requestClientModuleView(scope, signal, body) {
6001
+ if (signal.aborted) throw new DOMException("Dialog closed", "AbortError");
6002
+ const controller = new AbortController();
6003
+ let rejectCancellation = () => {};
6004
+ const cancellation = new Promise((_, reject) => {
6005
+ rejectCancellation = reject;
6006
+ });
6007
+ const abort = () => {
6008
+ controller.abort();
6009
+ rejectCancellation(new DOMException("Dialog closed", "AbortError"));
6010
+ };
6011
+ signal.addEventListener("abort", abort, { once: true });
6012
+ const timer = setTimeout(() => {
6013
+ rejectCancellation(new ClientModuleRequestTimeout(body !== void 0));
6014
+ controller.abort();
6015
+ }, body === void 0 ? CLIENT_MODULE_REQUEST_TIMEOUT_MS.read : CLIENT_MODULE_REQUEST_TIMEOUT_MS.write);
6016
+ const operation = async () => {
6017
+ const init = {
6018
+ method: body === void 0 ? "GET" : "POST",
6019
+ signal: controller.signal,
6020
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
6021
+ };
6022
+ init.headers = scope === "computer" ? localAdminRequestHeaders(init, location) : { "content-type": "application/json" };
6023
+ if (scope === "device" && body !== void 0) {
6024
+ const cookies = document.cookie.split(";").map((value) => value.trim());
6025
+ const csrfCookie = cookies.find((value) => value.startsWith("__Host-dsh_ma_csrf=")) ?? cookies.find((value) => value.startsWith("dsh_ma_csrf="));
6026
+ if (csrfCookie !== void 0) {
6027
+ const headers = new Headers(init.headers);
6028
+ headers.set("x-dsh-mobile-csrf", csrfCookie.slice(csrfCookie.indexOf("=") + 1));
6029
+ init.headers = headers;
6030
+ }
6031
+ }
6032
+ const response = await fetch(scope === "computer" ? "/api/mobile-access/client-modules" : "/mobile-access/client-modules", init);
6033
+ const value = await response.json();
6034
+ if (!response.ok) {
6035
+ const detail = typeof value === "object" && value !== null && "detail" in value && typeof value.detail === "string" ? value.detail.slice(0, 512) : "";
6036
+ if (response.status === 409 && detail !== "" && typeof value === "object" && value !== null && "error" in value && value.error === "excluded_client_modules_invalid") throw new ClientModuleUiConflict(detail);
6037
+ throw new Error("client_module_request_failed");
6038
+ }
6039
+ return parseClientModuleView(value);
6040
+ };
6041
+ try {
6042
+ return await Promise.race([operation(), cancellation]);
6043
+ } finally {
6044
+ clearTimeout(timer);
6045
+ signal.removeEventListener("abort", abort);
6046
+ }
6047
+ }
5487
6048
  const CLIENT_MODULE_STYLES = `
5488
6049
  .dsh-module-row{display:flex;align-items:center;gap:8px;padding:16px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}
5489
6050
  .dsh-module-rowText{flex:1;min-width:0}.dsh-module-title{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}
5490
6051
  .dsh-module-description{margin-top:4px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
5491
6052
  .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
6053
  .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}
6054
+ .dsh-module-button:not(:disabled):active{background:var(--dsw-alias-interactive-bg-active)}
6055
+ .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}
6056
+ .dsh-module-dialog[open]{display:flex;flex-direction:column}.dsh-module-dialog::backdrop{background:rgb(15 23 42 / 40%)}
6057
+ .dsh-module-header{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
6058
+ .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}
6059
+ .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}
6060
+ .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))}
6061
+ .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}
6062
+ .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)}
6063
+ .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}
6064
+ .dsh-module-dialog :is(input,select,summary):focus-visible{outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb);outline-offset:2px}
6065
+ .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}
6066
+ .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}
6067
+ .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}
6068
+ .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}
6069
+ .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))}
6070
+ .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}
6071
+ .dsh-module-entryDetails p,.dsh-module-entryDetails code{overflow-wrap:anywhere;font-size:13px}.dsh-module-entryDetails code{white-space:normal}
6072
+ .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)}
6073
+ .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}
6074
+ .dsh-module-button:disabled{opacity:.5;cursor:default}.dsh-module-save{background:var(--dsw-alias-bg-module-platform);font-weight:600}
6075
+ .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}
6076
+ @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
6077
  .dsh-module-error{color:var(--dsw-alias-state-error-primary,#b42318)}
5500
6078
  `;
5501
6079
  const openDialogs = /* @__PURE__ */ new Set();
5502
6080
  /** Cancel pending dialogs when the owning plugin generation is disposed. */
5503
6081
  function disposeClientModuleDialogs() {
5504
- for (const dispose of [...openDialogs]) dispose();
6082
+ for (const dialog of [...openDialogs]) dialog.dispose();
5505
6083
  }
5506
6084
  /** Close the foreground module modal before navigating its underlying DSH settings.
5507
6085
  * @returns Whether an open module modal consumed the return action.
5508
6086
  */
5509
6087
  function closeClientModuleDialog() {
5510
- const dispose = [...openDialogs].at(-1);
5511
- if (dispose === void 0) return false;
5512
- dispose();
6088
+ const dialog = [...openDialogs].at(-1);
6089
+ if (dialog === void 0) return false;
6090
+ dialog.requestClose();
5513
6091
  return true;
5514
6092
  }
5515
6093
  /** Open one modal; closing cancels reads and restores focus without reloading the page.
@@ -5519,143 +6097,338 @@ window.__ModuleLoader__.load({
5519
6097
  */
5520
6098
  function openClientModuleSettings(scope, locale, trigger) {
5521
6099
  const copy = CLIENT_MODULE_COPY[locale];
5522
- const endpoint = scope === "computer" ? "/api/mobile-access/client-modules" : "/mobile-access/client-modules";
5523
6100
  const controller = new AbortController();
5524
6101
  const dialog = document.createElement("dialog");
5525
6102
  dialog.className = "dsh-module-dialog";
5526
6103
  dialog.setAttribute("aria-label", copy.title);
6104
+ dialog.lang = locale;
6105
+ const button = (text) => {
6106
+ const result = document.createElement("button");
6107
+ result.type = "button";
6108
+ result.className = "dsh-module-button";
6109
+ result.textContent = text;
6110
+ return result;
6111
+ };
6112
+ const paragraph = (text, className = "") => {
6113
+ const result = document.createElement("p");
6114
+ result.textContent = text;
6115
+ result.className = className;
6116
+ return result;
6117
+ };
6118
+ const header = document.createElement("div");
6119
+ header.className = "dsh-module-header";
5527
6120
  const title = document.createElement("h2");
5528
6121
  title.textContent = copy.title;
5529
- const intro = document.createElement("p");
5530
- intro.textContent = `${copy[scope]} ${copy.hint}`;
5531
- const status = document.createElement("p");
6122
+ const close = button(copy.close);
6123
+ header.append(title, close);
6124
+ const body = document.createElement("div");
6125
+ body.className = "dsh-module-body";
6126
+ const intro = paragraph(copy[scope]);
6127
+ const hint = paragraph(copy.hint, "dsh-module-hint");
6128
+ const help = document.createElement("details");
6129
+ help.className = "dsh-module-help";
6130
+ const helpTitle = document.createElement("summary");
6131
+ helpTitle.textContent = copy.help;
6132
+ help.append(helpTitle, hint);
6133
+ const source = paragraph("", "dsh-module-source");
6134
+ const status = paragraph("", "dsh-module-notice");
5532
6135
  status.setAttribute("role", "status");
5533
6136
  status.setAttribute("aria-live", "polite");
5534
6137
  status.tabIndex = -1;
6138
+ const tools = document.createElement("div");
6139
+ tools.className = "dsh-module-tools";
6140
+ const searchLabel = document.createElement("label");
6141
+ searchLabel.className = "dsh-module-field";
6142
+ searchLabel.textContent = copy.search;
6143
+ const search = document.createElement("input");
6144
+ search.type = "search";
6145
+ search.placeholder = copy.searchPlaceholder;
6146
+ search.setAttribute("aria-label", copy.search);
6147
+ searchLabel.append(search);
6148
+ const filterLabel = document.createElement("label");
6149
+ filterLabel.className = "dsh-module-field";
6150
+ filterLabel.textContent = copy.filter;
6151
+ const filter = document.createElement("select");
6152
+ filter.dataset.clientModuleFilter = "true";
6153
+ filter.setAttribute("aria-label", copy.filter);
6154
+ for (const value of [
6155
+ "all",
6156
+ "loaded",
6157
+ "excluded"
6158
+ ]) {
6159
+ const option = document.createElement("option");
6160
+ option.value = value;
6161
+ option.textContent = copy[value];
6162
+ filter.append(option);
6163
+ }
6164
+ filterLabel.append(filter);
6165
+ tools.append(searchLabel, filterLabel);
6166
+ const results = paragraph("", "dsh-module-results");
6167
+ results.setAttribute("aria-live", "polite");
5535
6168
  const list = document.createElement("div");
5536
6169
  list.className = "dsh-module-list";
6170
+ const footer = document.createElement("div");
6171
+ footer.className = "dsh-module-footer";
6172
+ const changes = paragraph(copy.unchanged);
6173
+ changes.setAttribute("role", "status");
6174
+ changes.setAttribute("aria-live", "polite");
5537
6175
  const actions = document.createElement("div");
5538
6176
  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
6177
  const save = button(copy.save);
6178
+ save.classList.add("dsh-module-save");
5547
6179
  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);
6180
+ const reread = button(copy.retry);
6181
+ actions.append(save, reset, reread);
6182
+ footer.append(changes, status, actions);
6183
+ body.append(intro, source, tools, results, list, help);
6184
+ dialog.append(header, body, footer);
5551
6185
  let disposed = false;
5552
- let busy = false;
5553
- let loaded = false;
6186
+ let busy;
6187
+ let generation = 0;
6188
+ let draft;
6189
+ let unconfirmedWrite = false;
6190
+ const expansion = /* @__PURE__ */ new Map([
6191
+ ["extensions", true],
6192
+ ["advanced", false],
6193
+ ["required", false]
6194
+ ]);
5554
6195
  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;
6196
+ const invalidActions = /* @__PURE__ */ new Map();
6197
+ const filterValue = () => filter.value === "loaded" ? "loaded" : filter.value === "excluded" ? "excluded" : "all";
6198
+ const updateControls = () => {
6199
+ const changed = draft?.changeCount ?? 0;
6200
+ save.disabled = busy !== void 0 || changed === 0;
6201
+ reset.disabled = busy !== void 0 || draft === void 0;
6202
+ reread.disabled = busy !== void 0;
6203
+ search.disabled = busy !== void 0 || draft === void 0;
6204
+ filter.disabled = search.disabled;
6205
+ save.textContent = busy === "write" ? copy.saving : copy.save;
6206
+ changes.textContent = `${changed === 0 ? copy.unchanged : copy.changes.replace("{count}", String(changed))} · ${copy.nextOpen}`;
6207
+ list.setAttribute("aria-busy", String(busy !== void 0));
6208
+ for (const input of choices.values()) input.disabled = busy !== void 0 || input.dataset.required === "true";
6209
+ for (const action of invalidActions.values()) action.disabled = busy !== void 0;
6210
+ };
6211
+ const renderList = () => {
6212
+ choices.clear();
6213
+ invalidActions.clear();
6214
+ list.replaceChildren();
6215
+ if (draft === void 0) {
6216
+ results.textContent = "";
6217
+ return;
6218
+ }
6219
+ const visible = draft.matchingEntries(search.value, filterValue());
6220
+ const invalid = draft.matchingInvalidExclusions(search.value, filterValue());
6221
+ results.textContent = copy.results.replace("{visible}", String(visible.length + invalid.length)).replace("{total}", String(draft.entries.length + draft.invalidExclusions.length));
6222
+ const filtered = search.value.trim() !== "" || filterValue() !== "all";
6223
+ if (visible.length === 0 && invalid.length === 0) {
6224
+ const empty = document.createElement("div");
6225
+ empty.className = "dsh-module-empty";
6226
+ empty.append(paragraph(copy.empty));
6227
+ const clear = button(copy.clearFilters);
6228
+ clear.addEventListener("click", () => {
6229
+ search.value = "";
6230
+ filter.value = "all";
6231
+ renderList();
6232
+ search.focus({ preventScroll: true });
6233
+ });
6234
+ empty.append(clear);
6235
+ list.append(empty);
6236
+ }
6237
+ if (invalid.length > 0) {
6238
+ const section = document.createElement("section");
6239
+ section.className = "dsh-module-group";
6240
+ section.dataset.clientModuleGroup = "invalid";
6241
+ const heading = document.createElement("h3");
6242
+ heading.textContent = `${copy.invalidSelections} (${invalid.length})`;
6243
+ section.append(heading, paragraph(copy.invalidHint, "dsh-module-groupHint"));
6244
+ for (const entry of invalid) {
6245
+ const row = document.createElement("div");
6246
+ row.className = "dsh-module-entry dsh-module-invalid-entry";
6247
+ row.dataset.invalidClientModuleId = entry.id;
6248
+ const id = document.createElement("code");
6249
+ id.textContent = entry.id;
6250
+ const remove = button(copy.clearInvalid);
6251
+ remove.setAttribute("aria-label", `${copy.clearInvalid}: ${entry.id}`);
6252
+ remove.addEventListener("click", () => {
6253
+ if (disposed || busy !== void 0 || draft?.clearInvalidExclusion(entry.id) !== true) return;
6254
+ status.textContent = "";
6255
+ status.classList.remove("dsh-module-error");
6256
+ renderList();
6257
+ (invalidActions.values().next().value ?? save).focus({ preventScroll: true });
6258
+ });
6259
+ row.append(id, paragraph(entry.reason === "missing" ? copy.invalidMissing : copy.invalidRequired), remove);
6260
+ invalidActions.set(entry.id, remove);
6261
+ section.append(row);
5574
6262
  }
6263
+ list.append(section);
5575
6264
  }
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");
6265
+ for (const group of [
6266
+ "extensions",
6267
+ "advanced",
6268
+ "required"
6269
+ ]) {
6270
+ const entries = visible.filter((entry) => clientModuleGroup(entry) === group);
6271
+ if (entries.length === 0) continue;
6272
+ const section = document.createElement(group === "extensions" ? "section" : "details");
6273
+ section.className = "dsh-module-group";
6274
+ section.dataset.group = group;
6275
+ section.dataset.clientModuleGroup = group === "advanced" ? "system" : group;
6276
+ const headingText = `${group === "required" ? copy.requiredGroup : copy[group]} (${entries.length})`;
6277
+ if (section instanceof HTMLDetailsElement) {
6278
+ section.open = filtered || expansion.get(group) === true;
6279
+ const heading = document.createElement("summary");
6280
+ heading.textContent = headingText;
6281
+ section.append(heading);
6282
+ section.addEventListener("toggle", () => {
6283
+ if (!filtered) expansion.set(group, section.open);
6284
+ });
6285
+ } else {
6286
+ const heading = document.createElement("h3");
6287
+ heading.textContent = headingText;
6288
+ section.append(heading);
6289
+ }
6290
+ const content = document.createElement("div");
6291
+ content.append(paragraph(group === "required" ? copy.requiredHint : group === "advanced" ? copy.advancedHint : copy.extensionsHint, "dsh-module-groupHint"));
6292
+ for (const entry of entries) {
6293
+ const row = document.createElement("div");
6294
+ row.className = "dsh-module-entry";
6295
+ row.dataset.clientModuleId = entry.id;
6296
+ const label = document.createElement("label");
6297
+ label.className = "dsh-module-choice";
6298
+ const input = document.createElement("input");
6299
+ input.type = "checkbox";
6300
+ input.checked = draft.isLoaded(entry.id);
6301
+ if (entry.required) input.dataset.required = "true";
6302
+ const name = document.createElement("span");
6303
+ const shortName = document.createElement("span");
6304
+ shortName.className = "dsh-module-choiceName";
6305
+ shortName.textContent = clientModuleDisplayName(entry.id);
6306
+ const selected = document.createElement("small");
6307
+ selected.textContent = entry.required ? copy.required : input.checked ? copy.loaded : copy.excluded;
6308
+ name.append(shortName, selected);
6309
+ label.append(input, name);
6310
+ row.append(label);
6311
+ const detail = document.createElement("details");
6312
+ detail.className = "dsh-module-entryDetails";
6313
+ const detailSummary = document.createElement("summary");
6314
+ detailSummary.textContent = copy.details;
6315
+ const id = paragraph(`${copy.moduleId}: `);
6316
+ const code = document.createElement("code");
6317
+ code.textContent = entry.id;
6318
+ id.append(code);
6319
+ detail.append(detailSummary, id, paragraph(entry.dependencies.length === 0 ? copy.noDependencies : `${copy.dependencies}${entry.dependencies.join(", ")}`));
6320
+ row.append(detail);
6321
+ input.addEventListener("change", () => {
6322
+ if (busy !== void 0 || draft === void 0) return;
6323
+ draft.setLoaded(entry.id, input.checked);
6324
+ input.checked = draft.isLoaded(entry.id);
6325
+ selected.textContent = entry.required ? copy.required : input.checked ? copy.loaded : copy.excluded;
6326
+ status.textContent = "";
6327
+ status.classList.remove("dsh-module-error");
6328
+ if (filterValue() !== "all") {
6329
+ renderList();
6330
+ filter.focus({ preventScroll: true });
6331
+ }
6332
+ updateControls();
6333
+ });
6334
+ choices.set(entry.id, input);
6335
+ content.append(row);
6336
+ }
6337
+ section.append(content);
6338
+ list.append(section);
5582
6339
  }
5583
- return parseClientModuleView(value);
6340
+ updateControls();
5584
6341
  };
5585
6342
  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;
6343
+ draft = new ClientModuleDraft(view.entries, view.excludedClientModules);
6344
+ source.textContent = view.source === "device" ? copy.deviceSource : view.source === "computer" ? copy.computerSource : copy.pluginSource;
6345
+ renderList();
5609
6346
  };
5610
- const failed = (error) => {
5611
- if (disposed || controller.signal.aborted) return;
6347
+ const failed = (error, mutation) => {
5612
6348
  status.classList.add("dsh-module-error");
5613
- status.textContent = error instanceof ClientModuleUiConflict ? `${copy.conflict}${error.detail}` : copy.failed;
6349
+ status.textContent = error instanceof ClientModuleUiConflict ? `${copy.conflict}${error.detail}` : error instanceof ClientModuleRequestTimeout ? error.mutation ? copy.writeTimeout : copy.readTimeout : mutation ? copy.saveFailed : copy.failed;
5614
6350
  status.focus({ preventScroll: true });
5615
6351
  };
5616
- const mutate = (body) => {
5617
- if (busy || !loaded) return;
5618
- setBusy(true);
5619
- request(body).then((view) => {
6352
+ const runRequest = (payload) => {
6353
+ if (disposed || busy !== void 0 || payload !== void 0 && draft === void 0) return;
6354
+ const ownGeneration = ++generation;
6355
+ busy = payload === void 0 ? "read" : "write";
6356
+ if (payload !== void 0) unconfirmedWrite = true;
6357
+ status.classList.remove("dsh-module-error");
6358
+ status.textContent = payload === void 0 ? copy.loading : copy.saving;
6359
+ updateControls();
6360
+ requestClientModuleView(scope, controller.signal, payload).then((view) => {
6361
+ if (disposed || ownGeneration !== generation) return;
6362
+ unconfirmedWrite = false;
5620
6363
  render(view);
5621
- if (!disposed) status.textContent = copy.saved;
5622
- }, failed).finally(() => {
5623
- if (!disposed) setBusy(false);
6364
+ status.textContent = payload === void 0 ? "" : copy.saved;
6365
+ }, (error) => {
6366
+ if (disposed || ownGeneration !== generation) return;
6367
+ if (error instanceof ClientModuleUiConflict) unconfirmedWrite = false;
6368
+ failed(error, payload !== void 0);
6369
+ }).finally(() => {
6370
+ if (disposed || ownGeneration !== generation) return;
6371
+ busy = void 0;
6372
+ updateControls();
5624
6373
  });
5625
6374
  };
6375
+ search.addEventListener("input", renderList);
6376
+ filter.addEventListener("change", renderList);
5626
6377
  save.addEventListener("click", () => {
5627
- mutate({ excludedClientModules: [...choices].filter(([, input]) => !input.checked).map(([id]) => id) });
6378
+ if ((draft?.changeCount ?? 0) > 0) runRequest({ excludedClientModules: draft?.excludedIds });
5628
6379
  });
5629
6380
  reset.addEventListener("click", () => {
5630
- if (window.confirm(copy.resetConfirm)) mutate({ reset: true });
6381
+ if (busy === void 0 && draft !== void 0 && window.confirm(copy.resetConfirm)) runRequest({ reset: true });
6382
+ });
6383
+ reread.addEventListener("click", () => {
6384
+ if (busy === void 0 && ((draft?.changeCount ?? 0) === 0 || window.confirm(copy.reloadConfirm))) runRequest();
5631
6385
  });
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
6386
  const dispose = () => {
5640
6387
  if (disposed) return;
5641
6388
  disposed = true;
6389
+ ++generation;
5642
6390
  controller.abort();
5643
- openDialogs.delete(dispose);
6391
+ openDialogs.delete(openDialog);
5644
6392
  window.removeEventListener("dsh-mobile:native-back", onBack, true);
6393
+ window.removeEventListener("keydown", onEscape, true);
5645
6394
  if (dialog.open) dialog.close();
5646
6395
  dialog.remove();
5647
6396
  if (trigger?.isConnected) trigger.focus({ preventScroll: true });
5648
6397
  };
5649
- openDialogs.add(dispose);
6398
+ const requestClose = () => {
6399
+ if (disposed) return;
6400
+ if (unconfirmedWrite && !window.confirm(copy.savingCloseConfirm)) return;
6401
+ if (!unconfirmedWrite && (draft?.changeCount ?? 0) > 0 && !window.confirm(copy.discardConfirm)) return;
6402
+ dispose();
6403
+ };
6404
+ const onBack = (event) => {
6405
+ if (!event.cancelable || !dialog.open || event.defaultPrevented || [...openDialogs].at(-1) !== openDialog) return;
6406
+ event.preventDefault();
6407
+ event.stopImmediatePropagation();
6408
+ requestClose();
6409
+ };
6410
+ const onEscape = (event) => {
6411
+ if (event.key !== "Escape" || event.isComposing || event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented || !dialog.open || [...openDialogs].at(-1) !== openDialog) return;
6412
+ event.preventDefault();
6413
+ event.stopImmediatePropagation();
6414
+ requestClose();
6415
+ };
6416
+ const openDialog = {
6417
+ dispose,
6418
+ requestClose
6419
+ };
6420
+ openDialogs.add(openDialog);
6421
+ window.addEventListener("dsh-mobile:native-back", onBack, true);
6422
+ window.addEventListener("keydown", onEscape, true);
6423
+ dialog.addEventListener("cancel", (event) => {
6424
+ event.preventDefault();
6425
+ requestClose();
6426
+ });
5650
6427
  dialog.addEventListener("close", dispose, { once: true });
5651
- close.addEventListener("click", dispose);
6428
+ close.addEventListener("click", requestClose);
5652
6429
  document.body.append(dialog);
5653
6430
  dialog.showModal();
5654
- setBusy(true);
5655
- status.textContent = copy.loading;
5656
- request().then(render, failed).finally(() => {
5657
- if (!disposed) setBusy(false);
5658
- });
6431
+ runRequest();
5659
6432
  }
5660
6433
  /** Settings row used on mobile pages and on the computer's General settings.
5661
6434
  * @param props - Selection scope and current locale.
@@ -6161,42 +6934,29 @@ window.__ModuleLoader__.load({
6161
6934
  error: "Could not switch computers. Try again."
6162
6935
  };
6163
6936
  }
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");
6937
+ function useMobileNativeSettingsAction(action) {
6938
+ const [controller] = (0, react.useState)(() => new MobileNativeSettingsActionController(action, () => window.__DSH_MOBILE_NATIVE__));
6939
+ const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
6168
6940
  (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);
6941
+ controller.start();
6942
+ window.addEventListener("dsh-mobile-native-ready", controller.refresh);
6184
6943
  return () => {
6185
- disposed = true;
6186
- window.removeEventListener("dsh-mobile-native-ready", checkNativeAction);
6944
+ window.removeEventListener("dsh-mobile-native-ready", controller.refresh);
6945
+ controller.stop();
6187
6946
  };
6188
- }, []);
6189
- if (!nativeReady) return null;
6947
+ }, [controller]);
6948
+ return {
6949
+ ...snapshot,
6950
+ invoke: controller.invoke
6951
+ };
6952
+ }
6953
+ /** Render the native-app-only action in DSH's General settings row style. */
6954
+ function MobileSwitchComputerRow() {
6955
+ const { supported, state, invoke } = useMobileNativeSettingsAction("mobile.switch-computer");
6956
+ if (!supported) return null;
6190
6957
  const locale = selectedMobileControlLocale();
6191
6958
  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
- };
6959
+ const busy = state === "busy" || state === "success";
6200
6960
  return (0, react.createElement)("div", {
6201
6961
  className: "dsh-mobile-settings_row",
6202
6962
  lang: locale
@@ -6206,10 +6966,10 @@ window.__ModuleLoader__.load({
6206
6966
  }, state === "error" ? copy.error : copy.description)), (0, react.createElement)("button", {
6207
6967
  type: "button",
6208
6968
  className: "dsh-mobile-settings_selector",
6209
- disabled: state === "busy",
6210
- "aria-busy": state === "busy" ? "true" : void 0,
6969
+ disabled: busy,
6970
+ "aria-busy": busy ? "true" : void 0,
6211
6971
  onClick: invoke
6212
- }, state === "busy" ? copy.busy : state === "error" ? copy.retry : copy.action));
6972
+ }, busy ? copy.busy : state === "error" ? copy.retry : copy.action));
6213
6973
  }
6214
6974
  function mobileTaskNotificationCopy(locale) {
6215
6975
  if (locale === "zh") return {
@@ -6245,42 +7005,10 @@ window.__ModuleLoader__.load({
6245
7005
  }
6246
7006
  /** Render the Android-only notification permission entry in DSH General settings. */
6247
7007
  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;
7008
+ const { supported, state, invoke } = useMobileNativeSettingsAction("notification.settings");
7009
+ if (!supported) return null;
6272
7010
  const locale = selectedMobileControlLocale();
6273
7011
  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
7012
  return (0, react.createElement)("div", {
6285
7013
  className: "dsh-mobile-settings_row",
6286
7014
  lang: locale
@@ -6450,6 +7178,11 @@ window.__ModuleLoader__.load({
6450
7178
  cpolar_installer_missing: "cpolarExtractFailed",
6451
7179
  cpolar_executable_missing: "cpolarExtractFailed",
6452
7180
  cpolar_extract_failed: "cpolarExtractFailed",
7181
+ cpolar_extract_tools_failed: "cpolarExtractToolsFailed",
7182
+ cpolar_extract_archive_failed: "cpolarExtractArchiveFailed",
7183
+ cpolar_extract_database_failed: "cpolarExtractDatabaseFailed",
7184
+ cpolar_extract_cabinet_failed: "cpolarExtractCabinetFailed",
7185
+ cpolar_extract_file_failed: "cpolarExtractFileFailed",
6453
7186
  cpolar_storage_failed: "cpolarStorageFailed",
6454
7187
  cpolar_config_missing: "cpolarConfigMissing",
6455
7188
  cpolar_config_invalid: "cpolarConfigInvalid",
@@ -6531,6 +7264,11 @@ window.__ModuleLoader__.load({
6531
7264
  "cpolar_component_invalid",
6532
7265
  "cpolar_config_missing",
6533
7266
  "cpolar_config_invalid",
7267
+ "cpolar_extract_tools_failed",
7268
+ "cpolar_extract_archive_failed",
7269
+ "cpolar_extract_database_failed",
7270
+ "cpolar_extract_cabinet_failed",
7271
+ "cpolar_extract_file_failed",
6534
7272
  "funnel_state_invalid",
6535
7273
  "cpolar_start_timeout",
6536
7274
  "cpolar_stopped",