dsh-mobile 0.5.4 → 0.6.0

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
@@ -514,7 +514,24 @@ window.__ModuleLoader__.load({
514
514
  wsPathsAdvancedHint: "通常不需要手动填写:用上方按钮即可。只有当你确切知道所需路径时才填。"
515
515
  });
516
516
  Object.assign(MOBILE_CONTROL_MESSAGES.en, {
517
+ trustedNetworksTitle: "Additional trusted networks · Advanced",
518
+ trustedNetworksHelp: "Keep the LAN subnet and explicitly add only networks you control, such as a routed WireGuard subnet. Device pairing and HTTPS are still required. Separate CIDRs with spaces, commas, or newlines.",
519
+ trustedNetworksLabel: "Additional source networks",
520
+ trustedNetworksSave: "Save networks",
521
+ trustedNetworksUnavailable: "Available only with managed LAN setup.",
522
+ trustedNetworksRestart: "Saved. Restart DSH to apply; existing connections are not interrupted now.",
523
+ trustedNetworksWindows: "On Windows, the firewall must also allow these source networks; the default rule allows LocalSubnet only.",
524
+ trustedNetworksFailed: "Could not read this setting. Retry after reopening this section.",
525
+ trustedNetworksInvalid: "Could not save. Enter valid network CIDRs, for example 11.24.0.0/24, and check that DSH can write its private setup file.",
517
526
  cpolarDownloadFailed: "Could not download cpolar from its official site. Check this computer’s network or proxy, then retry.",
527
+ caddyUnavailable: "The fixed Caddy component is not published for this candidate. Keep using an existing reverse proxy or another remote provider; unverified downloads will not run.",
528
+ caddyVerified: "Managed HTTPS entry verified for this computer.",
529
+ caddyInstalled: "Caddy component installed; the connection is not yet verified.",
530
+ caddyConnectionFailed: "Managed HTTPS connection was not established. Check the domain, public inbound access, port mapping and DNS credentials.",
531
+ caddyModeChangeConfirm: "Switching this origin mode closes its current connection. Continue?",
532
+ caddyInstallConfirm: "Download and verify the fixed Caddy build into DSH Mobile’s private directory? No system service, PATH or startup item will be installed.",
533
+ caddyInstallFailed: "Caddy was not installed. Check that the fixed artifact is available and verified; do not bypass its checksum.",
534
+ caddyPurgeFailed: "Cleanup was not completed. Stop the managed connection and retry.",
518
535
  cpolarDownloadTimeout: "The cpolar download timed out. Check this computer’s network or proxy, then retry.",
519
536
  cpolarDownloadHttp: "The official cpolar download returned HTTP {status}. Retry later or check the official download page.",
520
537
  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.",
@@ -522,7 +539,24 @@ window.__ModuleLoader__.load({
522
539
  cpolarStorageFailed: "Could not write the cpolar component in DSH Mobile’s private directory. Check disk space, permissions, and security software, then retry."
523
540
  });
524
541
  Object.assign(MOBILE_CONTROL_MESSAGES.it, {
542
+ trustedNetworksTitle: "Reti attendibili aggiuntive · Avanzate",
543
+ trustedNetworksHelp: "Mantieni la sottorete LAN e aggiungi solo reti che controlli, come una sottorete WireGuard instradata. Abbinamento e HTTPS restano obbligatori. Separa i CIDR con spazi, virgole o righe nuove.",
544
+ trustedNetworksLabel: "Reti sorgenti aggiuntive",
545
+ trustedNetworksSave: "Salva reti",
546
+ trustedNetworksUnavailable: "Disponibile solo con la configurazione LAN gestita.",
547
+ trustedNetworksRestart: "Salvato. Riavvia DSH per applicare; le connessioni attuali non vengono interrotte.",
548
+ trustedNetworksWindows: "Su Windows anche il firewall deve consentire queste reti sorgenti; la regola predefinita consente solo LocalSubnet.",
549
+ trustedNetworksFailed: "Impossibile leggere questa impostazione. Riapri la sezione e riprova.",
550
+ trustedNetworksInvalid: "Salvataggio non riuscito. Inserisci CIDR validi, ad esempio 11.24.0.0/24, e verifica che DSH possa scrivere il file privato di configurazione.",
525
551
  cpolarDownloadFailed: "Impossibile scaricare cpolar dal sito ufficiale. Controlla rete o proxy del computer e riprova.",
552
+ caddyUnavailable: "Il componente Caddy fisso non è pubblicato per questa versione candidata. Usa il tuo proxy o un altro provider; i download non verificati non vengono eseguiti.",
553
+ caddyVerified: "Ingresso HTTPS gestito verificato per questo computer.",
554
+ caddyInstalled: "Componente Caddy installato; connessione non ancora verificata.",
555
+ caddyConnectionFailed: "Connessione HTTPS gestita non stabilita. Controlla dominio, accesso pubblico, porte e credenziali DNS.",
556
+ caddyModeChangeConfirm: "Cambiare modalità origin chiude la connessione attuale. Continuare?",
557
+ caddyInstallConfirm: "Scaricare e verificare Caddy nella directory privata DSH Mobile? Nessun servizio, PATH o avvio automatico viene installato.",
558
+ caddyInstallFailed: "Caddy non installato. Verifica disponibilità e integrità del file fisso; non ignorare il checksum.",
559
+ caddyPurgeFailed: "Pulizia non completata. Arresta la connessione gestita e riprova.",
526
560
  cpolarDownloadTimeout: "Download di cpolar scaduto. Controlla rete o proxy del computer e riprova.",
527
561
  cpolarDownloadHttp: "Il download ufficiale di cpolar ha restituito HTTP {status}. Riprova più tardi o controlla la pagina ufficiale.",
528
562
  cpolarIntegrityFailed: "Il pacchetto cpolar non corrisponde alla versione verificata e non è stato installato. Riprova più tardi o aggiorna DSH Mobile; non aggirare la verifica.",
@@ -531,6 +565,23 @@ window.__ModuleLoader__.load({
531
565
  });
532
566
  Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
533
567
  cpolarDownloadFailed: "无法从 cpolar 官网下载组件。请检查这台电脑的网络或代理,然后重试。",
568
+ caddyUnavailable: "本次候选尚未发布固定 Caddy 组件。请继续使用已有反向代理或其他远程方式;不会执行未经校验的下载。",
569
+ caddyVerified: "托管 HTTPS 入口已验证为这台电脑。",
570
+ caddyInstalled: "Caddy 组件已安装,远程连接尚未验证。",
571
+ caddyConnectionFailed: "托管 HTTPS 连接未建立,请检查域名、公网入站、端口映射和 DNS 凭据。",
572
+ caddyModeChangeConfirm: "切换此自有代理模式会关闭它当前的连接,是否继续?",
573
+ caddyInstallConfirm: "从固定来源下载并校验 Caddy 到 DSH Mobile 私有目录?不会安装系统服务、写入 PATH 或设置开机启动。",
574
+ caddyInstallFailed: "Caddy 没有安装成功,请检查固定组件是否可用并通过校验,不要跳过校验。",
575
+ caddyPurgeFailed: "尚未完成清理,请停止受管连接后重试。",
576
+ trustedNetworksTitle: "额外可信网段 · 高级",
577
+ trustedNetworksHelp: "保留当前局域网网段,只追加你可控的网络,例如路由接入的 WireGuard 网段。配对与 HTTPS 校验仍然保留。每行一个 CIDR,也可用空格或逗号分隔。",
578
+ trustedNetworksLabel: "额外允许的来源网段",
579
+ trustedNetworksSave: "保存网段",
580
+ trustedNetworksUnavailable: "仅托管局域网配置支持此设置。",
581
+ trustedNetworksRestart: "已保存,重启 DSH 后生效;不会立即中断当前连接。",
582
+ trustedNetworksWindows: "Windows 防火墙也需要放行这些来源网段;默认规则只允许 LocalSubnet。",
583
+ trustedNetworksFailed: "暂时无法读取设置,请重新打开此区域后重试。",
584
+ trustedNetworksInvalid: "保存失败。请填写有效的网络 CIDR,例如 11.24.0.0/24,并确认 DSH 可写入私有配置文件。",
534
585
  cpolarDownloadTimeout: "下载 cpolar 组件超时。请检查这台电脑的网络或代理,然后重试。",
535
586
  cpolarDownloadHttp: "cpolar 官网下载返回 HTTP {status}。请稍后重试,或检查官网下载页。",
536
587
  cpolarIntegrityFailed: "下载的 cpolar 组件未通过固定版本校验,因此没有安装。请稍后重试或更新 DSH Mobile,不要跳过校验。",
@@ -1099,6 +1150,19 @@ window.__ModuleLoader__.load({
1099
1150
  originCopyBackend: "Copy backend address",
1100
1151
  originBackendCopied: "HTTP backend address copied.",
1101
1152
  originPurge: "Clear proxy settings",
1153
+ caddyModeManaged: "Managed Caddy (plugin handles HTTPS and certificates)",
1154
+ caddyModeExternal: "I already run my own reverse proxy",
1155
+ caddySectionTitle: "Managed Caddy upstream",
1156
+ caddyInstall: "Install managed Caddy",
1157
+ caddyInstallNote: "Downloaded from the official Caddy custom build (caddy core + Tencent Cloud DNS plugin for DNS-01), verified by checksum, stored in the plugin private directory. No system service, PATH entry, or startup item is added.",
1158
+ caddyDomain: "Public domain",
1159
+ caddyDnsProvider: "DNS provider (DNS-01)",
1160
+ caddySecretId: "Tencent Cloud SecretId",
1161
+ caddySecretKey: "Tencent Cloud SecretKey",
1162
+ caddyListenPort: "Public HTTPS port",
1163
+ caddySaveConnect: "Save and start Caddy",
1164
+ caddyPurge: "Remove managed Caddy and settings",
1165
+ caddyReady: "Managed Caddy is running; certificates are issued and renewed automatically.",
1102
1166
  originPurgeConfirm: "Stop this private HTTP backend and delete only its reverse-proxy settings? Paired remote devices, LAN access, and other providers are kept.",
1103
1167
  originPurged: "The backend is stopped and its proxy settings are cleared. Paired devices are kept.",
1104
1168
  originSaving: "Saving settings and starting the private backend…",
@@ -1141,6 +1205,19 @@ window.__ModuleLoader__.load({
1141
1205
  originCopyBackend: "Copia indirizzo backend",
1142
1206
  originBackendCopied: "Indirizzo backend HTTP copiato.",
1143
1207
  originPurge: "Cancella impostazioni proxy",
1208
+ caddyModeManaged: "Caddy gestito (il plugin gestisce HTTPS e certificati)",
1209
+ caddyModeExternal: "Uso già il mio reverse proxy",
1210
+ caddySectionTitle: "Upstream Caddy gestito",
1211
+ caddyInstall: "Installa Caddy gestito",
1212
+ caddyInstallNote: "Scaricato dalla build personalizzata ufficiale di Caddy (core + plugin DNS Tencent Cloud per DNS-01), verificato tramite checksum e conservato nella directory privata del plugin. Non aggiunge servizi di sistema, voci PATH o avvio automatico.",
1213
+ caddyDomain: "Dominio pubblico",
1214
+ caddyDnsProvider: "Provider DNS (DNS-01)",
1215
+ caddySecretId: "SecretId Tencent Cloud",
1216
+ caddySecretKey: "SecretKey Tencent Cloud",
1217
+ caddyListenPort: "Porta HTTPS pubblica",
1218
+ caddySaveConnect: "Salva e avvia Caddy",
1219
+ caddyPurge: "Rimuovi Caddy gestito e impostazioni",
1220
+ caddyReady: "Caddy gestito in esecuzione; i certificati vengono emessi e rinnovati automaticamente.",
1144
1221
  originPurgeConfirm: "Fermare questo backend HTTP privato ed eliminare solo le sue impostazioni reverse proxy? Dispositivi remoti abbinati, accesso LAN e altri provider vengono conservati.",
1145
1222
  originPurged: "Backend fermato e impostazioni proxy cancellate. I dispositivi abbinati sono conservati.",
1146
1223
  originSaving: "Salvataggio impostazioni e avvio del backend privato…",
@@ -1183,6 +1260,19 @@ window.__ModuleLoader__.load({
1183
1260
  originCopyBackend: "复制后端地址",
1184
1261
  originBackendCopied: "已复制 HTTP 后端地址。",
1185
1262
  originPurge: "清除代理配置",
1263
+ caddyModeManaged: "托管 Caddy(插件负责 HTTPS 与证书)",
1264
+ caddyModeExternal: "我已自行配置反向代理",
1265
+ caddySectionTitle: "托管 Caddy 上游",
1266
+ caddyInstall: "安装托管 Caddy",
1267
+ caddyInstallNote: "从 Caddy 官方定制构建下载(caddy 核心 + 腾讯云 DNS 插件,用于 DNS-01),经校验和验证后保存在插件私有目录。不添加系统服务、PATH 条目或开机启动项。",
1268
+ caddyDomain: "公网域名",
1269
+ caddyDnsProvider: "DNS 提供商(DNS-01)",
1270
+ caddySecretId: "腾讯云 SecretId",
1271
+ caddySecretKey: "腾讯云 SecretKey",
1272
+ caddyListenPort: "公网 HTTPS 端口",
1273
+ caddySaveConnect: "保存并启动 Caddy",
1274
+ caddyPurge: "移除托管 Caddy 与配置",
1275
+ caddyReady: "托管 Caddy 正在运行;证书会自动签发并续期。",
1186
1276
  originPurgeConfirm: "停止此私有 HTTP 后端,并且只删除它的反向代理配置?已配对的远程设备、局域网访问和其他提供方均保留。",
1187
1277
  originPurged: "已停止后端并清除代理配置,已配对设备仍保留。",
1188
1278
  originSaving: "正在保存配置并启动私有后端…",
@@ -1212,7 +1302,7 @@ window.__ModuleLoader__.load({
1212
1302
  cloudflaredBadge: "Account optional",
1213
1303
  cloudflaredDescription: "Installs the official cloudflared client on demand. A quick tunnel needs no account and gives a temporary public address; a named tunnel uses a Cloudflare account token and keeps one hostname that survives restarts.",
1214
1304
  prepareCloudflared: "Prepare cloudflared",
1215
- cloudflaredUnsupported: "Windows x64 and Linux x64/arm64 are currently supported.",
1305
+ cloudflaredUnsupported: "Windows x64, Linux x64/arm64, and macOS x64/arm64 are supported.",
1216
1306
  cloudflaredNotInstalled: "Not installed. A fixed version is downloaded from the official cloudflared release page only after you click the button below.",
1217
1307
  cloudflaredReady: "Official component {version} is ready.",
1218
1308
  cloudflaredQuickNote: "Quick tunnel: no Cloudflare account is needed, but Cloudflare positions quick tunnels for testing — the address is random, rate-limited, and carries no uptime guarantee. Do not rely on one for access that must stay reachable.",
@@ -1280,7 +1370,7 @@ window.__ModuleLoader__.load({
1280
1370
  cloudflaredBadge: "Account facoltativo",
1281
1371
  cloudflaredDescription: "Installa su richiesta il client cloudflared ufficiale. Il tunnel rapido non richiede account e fornisce un indirizzo pubblico temporaneo; il tunnel con nome usa il token di un account Cloudflare e mantiene un hostname stabile tra i riavvii.",
1282
1372
  prepareCloudflared: "Prepara cloudflared",
1283
- cloudflaredUnsupported: "Attualmente sono supportati Windows x64 e Linux x64/arm64.",
1373
+ cloudflaredUnsupported: "Sono supportati Windows x64, Linux x64/arm64 e macOS x64/arm64.",
1284
1374
  cloudflaredNotInstalled: "Non installato. Una versione fissa viene scaricata dalla pagina ufficiale di cloudflared solo dopo aver premuto il pulsante.",
1285
1375
  cloudflaredReady: "Componente ufficiale {version} pronto.",
1286
1376
  cloudflaredQuickNote: "Tunnel rapido: non serve un account Cloudflare, ma Cloudflare li considera pensati per i test — l’indirizzo è casuale, soggetto a limiti di frequenza e senza garanzia di disponibilità. Non usarlo per un accesso che deve restare raggiungibile.",
@@ -1348,7 +1438,7 @@ window.__ModuleLoader__.load({
1348
1438
  cloudflaredBadge: "账号可选",
1349
1439
  cloudflaredDescription: "按需安装官方 cloudflared 客户端。快速隧道无需账号,提供临时公网地址;命名隧道使用 Cloudflare 账号令牌,地址固定、重启不变。",
1350
1440
  prepareCloudflared: "准备 cloudflared",
1351
- cloudflaredUnsupported: "当前支持 Windows x64 与 Linux x64/arm64。",
1441
+ cloudflaredUnsupported: "支持 Windows x64、Linux x64/arm64 与 macOS x64/arm64。",
1352
1442
  cloudflaredNotInstalled: "尚未安装。只有点击下方按钮后,才会从 cloudflared 官方发布页下载固定版本。",
1353
1443
  cloudflaredReady: "官方组件 {version} 已就绪。",
1354
1444
  cloudflaredQuickNote: "快速隧道:无需 Cloudflare 账号,但官方定位是测试用途 —— 地址随机、有限流、无可用性保证。请勿用于必须长期可达的访问。",
@@ -2631,7 +2721,13 @@ window.__ModuleLoader__.load({
2631
2721
  the query restores the fixed scrim, and its more specific [hidden] rule
2632
2722
  keeps winning there. */
2633
2723
  .dsh-native-mobile-backdrop,.dsh-mobile-branch-toast,.dsh-mobile-media-toast { display:none; }
2634
- /* The content-font preference may exceed Safari's focused-editable minimum. */
2724
+ /* The content-font preference may exceed Safari's focused-editable minimum.
2725
+ The html-level class exists on every non-desktop mobile page, so it also
2726
+ covers body-level portals (model picker search) the center column never
2727
+ contained; the center marker stays alongside it because a disposed
2728
+ surface removes the html class while styles applied from its string can
2729
+ outlive the class on a page that still carries the center column. */
2730
+ html.dsh-native-mobile-active :is(input,textarea,[contenteditable="true"],[contenteditable="plaintext-only"],[contenteditable=""]),
2635
2731
  [data-dsh-mobile-center] :is(input,textarea,[contenteditable="true"],[contenteditable="plaintext-only"],[contenteditable=""]) { font-size:max(16px,1em,var(--dsh-content-font-size,1em)) !important; }
2636
2732
  .dsh-mobile-settings_row { display:flex; align-items:center; gap:8px; padding:16px 0; border-bottom:0.5px solid var(--dsw-alias-border-l2); }
2637
2733
  .dsh-mobile-settings_rowText { flex:1; min-width:0; display:flex; flex-direction:column; gap:4px; padding-right:48px; }
@@ -2642,6 +2738,16 @@ window.__ModuleLoader__.load({
2642
2738
  .dsh-mobile-settings_selector:active:not(:disabled) { background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover)); }
2643
2739
  .dsh-mobile-settings_selector:focus-visible { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
2644
2740
  .dsh-mobile-settings_selector:disabled { cursor:wait; opacity:.55; }
2741
+ .dsh-mobile-settings_fontControls { display:flex; align-items:center; gap:8px; flex-shrink:0; }
2742
+ .dsh-mobile-settings_fontControls button { min-width:48px; padding:0 12px; }
2743
+ .dsh-mobile-settings_fontControls output { min-width:42px; text-align:center; font-variant-numeric:tabular-nums; }
2744
+ /* The mobile row replaces the Host-backed font control, not the theme selector. */
2745
+ 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; }
2746
+ /* Landscape and wide App windows also constrain standard extension controls. */
2747
+ [data-dsh-mobile-composer-row] { min-width:0 !important; max-width:100% !important; }
2748
+ [data-dsh-mobile-composer-tools]:not([hidden]),[data-dsh-mobile-composer-controls]:not([hidden]) { flex-wrap:wrap !important; min-width:0 !important; max-width:100% !important; }
2749
+ [data-dsh-mobile-composer-tools] > *,[data-dsh-mobile-composer-tools] > [data-slot] > *,[data-dsh-mobile-composer-controls] > *,[data-dsh-mobile-composer-controls] > [data-slot] > * { min-width:0 !important; max-width:100% !important; }
2750
+ [data-dsh-mobile-composer-tools] button,[data-dsh-mobile-composer-controls] button { white-space:normal !important; overflow-wrap:anywhere !important; }
2645
2751
  @media ${NATIVE_MOBILE_OVERLAY_QUERY} {
2646
2752
  html.dsh-native-mobile-active,html.dsh-native-mobile-active body { width:100%; height:100%; overflow:hidden; }
2647
2753
  html.dsh-native-mobile-active { --dsh-mobile-motion-duration:200ms; --dsh-mobile-motion-ease:cubic-bezier(.22,1,.36,1); }
@@ -2751,12 +2857,15 @@ window.__ModuleLoader__.load({
2751
2857
  [data-dsh-mobile-message-column] [class*="_body"] { max-width:100% !important; overflow-wrap:anywhere; }
2752
2858
  /* Keep folded and expanded reasoning visually separate from the reply. */
2753
2859
  [data-dsh-mobile-message-column] [class*="_body"] > div:has(> [data-variant="think"]) { margin-bottom:12px !important; }
2754
- [data-dsh-mobile-center] [data-composer-card] ~ [class*="_root"],
2755
- [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"] { box-sizing:border-box !important; width:100% !important; max-width:100% !important; margin-bottom:-6px !important; padding:3px 4px 0 !important; font-size:11px !important; line-height:18px !important; white-space:normal !important; overflow:visible !important; text-overflow:clip !important; }
2860
+ [data-dsh-mobile-center] [data-composer-card] ~ [class*="_root"]:not(:where(input,textarea,[contenteditable])),
2861
+ [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"]:not(:where(input,textarea,[contenteditable])) { box-sizing:border-box !important; width:100% !important; max-width:100% !important; margin-bottom:-6px !important; padding:3px 4px 0 !important; font-size:11px !important; line-height:18px !important; white-space:normal !important; overflow:visible !important; text-overflow:clip !important; }
2756
2862
  [data-dsh-mobile-center] [data-composer-card] ~ [class*="_root"] [class*="_sep"],
2757
2863
  [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"] [class*="_sep"] { margin:0 6px !important; }
2758
- /* Composer dock stats strip (turns/steps/tokens) reads small on phones. */
2759
- [data-dsh-mobile-center] [data-slot="conversation.composer.dock"] [class*="_root"] { font-size:10px !important; line-height:16px !important; }
2864
+ /* Composer dock stats strip (turns/steps/tokens) reads small on phones.
2865
+ :not(:where(...)) keeps editables exempt at zero specificity cost, so
2866
+ the small !important fonts cannot beat the 16px editable floor while
2867
+ the dock spacing rules keep their cascade position. */
2868
+ [data-dsh-mobile-center] [data-slot="conversation.composer.dock"] [class*="_root"]:not(:where(input,textarea,[contenteditable])) { font-size:10px !important; line-height:16px !important; }
2760
2869
  /* The context ring is a composer-metadata _root too, so the metadata rule
2761
2870
  above stretched it across the whole dock. It cannot shrink, which left the
2762
2871
  session statistics beside it a few pixels wide, every number clipped to an
@@ -2813,18 +2922,37 @@ window.__ModuleLoader__.load({
2813
2922
  .dsh-mobile-media-action svg { flex:none; width:16px; height:16px; color:var(--dsw-alias-label-tertiary,currentColor); }
2814
2923
  .dsh-mobile-media-action span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
2815
2924
  [data-dsh-mobile-center] [class*="_composer"] { padding-left:8px !important; padding-right:8px !important; padding-bottom:max(8px,env(safe-area-inset-bottom)) !important; }
2816
- /* The desktop composer intentionally wraps whole toolbar groups. On a phone,
2817
- dynamic model and status labels made that row alternate between one and
2818
- two lines. Keep two stable columns and let only the model label shrink. */
2819
- [data-dsh-mobile-composer-row] { display:grid !important; grid-template-columns:max-content minmax(0,1fr) !important; align-items:center !important; gap:4px 8px !important; }
2820
- [data-dsh-mobile-composer-tools] { display:flex !important; flex-wrap:nowrap !important; width:max-content !important; min-width:0 !important; max-width:max-content !important; gap:6px !important; }
2821
- [data-dsh-mobile-composer-trailing] { display:flex !important; flex-wrap:nowrap !important; width:100% !important; min-width:0 !important; max-width:100% !important; gap:6px !important; margin-left:0 !important; justify-content:flex-end !important; }
2822
- [data-dsh-mobile-composer-model] { flex:1 1 0 !important; width:auto !important; min-width:0 !important; max-width:none !important; }
2925
+ /* Collapse the scroll viewport only; the editor, draft and attachments stay
2926
+ owned by DSH. Toolbar focus keeps the card expanded during menu taps. */
2927
+ [data-dsh-mobile-center] [data-composer-card]:not(:focus-within) > [data-input-scroll] { max-height:72px !important; overflow-y:auto !important; }
2928
+ /* Stock Send/Queue/Steer and Stop keep their handlers and disabled state. */
2929
+ [data-dsh-mobile-composer-row] button { min-width:44px !important; min-height:44px !important; touch-action:manipulation; }
2930
+ [data-dsh-mobile-composer-row] button[class*="_primary"] { min-width:44px !important; flex-shrink:0 !important; }
2931
+ /* Keep ordinary controls on one row. The trailing group reserves room for
2932
+ its model and primary buttons; extra tools wrap only when space is short. */
2933
+ [data-dsh-mobile-composer-row] { display:flex !important; flex-wrap:wrap !important; align-items:center !important; gap:4px 8px !important; min-width:0 !important; max-width:100% !important; }
2934
+ [data-dsh-mobile-composer-tools]:not([hidden]) { display:flex !important; flex:0 1 auto !important; flex-wrap:wrap !important; width:auto !important; min-width:0 !important; max-width:100% !important; gap:6px !important; }
2935
+ [data-dsh-mobile-composer-tools] > *,[data-dsh-mobile-composer-tools] > [data-slot] > * { min-width:0 !important; max-width:100% !important; }
2936
+ [data-dsh-mobile-composer-tools] button { white-space:normal !important; overflow-wrap:anywhere !important; }
2937
+ [data-dsh-mobile-composer-trailing] { display:flex !important; flex:1 0 100px !important; flex-wrap:nowrap !important; width:auto !important; min-width:min(100%,100px) !important; max-width:100% !important; gap:6px !important; margin-left:0 !important; justify-content:flex-end !important; }
2938
+ [data-dsh-mobile-composer-trailing]:has(> button[class*="_primary"] ~ button[class*="_primary"]) { flex-basis:144px !important; min-width:min(100%,144px) !important; }
2939
+ [data-dsh-mobile-composer-controls]:not([hidden]) { display:flex !important; flex:1 1 0 !important; flex-wrap:wrap !important; justify-content:flex-end !important; min-width:0 !important; max-width:100% !important; gap:6px !important; }
2940
+ [data-dsh-mobile-composer-controls] > *,[data-dsh-mobile-composer-controls] > [data-slot] > * { min-width:0 !important; max-width:100% !important; }
2941
+ [data-dsh-mobile-composer-controls] button { white-space:normal !important; overflow-wrap:anywhere !important; }
2942
+ [data-dsh-mobile-composer-model] { flex:0 1 auto !important; width:auto !important; min-width:44px !important; max-width:100% !important; }
2823
2943
  [data-dsh-mobile-composer-model-trigger] { box-sizing:border-box !important; width:100% !important; max-width:100% !important; min-width:0 !important; padding-left:6px !important; padding-right:4px !important; }
2824
2944
  [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; }
2825
2945
  [data-dsh-mobile-center] [class*="_root"]:has(> [class*="_card"] textarea) { box-sizing:border-box !important; width:100% !important; padding:0 0 8px !important; }
2826
2946
  [data-dsh-mobile-center] [class*="_root"]:has(> [class*="_card"] textarea) > [class=""]:last-child { display:none !important; }
2827
2947
  }
2948
+ @media (max-width:720px) and (max-height:500px) {
2949
+ /* An open software keyboard leaves less room for the focused draft. */
2950
+ html.dsh-native-mobile-active [data-dsh-mobile-center] [data-composer-card] > [data-input-scroll] { max-height:72px !important; overflow-y:auto !important; }
2951
+ }
2952
+ @media (max-width:720px) and (max-height:400px) {
2953
+ [data-dsh-mobile-center] [data-composer-card] { gap:8px !important; }
2954
+ html.dsh-native-mobile-active [data-dsh-mobile-center] [data-composer-card] > [data-input-scroll] { max-height:48px !important; }
2955
+ }
2828
2956
  @keyframes dsh-mobile-fade-in { from { opacity:0; } }
2829
2957
  @keyframes dsh-mobile-panel-in { from { opacity:.72; transform:translateY(6px); } }
2830
2958
  @keyframes dsh-mobile-view-in { from { opacity:.58; transform:translateY(5px); } }
@@ -3092,7 +3220,7 @@ window.__ModuleLoader__.load({
3092
3220
  /** Only a known on-screen keyboard in an active App composer may change Enter into a line break. */
3093
3221
  function isSoftKeyboardEnterLineBreak(event, context) {
3094
3222
  if (context.nativeState?.imeVisible !== true || context.nativeState.noHardwareKeyboard !== true || !context.editable || !context.activeSession || context.commandMenuOpen || context.recentlyComposing) return false;
3095
- if (!event.isTrusted || event.key !== "Enter" || event.isComposing || event.keyCode === 229 || event.repeat) return false;
3223
+ if (!event.isTrusted || event.key !== "Enter" || event.defaultPrevented || event.isComposing || event.keyCode === 229 || event.repeat) return false;
3096
3224
  return !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey && !event.getModifierState("AltGraph");
3097
3225
  }
3098
3226
  /** Attach the App-only Enter translation while retaining DSH's original physical-key semantics. */
@@ -3131,10 +3259,193 @@ window.__ModuleLoader__.load({
3131
3259
  editor.removeEventListener("keydown", onKeyDown, { capture: true });
3132
3260
  };
3133
3261
  }
3262
+ /**
3263
+ * Touch-primary device hint for the browser Enter translation. Duplicates the literal of
3264
+ * `TOUCH_PRIMARY_QUERY` in mobile-layout.ts, which builds a separate bundle entry that
3265
+ * this module must not import; a source test keeps the two from drifting apart.
3266
+ */
3267
+ const BROWSER_TOUCH_PRIMARY_QUERY = "(hover: none), (pointer: coarse)";
3268
+ /** The stock composer editor a phone-browser Enter may keep as a native line break. */
3269
+ const BROWSER_COMPOSER_EDITOR_QUERY = "[data-composer-input][contenteditable=\"true\"]";
3270
+ /**
3271
+ * Whether the App adapter or its earlier origin-scoped WebMessage channel owns Enter.
3272
+ * @param view - the page's current native bridge indicators.
3273
+ * @returns true without assuming that native keyboard state is already available.
3274
+ */
3275
+ function nativeAppOwnsComposerEnter(view) {
3276
+ const channel = view.dshMobileNative;
3277
+ return view.__DSH_MOBILE_NATIVE__ !== void 0 || typeof channel === "object" && channel !== null && "postMessage" in channel && typeof channel.postMessage === "function";
3278
+ }
3279
+ /**
3280
+ * Whether a phone-browser plain Enter keeps its native editing action instead of reaching
3281
+ * DSH's submit shortcut. Unlike the App gate there is no soft-keyboard proof, so this
3282
+ * mirrors the shipped question-card policy: the touch-primary hint plus the same event,
3283
+ * editor, and IME guards. A held key (`repeat`) keeps the native behavior every editor
3284
+ * has, and already-prevented or modified Enters stay under stock control — Cmd/Ctrl+Enter
3285
+ * keeps its stock submit path, which is how an attached hardware keyboard still sends.
3286
+ */
3287
+ function isBrowserTouchEnterLineBreak(event, context) {
3288
+ if (context.appBridge || !context.browserTouch || !context.editable || !context.activeSession || context.commandMenuOpen || context.recentlyComposing || !context.hasDraft) return false;
3289
+ if (!event.isTrusted || event.key !== "Enter" || event.defaultPrevented || event.isComposing || event.keyCode === 229) return false;
3290
+ return !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey && !event.getModifierState("AltGraph");
3291
+ }
3292
+ /**
3293
+ * Whether the stock composer still holds a draft. Mirrors `composerHasDraft` in
3294
+ * mobile-layout.ts, which builds a separate bundle entry this module must not import;
3295
+ * keep the two shapes aligned (checked against text, embedded decorators, attachments).
3296
+ */
3297
+ function browserComposerHasDraft(editor, card) {
3298
+ return (editor.textContent ?? "").replaceAll("​", "").trim() !== "" || editor.querySelector("[contenteditable=\"false\"],[data-lexical-decorator],img") !== null || card.querySelector("[data-slot=\"conversation.input.attachments\"] [role=\"group\"] > *") !== null;
3299
+ }
3300
+ /**
3301
+ * Composition tracking for the document-capture Enter translation. iOS Safari fires the
3302
+ * composition-confirming Enter with `isComposing === false` in the same task as
3303
+ * `compositionend`, so ownership extends ten milliseconds past the event — the same window
3304
+ * the stock composer and the App binder use.
3305
+ */
3306
+ function createDocumentCompositionGuard(target) {
3307
+ let composing = false;
3308
+ let composingUntil = 0;
3309
+ const onCompositionStart = () => {
3310
+ composing = true;
3311
+ };
3312
+ const onCompositionEnd = () => {
3313
+ composing = false;
3314
+ composingUntil = performance.now() + 10;
3315
+ };
3316
+ target.addEventListener("compositionstart", onCompositionStart, { capture: true });
3317
+ target.addEventListener("compositionend", onCompositionEnd, { capture: true });
3318
+ return {
3319
+ recentlyComposing: () => composing || performance.now() < composingUntil,
3320
+ dispose: () => {
3321
+ target.removeEventListener("compositionstart", onCompositionStart, { capture: true });
3322
+ target.removeEventListener("compositionend", onCompositionEnd, { capture: true });
3323
+ }
3324
+ };
3325
+ }
3326
+ /** Locate the stock composer editor under a keydown target: the editor itself or a node inside it. */
3327
+ function resolveBrowserComposerEditor(target) {
3328
+ const candidate = target;
3329
+ if (candidate === null || typeof candidate.closest !== "function") return null;
3330
+ const editor = candidate.closest.call(target, BROWSER_COMPOSER_EDITOR_QUERY);
3331
+ if (editor === null) return null;
3332
+ const card = editor.closest("[data-composer-card]");
3333
+ return card === null ? null : {
3334
+ editor,
3335
+ card
3336
+ };
3337
+ }
3338
+ /**
3339
+ * Phone-browser Enter translation for the stock composer. Lexical refuses keydown Enter
3340
+ * on Apple browsers and performs the line break from the native `beforeinput` flow, so
3341
+ * unlike the App binding this hides the keydown with `stopPropagation` only — no
3342
+ * `preventDefault`, no synthetic event — letting the browser's own insertParagraph produce
3343
+ * the same draft line break desktop Shift+Enter yields, while stock submit never runs.
3344
+ * `stopPropagation` (never the immediate variant) keeps same-node document listeners such
3345
+ * as the send-keyboard typing tracker and the question-card handler running. Document
3346
+ * capture runs before React root handlers and needs no per-editor binding lifecycle.
3347
+ */
3348
+ function bindBrowserComposerSoftEnter(view, context) {
3349
+ const guard = createDocumentCompositionGuard(view);
3350
+ const touchPrimary = typeof window === "object" && typeof window.matchMedia === "function" ? window.matchMedia(BROWSER_TOUCH_PRIMARY_QUERY) : void 0;
3351
+ const onKeyDown = (event) => {
3352
+ const target = resolveBrowserComposerEditor(event.target);
3353
+ if (target === null) return;
3354
+ if (!isBrowserTouchEnterLineBreak(event, {
3355
+ ...context(target.editor, target.card),
3356
+ browserTouch: touchPrimary?.matches === true,
3357
+ hasDraft: browserComposerHasDraft(target.editor, target.card),
3358
+ recentlyComposing: guard.recentlyComposing() || target.editor.hasAttribute("data-composer-composing")
3359
+ })) return;
3360
+ event.stopPropagation();
3361
+ };
3362
+ view.addEventListener("keydown", onKeyDown, { capture: true });
3363
+ return () => {
3364
+ view.removeEventListener("keydown", onKeyDown, { capture: true });
3365
+ guard.dispose();
3366
+ };
3367
+ }
3134
3368
  /** Check that an asynchronous picker result still belongs to its originating session and composer. */
3135
3369
  function isComposerMediaOriginCurrent(origin, current) {
3136
3370
  return !current.disposed && origin.generation === current.generation && origin.href === current.href && origin.composer !== null && current.composerConnected && origin.composer === current.composer && origin.sessionRoot !== null && origin.sessionRoot === current.sessionRoot && origin.sessionId !== null && origin.sessionId === current.sessionId;
3137
3371
  }
3372
+ /**
3373
+ * The App action that leaves the page for its paired-computer list. Mirrors the literal the
3374
+ * General-settings row invokes; the gesture below reads the same name from the bridge
3375
+ * capability list, so an App that does not advertise it keeps the stock drawer behavior.
3376
+ */
3377
+ const SWITCH_COMPUTER_NATIVE_ACTION = "mobile.switch-computer";
3378
+ /** Keeps the drawer from following the finger Android lifts at the end of a completed hold. */
3379
+ const SWITCH_COMPUTER_CLICK_SUPPRESSION_MS = 1200;
3380
+ /**
3381
+ * Hold policy for the collapsed header's drawer toggle. The App owns the paired-computer
3382
+ * list, so a hold only acts once the bridge advertises the action; every other press keeps
3383
+ * the stock toggle. DOM events stay in the installer so this policy stays testable.
3384
+ */
3385
+ function createSwitchComputerHold(options) {
3386
+ let hold;
3387
+ let supported = false;
3388
+ let probing = false;
3389
+ let disposed = false;
3390
+ let suppressClickUntil = 0;
3391
+ let firedAt = 0;
3392
+ const probeSupport = () => {
3393
+ if (disposed || supported || probing) return;
3394
+ probing = true;
3395
+ Promise.resolve().then(options.canSwitchComputer).then((value) => {
3396
+ probing = false;
3397
+ if (!disposed && value) supported = true;
3398
+ }, () => {
3399
+ probing = false;
3400
+ });
3401
+ };
3402
+ const stopTimer = () => {
3403
+ if (hold === void 0) return;
3404
+ options.clearTimer(hold.timer);
3405
+ hold = void 0;
3406
+ };
3407
+ const fire = (current) => {
3408
+ if (hold !== current || current.fired || !supported) return;
3409
+ current.fired = true;
3410
+ firedAt = options.now();
3411
+ suppressClickUntil = firedAt + SWITCH_COMPUTER_CLICK_SUPPRESSION_MS;
3412
+ options.switchComputer();
3413
+ };
3414
+ return {
3415
+ start: (x, y) => {
3416
+ if (disposed) return;
3417
+ stopTimer();
3418
+ suppressClickUntil = 0;
3419
+ firedAt = 0;
3420
+ probeSupport();
3421
+ const current = {
3422
+ x,
3423
+ y,
3424
+ timer: 0,
3425
+ fired: false
3426
+ };
3427
+ current.timer = options.setTimer(() => fire(current), 500);
3428
+ hold = current;
3429
+ },
3430
+ move: (x, y) => {
3431
+ if (hold === void 0 || hold.fired) return;
3432
+ if (Math.abs(x - hold.x) > 14 || Math.abs(y - hold.y) > 14) stopTimer();
3433
+ },
3434
+ end: stopTimer,
3435
+ consumeClickSuppression: (detail) => {
3436
+ if (detail === 0 || suppressClickUntil === 0 || options.now() > suppressClickUntil) return false;
3437
+ suppressClickUntil = 0;
3438
+ return true;
3439
+ },
3440
+ blocksContextMenu: () => firedAt !== 0 && options.now() - firedAt <= 1200,
3441
+ dispose: () => {
3442
+ disposed = true;
3443
+ stopTimer();
3444
+ suppressClickUntil = 0;
3445
+ firedAt = 0;
3446
+ }
3447
+ };
3448
+ }
3138
3449
  /** Add mobile semantics without replacing feature trees. */
3139
3450
  function installNativeMobileSurface(backServices) {
3140
3451
  document.documentElement.classList.add("dsh-native-mobile-active");
@@ -3185,6 +3496,59 @@ window.__ModuleLoader__.load({
3185
3496
  };
3186
3497
  document.addEventListener("pointerdown", onPointerDown, true);
3187
3498
  document.addEventListener("keydown", onKeyDown, true);
3499
+ const switchComputerHold = createSwitchComputerHold({
3500
+ now: () => window.performance.now(),
3501
+ setTimer: (callback, delayMs) => window.setTimeout(callback, delayMs),
3502
+ clearTimer: (handle) => {
3503
+ window.clearTimeout(handle);
3504
+ },
3505
+ canSwitchComputer: async () => {
3506
+ const bridge = window.__DSH_MOBILE_NATIVE__;
3507
+ if (bridge === void 0) return false;
3508
+ try {
3509
+ return (await bridge.capabilities()).includes(SWITCH_COMPUTER_NATIVE_ACTION);
3510
+ } catch {
3511
+ return false;
3512
+ }
3513
+ },
3514
+ switchComputer: () => {
3515
+ const bridge = window.__DSH_MOBILE_NATIVE__;
3516
+ if (bridge === void 0) return;
3517
+ Promise.resolve().then(() => bridge.invoke(SWITCH_COMPUTER_NATIVE_ACTION, {})).catch(() => void 0);
3518
+ }
3519
+ });
3520
+ const isDrawerToggleTarget = (target) => target instanceof Element && target.closest("[data-dsh-mobile-toggle]") !== null;
3521
+ const onTogglePointerDown = (event) => {
3522
+ if (!event.isPrimary || event.button !== 0 || !isDrawerToggleTarget(event.target)) return;
3523
+ switchComputerHold.start(event.clientX, event.clientY);
3524
+ };
3525
+ const onTogglePointerMove = (event) => {
3526
+ switchComputerHold.move(event.clientX, event.clientY);
3527
+ };
3528
+ const onTogglePointerEnd = () => {
3529
+ switchComputerHold.end();
3530
+ };
3531
+ const onToggleVisibilityChange = () => {
3532
+ if (document.hidden) switchComputerHold.end();
3533
+ };
3534
+ const onToggleClickCapture = (event) => {
3535
+ if (!isDrawerToggleTarget(event.target) || !switchComputerHold.consumeClickSuppression(event.detail)) return;
3536
+ event.preventDefault();
3537
+ event.stopPropagation();
3538
+ };
3539
+ const onToggleContextMenu = (event) => {
3540
+ if (!isDrawerToggleTarget(event.target) || !switchComputerHold.blocksContextMenu()) return;
3541
+ event.preventDefault();
3542
+ event.stopPropagation();
3543
+ };
3544
+ document.addEventListener("pointerdown", onTogglePointerDown, true);
3545
+ document.addEventListener("pointermove", onTogglePointerMove, true);
3546
+ document.addEventListener("pointerup", onTogglePointerEnd, true);
3547
+ document.addEventListener("pointercancel", onTogglePointerEnd, true);
3548
+ document.addEventListener("click", onToggleClickCapture, true);
3549
+ document.addEventListener("contextmenu", onToggleContextMenu, true);
3550
+ window.addEventListener("blur", onTogglePointerEnd);
3551
+ document.addEventListener("visibilitychange", onToggleVisibilityChange);
3188
3552
  const overlayQuery = window.matchMedia(NATIVE_MOBILE_OVERLAY_QUERY);
3189
3553
  const backdrop = document.createElement("button");
3190
3554
  backdrop.type = "button";
@@ -3243,6 +3607,12 @@ window.__ModuleLoader__.load({
3243
3607
  return token;
3244
3608
  });
3245
3609
  };
3610
+ const disposeBrowserComposerSoftEnter = bindBrowserComposerSoftEnter(document, (editor, composerCard) => ({
3611
+ appBridge: nativeAppOwnsComposerEnter(window),
3612
+ editable: editor.isConnected && editor.getAttribute("contenteditable") === "true" && editor.getAttribute("aria-disabled") !== "true" && editor.getAttribute("aria-haspopup") !== "menu" && composerCard.getAttribute("aria-busy") !== "true" && editor.getAttribute("inputmode") !== "none",
3613
+ activeSession: activeSessionForSoftEnter(composerCard, currentSessionOrigin().sessionId),
3614
+ commandMenuOpen: composerCard.querySelector("[data-trigger-menu],button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]") !== null
3615
+ }));
3246
3616
  const mediaRequestContext = () => {
3247
3617
  const session = currentSessionOrigin();
3248
3618
  return {
@@ -3686,6 +4056,8 @@ window.__ModuleLoader__.load({
3686
4056
  composerTrailing.dataset.dshMobileComposerTrailing = "true";
3687
4057
  const modelTrigger = composerTrailing.querySelector("button[aria-label^=\"选择模型\"],button[aria-label^=\"Select model\"],button[aria-label^=\"Seleziona modello\"]");
3688
4058
  if (modelTrigger !== null) {
4059
+ const controls = modelTrigger.closest("[class*=\"_standardControls\"]");
4060
+ if (controls !== null) controls.dataset.dshMobileComposerControls = "true";
3689
4061
  modelTrigger.dataset.dshMobileComposerModelTrigger = "true";
3690
4062
  modelTrigger.parentElement?.setAttribute("data-dsh-mobile-composer-model", "true");
3691
4063
  modelTrigger.querySelector("[class*=\"_triggerLabel\"]")?.setAttribute("data-dsh-mobile-composer-model-label", "true");
@@ -3709,9 +4081,8 @@ window.__ModuleLoader__.load({
3709
4081
  sidebarRoot.dataset.dshMobileSidebarRoot = "true";
3710
4082
  for (const brand of sidebarRoot.querySelectorAll("[class*=\"_fallbackBrandName\"]")) if (brand.textContent?.trim() === "DSH Local Build") brand.textContent = "DeepSeek Harness";
3711
4083
  if (toggle !== void 0) toggle.dataset.dshMobileToggle = "true";
3712
- const collapsed = classToken(sidebarRoot, "_collapsed");
3713
- sidebar.dataset.open = String(!collapsed);
3714
- backdrop.hidden = !drawerScrimVisible(collapsed, overlayQuery.matches);
4084
+ if (frame !== void 0) sidebar.dataset.open = String(!classToken(sidebarRoot, "_collapsed"));
4085
+ backdrop.hidden = frame === void 0 || !drawerScrimVisible(classToken(sidebarRoot, "_collapsed"), overlayQuery.matches);
3715
4086
  };
3716
4087
  const schedule = () => {
3717
4088
  if (scheduled === 0) scheduled = requestAnimationFrame(sync);
@@ -3782,6 +4153,7 @@ window.__ModuleLoader__.load({
3782
4153
  restoreLanguageMarker();
3783
4154
  for (const cleanup of [...browserPickerCleanups]) cleanup();
3784
4155
  disposeEnterBinding?.();
4156
+ disposeBrowserComposerSoftEnter();
3785
4157
  observer.disconnect();
3786
4158
  overlayQuery.removeEventListener("change", schedule);
3787
4159
  document.removeEventListener("click", onBranchClick, true);
@@ -3808,6 +4180,15 @@ window.__ModuleLoader__.load({
3808
4180
  historyScroller?.removeEventListener("scroll", onHistoryScroll);
3809
4181
  document.removeEventListener("pointerdown", onPointerDown, true);
3810
4182
  document.removeEventListener("keydown", onKeyDown, true);
4183
+ document.removeEventListener("pointerdown", onTogglePointerDown, true);
4184
+ document.removeEventListener("pointermove", onTogglePointerMove, true);
4185
+ document.removeEventListener("pointerup", onTogglePointerEnd, true);
4186
+ document.removeEventListener("pointercancel", onTogglePointerEnd, true);
4187
+ document.removeEventListener("click", onToggleClickCapture, true);
4188
+ document.removeEventListener("contextmenu", onToggleContextMenu, true);
4189
+ window.removeEventListener("blur", onTogglePointerEnd);
4190
+ document.removeEventListener("visibilitychange", onToggleVisibilityChange);
4191
+ switchComputerHold.dispose();
3811
4192
  document.removeEventListener("click", animateNavigation);
3812
4193
  backdrop.remove();
3813
4194
  document.documentElement.classList.remove("dsh-native-mobile-active");
@@ -3916,6 +4297,132 @@ window.__ModuleLoader__.load({
3916
4297
  };
3917
4298
  }
3918
4299
  //#endregion
4300
+ //#region src/mobile-font.ts
4301
+ /** Device-local typography for authenticated mobile pages, without Host settings writes. */
4302
+ const MOBILE_FONT_STORAGE_KEY = "dsh-mobile.content-font-size.v1";
4303
+ /** Read one local storage value; malformed or unavailable preferences use the mobile default. */
4304
+ function parseMobileFontSize(value) {
4305
+ if (value === null || !/^\d+$/.test(value)) return 16;
4306
+ const size = Number(value);
4307
+ return Number.isInteger(size) && size >= 12 && size <= 32 ? size : 16;
4308
+ }
4309
+ /** Local preference storage; unavailable browser storage still permits in-page adjustment. */
4310
+ var MobileFontPreference = class {
4311
+ storage;
4312
+ size = 16;
4313
+ listeners = /* @__PURE__ */ new Set();
4314
+ constructor(storage) {
4315
+ this.storage = storage;
4316
+ try {
4317
+ this.size = parseMobileFontSize(storage?.getItem("dsh-mobile.content-font-size.v1") ?? null);
4318
+ } catch (error) {}
4319
+ }
4320
+ getSnapshot = () => this.size;
4321
+ subscribe = (listener) => {
4322
+ this.listeners.add(listener);
4323
+ return () => {
4324
+ this.listeners.delete(listener);
4325
+ };
4326
+ };
4327
+ /** Apply a user-selected size locally; never send it to DSH's settings RPC. */
4328
+ set(size) {
4329
+ if (!Number.isInteger(size) || size < 12 || size > 32) throw new RangeError("Mobile font size must be an integer from 12 to 32");
4330
+ if (this.size === size) return;
4331
+ this.size = size;
4332
+ try {
4333
+ this.storage?.setItem(MOBILE_FONT_STORAGE_KEY, String(size));
4334
+ } catch (error) {}
4335
+ for (const listener of this.listeners) listener();
4336
+ }
4337
+ };
4338
+ /** Keep mobile typography independent from Host theme changes, including body-level portals. */
4339
+ function installMobileFontPreference() {
4340
+ let storage = null;
4341
+ try {
4342
+ storage = window.localStorage;
4343
+ } catch (error) {}
4344
+ const preference = new MobileFontPreference(storage);
4345
+ const property = "--dsh-content-font-size";
4346
+ let previous = document.body.style.getPropertyValue(property);
4347
+ let previousPriority = document.body.style.getPropertyPriority(property);
4348
+ let ownedValue;
4349
+ const apply = () => {
4350
+ const current = document.body.style.getPropertyValue(property);
4351
+ if (ownedValue !== void 0 && current !== ownedValue) {
4352
+ previous = current;
4353
+ previousPriority = document.body.style.getPropertyPriority(property);
4354
+ }
4355
+ const value = `${preference.getSnapshot()}px`;
4356
+ ownedValue = value;
4357
+ if (current !== value) document.body.style.setProperty(property, value);
4358
+ };
4359
+ apply();
4360
+ const unsubscribe = preference.subscribe(apply);
4361
+ const observer = new MutationObserver(apply);
4362
+ observer.observe(document.body, {
4363
+ attributes: true,
4364
+ attributeFilter: ["style"]
4365
+ });
4366
+ return {
4367
+ preference,
4368
+ dispose: () => {
4369
+ observer.disconnect();
4370
+ unsubscribe();
4371
+ if (previous === "") document.body.style.removeProperty(property);
4372
+ else document.body.style.setProperty(property, previous, previousPriority);
4373
+ }
4374
+ };
4375
+ }
4376
+ const messages = {
4377
+ zh: {
4378
+ title: "移动端字号",
4379
+ description: "仅保存在当前访问地址的本地数据中,不影响电脑或其他设备。",
4380
+ decrease: "减小移动端字号",
4381
+ increase: "增大移动端字号"
4382
+ },
4383
+ en: {
4384
+ title: "Mobile font size",
4385
+ description: "Saved locally for this address, without changing your computer or other devices.",
4386
+ decrease: "Decrease mobile font size",
4387
+ increase: "Increase mobile font size"
4388
+ },
4389
+ it: {
4390
+ title: "Dimensione testo mobile",
4391
+ description: "Salvata localmente per questo indirizzo, senza modificare il computer o altri dispositivi.",
4392
+ decrease: "Riduci il testo mobile",
4393
+ increase: "Aumenta il testo mobile"
4394
+ }
4395
+ };
4396
+ /** General settings row matching the existing Mobile controls and DSH theme tokens. */
4397
+ function MobileFontSizeRow({ preference, locale }) {
4398
+ const size = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
4399
+ const copy = messages[locale];
4400
+ return (0, react.createElement)("div", {
4401
+ className: "dsh-mobile-settings_row",
4402
+ lang: locale,
4403
+ "data-mobile-font-setting": true
4404
+ }, (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)), (0, react.createElement)("div", { className: "dsh-mobile-settings_fontControls" }, (0, react.createElement)("button", {
4405
+ type: "button",
4406
+ className: "dsh-mobile-settings_selector",
4407
+ "aria-label": copy.decrease,
4408
+ disabled: size === 12,
4409
+ onClick: () => {
4410
+ preference.set(size - 1);
4411
+ }
4412
+ }, "−"), (0, react.createElement)("output", {
4413
+ "aria-live": "polite",
4414
+ "aria-label": copy.title
4415
+ }, `${size}px`), (0, react.createElement)("button", {
4416
+ type: "button",
4417
+ className: "dsh-mobile-settings_selector",
4418
+ "aria-label": copy.increase,
4419
+ disabled: size === 32,
4420
+ onClick: () => {
4421
+ preference.set(size + 1);
4422
+ }
4423
+ }, "+")));
4424
+ }
4425
+ //#endregion
3919
4426
  //#region src/local-admin-host.ts
3920
4427
  /** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
3921
4428
  const DESKTOP_ADMIN_HEADER = "x-dsh-mobile-desktop-admin";
@@ -3976,6 +4483,295 @@ window.__ModuleLoader__.load({
3976
4483
  return (protocol === "dsh-app:" ? hostname === "app" : (protocol === "" || protocol === "http:" || protocol === "https:") && isLocalAdminHostname(hostname)) && frontend !== "dedicated" && !new URLSearchParams(query).has("dsh-mobile-preview");
3977
4484
  }
3978
4485
  //#endregion
4486
+ //#region src/client-module-ui.ts
4487
+ /** Device-local module selection and the computer's defaults share a read-only catalog. */
4488
+ const CLIENT_MODULE_COPY = {
4489
+ zh: {
4490
+ title: "移动页面模块",
4491
+ computer: "为移动访问设置默认加载模块,不卸载电脑上的插件。",
4492
+ device: "只为本设备选择加载模块,不改变其他手机或电脑。",
4493
+ open: "管理",
4494
+ loading: "正在读取模块…",
4495
+ hint: "勾选表示加载。启动必需的模块不能取消;依赖冲突会在保存时说明。",
4496
+ save: "保存,下次打开生效",
4497
+ reset: "恢复默认",
4498
+ close: "关闭",
4499
+ required: "启动必需",
4500
+ dependencies: "依赖:",
4501
+ saved: "已保存,不会自动刷新当前页面。重新打开 DSH 后生效。",
4502
+ failed: "暂时无法完成。请确认移动访问已启用,或检查下面的依赖冲突。",
4503
+ conflict: "无法保存:",
4504
+ computerSource: "使用电脑默认设置",
4505
+ deviceSource: "使用本设备设置",
4506
+ resetConfirm: "恢复默认模块选择?不会重新加载当前会话。"
4507
+ },
4508
+ en: {
4509
+ title: "Mobile page modules",
4510
+ computer: "Choose default modules for mobile access without uninstalling computer plugins.",
4511
+ device: "Choose modules for this device only; other phones and computers are unchanged.",
4512
+ open: "Manage",
4513
+ loading: "Reading modules…",
4514
+ hint: "Checked means loaded. Required boot modules cannot be unchecked; saving explains dependency conflicts.",
4515
+ save: "Save for next open",
4516
+ reset: "Restore defaults",
4517
+ close: "Close",
4518
+ required: "Required for boot",
4519
+ dependencies: "Depends on: ",
4520
+ saved: "Saved. This page is not reloaded automatically. Reopen DSH to apply.",
4521
+ failed: "Could not complete this operation. Enable mobile access or check the dependency conflict below.",
4522
+ conflict: "Could not save: ",
4523
+ computerSource: "Using computer defaults",
4524
+ deviceSource: "Using this device’s settings",
4525
+ resetConfirm: "Restore default module selection? The current conversation will not reload."
4526
+ },
4527
+ it: {
4528
+ title: "Moduli della pagina mobile",
4529
+ computer: "Scegli i moduli predefiniti senza disinstallare i plugin del computer.",
4530
+ device: "Scegli i moduli solo per questo dispositivo, senza cambiare gli altri.",
4531
+ open: "Gestisci",
4532
+ loading: "Lettura dei moduli…",
4533
+ hint: "Selezionato significa caricato. I moduli obbligatori non possono essere rimossi; il salvataggio mostra i conflitti.",
4534
+ save: "Salva per la prossima apertura",
4535
+ reset: "Ripristina predefiniti",
4536
+ close: "Chiudi",
4537
+ required: "Obbligatorio",
4538
+ dependencies: "Dipende da: ",
4539
+ saved: "Salvato. La pagina non viene ricaricata automaticamente. Riapri DSH per applicare.",
4540
+ failed: "Operazione non completata. Abilita accesso mobile o controlla il conflitto sotto.",
4541
+ conflict: "Impossibile salvare: ",
4542
+ computerSource: "Predefiniti del computer",
4543
+ deviceSource: "Impostazioni del dispositivo",
4544
+ resetConfirm: "Ripristinare i moduli predefiniti? La conversazione attuale non verrà ricaricata."
4545
+ }
4546
+ };
4547
+ var ClientModuleUiConflict = class extends Error {
4548
+ detail;
4549
+ constructor(detail) {
4550
+ super("client_module_conflict");
4551
+ this.detail = detail;
4552
+ }
4553
+ };
4554
+ /** Validate catalog response fields before creating interactive controls.
4555
+ * @param value - Gateway or computer-admin JSON response.
4556
+ * @returns The fields used by the module-selection dialog.
4557
+ */
4558
+ function parseClientModuleView(value) {
4559
+ if (typeof value !== "object" || value === null || !("entries" in value) || !Array.isArray(value.entries) || !("excludedClientModules" in value) || !Array.isArray(value.excludedClientModules) || !("source" in value) || ![
4560
+ "device",
4561
+ "computer",
4562
+ "plugin"
4563
+ ].includes(String(value.source))) throw new Error("invalid_module_catalog");
4564
+ return {
4565
+ entries: value.entries.map((entry) => {
4566
+ 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");
4567
+ const dependencies = entry.dependencies.map((dependency) => {
4568
+ if (typeof dependency !== "string") throw new Error("invalid_module_catalog");
4569
+ return dependency;
4570
+ });
4571
+ return {
4572
+ id: entry.id,
4573
+ required: entry.required,
4574
+ dependencies
4575
+ };
4576
+ }),
4577
+ excludedClientModules: value.excludedClientModules.map((id) => {
4578
+ if (typeof id !== "string") throw new Error("invalid_module_catalog");
4579
+ return id;
4580
+ }),
4581
+ source: value.source === "device" ? "device" : value.source === "computer" ? "computer" : "plugin"
4582
+ };
4583
+ }
4584
+ const CLIENT_MODULE_STYLES = `
4585
+ .dsh-module-row{display:flex;align-items:center;gap:8px;padding:16px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}
4586
+ .dsh-module-rowText{flex:1;min-width:0}.dsh-module-title{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}
4587
+ .dsh-module-description{margin-top:4px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
4588
+ .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}
4589
+ .dsh-module-button:focus-visible{outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb);outline-offset:2px}
4590
+ .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}
4591
+ .dsh-module-dialog::backdrop{background:rgb(15 23 42 / 40%)}.dsh-module-dialog h2{font-size:20px;margin:0 0 12px}
4592
+ .dsh-module-dialog p{overflow-wrap:anywhere}.dsh-module-list{display:flex;flex-direction:column;gap:4px;margin:12px 0}
4593
+ .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)}
4594
+ .dsh-module-choice input{width:24px;height:24px;flex:none;margin:2px 0}.dsh-module-choice span{min-width:0;overflow-wrap:anywhere}
4595
+ .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}
4596
+ .dsh-module-error{color:var(--dsw-alias-state-error-primary,#b42318)}
4597
+ `;
4598
+ const openDialogs = /* @__PURE__ */ new Set();
4599
+ /** Cancel pending dialogs when the owning plugin generation is disposed. */
4600
+ function disposeClientModuleDialogs() {
4601
+ for (const dispose of [...openDialogs]) dispose();
4602
+ }
4603
+ /** Close the foreground module modal before navigating its underlying DSH settings.
4604
+ * @returns Whether an open module modal consumed the return action.
4605
+ */
4606
+ function closeClientModuleDialog() {
4607
+ const dispose = [...openDialogs].at(-1);
4608
+ if (dispose === void 0) return false;
4609
+ dispose();
4610
+ return true;
4611
+ }
4612
+ /** Open one modal; closing cancels reads and restores focus without reloading the page.
4613
+ * @param scope - Computer defaults or the authenticated device's own override.
4614
+ * @param locale - Current DSH display language.
4615
+ * @param trigger - Control to focus again when the modal closes.
4616
+ */
4617
+ function openClientModuleSettings(scope, locale, trigger) {
4618
+ const copy = CLIENT_MODULE_COPY[locale];
4619
+ const endpoint = scope === "computer" ? "/api/mobile-access/client-modules" : "/mobile-access/client-modules";
4620
+ const controller = new AbortController();
4621
+ const dialog = document.createElement("dialog");
4622
+ dialog.className = "dsh-module-dialog";
4623
+ dialog.setAttribute("aria-label", copy.title);
4624
+ const title = document.createElement("h2");
4625
+ title.textContent = copy.title;
4626
+ const intro = document.createElement("p");
4627
+ intro.textContent = `${copy[scope]} ${copy.hint}`;
4628
+ const status = document.createElement("p");
4629
+ status.setAttribute("role", "status");
4630
+ status.setAttribute("aria-live", "polite");
4631
+ status.tabIndex = -1;
4632
+ const list = document.createElement("div");
4633
+ list.className = "dsh-module-list";
4634
+ const actions = document.createElement("div");
4635
+ actions.className = "dsh-module-actions";
4636
+ const button = (text) => {
4637
+ const result = document.createElement("button");
4638
+ result.type = "button";
4639
+ result.className = "dsh-module-button";
4640
+ result.textContent = text;
4641
+ return result;
4642
+ };
4643
+ const save = button(copy.save);
4644
+ const reset = button(copy.reset);
4645
+ const close = button(copy.close);
4646
+ actions.append(save, reset, close);
4647
+ dialog.append(title, intro, status, list, actions);
4648
+ let disposed = false;
4649
+ let busy = false;
4650
+ let loaded = false;
4651
+ const choices = /* @__PURE__ */ new Map();
4652
+ const setBusy = (value) => {
4653
+ busy = value;
4654
+ save.disabled = value || !loaded;
4655
+ reset.disabled = value || !loaded;
4656
+ for (const input of choices.values()) if (!input.dataset.required) input.disabled = value;
4657
+ };
4658
+ const request = async (body) => {
4659
+ const init = {
4660
+ method: body === void 0 ? "GET" : "POST",
4661
+ signal: controller.signal,
4662
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
4663
+ };
4664
+ init.headers = scope === "computer" ? localAdminRequestHeaders(init, location) : { "content-type": "application/json" };
4665
+ if (scope === "device" && body !== void 0) {
4666
+ 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="));
4667
+ if (csrfCookie !== void 0) {
4668
+ const headers = new Headers(init.headers);
4669
+ headers.set("x-dsh-mobile-csrf", csrfCookie.slice(csrfCookie.indexOf("=") + 1));
4670
+ init.headers = headers;
4671
+ }
4672
+ }
4673
+ const response = await fetch(endpoint, init);
4674
+ const value = await response.json();
4675
+ if (!response.ok) {
4676
+ const detail = typeof value === "object" && value !== null && "detail" in value && typeof value.detail === "string" ? value.detail.slice(0, 512) : "";
4677
+ if (response.status === 409 && detail !== "" && typeof value === "object" && value !== null && "error" in value && value.error === "excluded_client_modules_invalid") throw new ClientModuleUiConflict(detail);
4678
+ throw new Error("client_module_request_failed");
4679
+ }
4680
+ return parseClientModuleView(value);
4681
+ };
4682
+ const render = (view) => {
4683
+ if (disposed) return;
4684
+ loaded = true;
4685
+ choices.clear();
4686
+ list.replaceChildren();
4687
+ for (const entry of view.entries) {
4688
+ const label = document.createElement("label");
4689
+ label.className = "dsh-module-choice";
4690
+ const input = document.createElement("input");
4691
+ input.type = "checkbox";
4692
+ input.checked = !view.excludedClientModules.includes(entry.id);
4693
+ input.disabled = entry.required;
4694
+ if (entry.required) input.dataset.required = "true";
4695
+ const name = document.createElement("span");
4696
+ name.textContent = entry.id;
4697
+ const hint = document.createElement("small");
4698
+ hint.textContent = entry.required ? copy.required : entry.dependencies.length === 0 ? "" : `${copy.dependencies}${entry.dependencies.join(", ")}`;
4699
+ name.append(hint);
4700
+ label.append(input, name);
4701
+ list.append(label);
4702
+ choices.set(entry.id, input);
4703
+ }
4704
+ status.classList.remove("dsh-module-error");
4705
+ status.textContent = view.source === "device" ? copy.deviceSource : copy.computerSource;
4706
+ };
4707
+ const failed = (error) => {
4708
+ if (disposed || controller.signal.aborted) return;
4709
+ status.classList.add("dsh-module-error");
4710
+ status.textContent = error instanceof ClientModuleUiConflict ? `${copy.conflict}${error.detail}` : copy.failed;
4711
+ status.focus({ preventScroll: true });
4712
+ };
4713
+ const mutate = (body) => {
4714
+ if (busy || !loaded) return;
4715
+ setBusy(true);
4716
+ request(body).then((view) => {
4717
+ render(view);
4718
+ if (!disposed) status.textContent = copy.saved;
4719
+ }, failed).finally(() => {
4720
+ if (!disposed) setBusy(false);
4721
+ });
4722
+ };
4723
+ save.addEventListener("click", () => {
4724
+ mutate({ excludedClientModules: [...choices].filter(([, input]) => !input.checked).map(([id]) => id) });
4725
+ });
4726
+ reset.addEventListener("click", () => {
4727
+ if (window.confirm(copy.resetConfirm)) mutate({ reset: true });
4728
+ });
4729
+ const onBack = (event) => {
4730
+ if (!event.cancelable || !dialog.open || event.defaultPrevented) return;
4731
+ event.preventDefault();
4732
+ event.stopImmediatePropagation();
4733
+ dispose();
4734
+ };
4735
+ window.addEventListener("dsh-mobile:native-back", onBack, true);
4736
+ const dispose = () => {
4737
+ if (disposed) return;
4738
+ disposed = true;
4739
+ controller.abort();
4740
+ openDialogs.delete(dispose);
4741
+ window.removeEventListener("dsh-mobile:native-back", onBack, true);
4742
+ if (dialog.open) dialog.close();
4743
+ dialog.remove();
4744
+ if (trigger?.isConnected) trigger.focus({ preventScroll: true });
4745
+ };
4746
+ openDialogs.add(dispose);
4747
+ dialog.addEventListener("close", dispose, { once: true });
4748
+ close.addEventListener("click", dispose);
4749
+ document.body.append(dialog);
4750
+ dialog.showModal();
4751
+ setBusy(true);
4752
+ status.textContent = copy.loading;
4753
+ request().then(render, failed).finally(() => {
4754
+ if (!disposed) setBusy(false);
4755
+ });
4756
+ }
4757
+ /** Settings row used on mobile pages and on the computer's General settings.
4758
+ * @param props - Selection scope and current locale.
4759
+ * @returns A native-style General settings row.
4760
+ */
4761
+ function ClientModuleSettingsRow(props) {
4762
+ const copy = CLIENT_MODULE_COPY[props.locale];
4763
+ return (0, react.createElement)("div", {
4764
+ className: "dsh-module-row",
4765
+ lang: props.locale
4766
+ }, (0, react.createElement)("div", { className: "dsh-module-rowText" }, (0, react.createElement)("div", { className: "dsh-module-title" }, copy.title), (0, react.createElement)("div", { className: "dsh-module-description" }, copy[props.scope])), (0, react.createElement)("button", {
4767
+ type: "button",
4768
+ className: "dsh-module-button",
4769
+ onClick: (event) => {
4770
+ openClientModuleSettings(props.scope, props.locale, event.currentTarget);
4771
+ }
4772
+ }, copy.open));
4773
+ }
4774
+ //#endregion
3979
4775
  //#region src/client.ts
3980
4776
  /** Map a host-reported provider onto the UI union, defaulting to the built-in one. */
3981
4777
  function parseMobileRemoteProvider(value) {
@@ -3997,6 +4793,7 @@ window.__ModuleLoader__.load({
3997
4793
  }
3998
4794
  /**
3999
4795
  * Consume page-level surfaces and dismissible DSH overlays before layout navigation.
4796
+ * Composer model popups use DSH's Escape handling to return from a pane before closing.
4000
4797
  * @param view - the page window receiving the native Back request.
4001
4798
  * @param page - the current DSH document.
4002
4799
  * @param blocked - feedback for a foreground layer without a safe close action.
@@ -4009,16 +4806,45 @@ window.__ModuleLoader__.load({
4009
4806
  event.preventDefault();
4010
4807
  event.stopImmediatePropagation();
4011
4808
  };
4809
+ if (closeClientModuleDialog()) {
4810
+ claim();
4811
+ return;
4812
+ }
4012
4813
  const surface = [...page.querySelectorAll("[data-dsh-mobile-surface-placement=\"page\"], [data-dsh-mobile-surface-placement=\"overlay\"]")].filter((element) => visibleBackLayer(view, element)).at(-1);
4013
4814
  if (surface !== void 0) {
4014
4815
  setMobileSurfaceOpen(surface, false);
4015
4816
  claim();
4016
4817
  return;
4017
4818
  }
4018
- const layers = [...page.querySelectorAll("[role=\"dialog\"][aria-modal=\"true\"], [role=\"menu\"]")].filter((element) => visibleBackLayer(view, element));
4819
+ const modelPopups = /* @__PURE__ */ new Map();
4820
+ for (const trigger of page.querySelectorAll("[data-dsh-mobile-composer-model-trigger][aria-expanded=\"true\"]")) {
4821
+ const popupId = trigger.getAttribute("aria-controls");
4822
+ const popup = popupId === null ? null : page.getElementById(popupId);
4823
+ if (popup !== null && visibleBackLayer(view, trigger) && visibleBackLayer(view, popup) && (popup.getAttribute("role") === "menu" || popup.getAttribute("role") === "group")) modelPopups.set(popup, trigger);
4824
+ }
4825
+ const layers = [...page.querySelectorAll("[role=\"dialog\"][aria-modal=\"true\"], [role=\"menu\"], [role=\"group\"][id]")].filter((element) => visibleBackLayer(view, element) && (element.getAttribute("role") !== "group" || modelPopups.has(element)));
4019
4826
  const top = layers.at(-1);
4020
4827
  if (top === void 0) return;
4021
4828
  claim();
4829
+ const modelEntry = [...modelPopups].find(([popup]) => popup.contains(top));
4830
+ if (modelEntry !== void 0) {
4831
+ const [modelPopup, trigger] = modelEntry;
4832
+ if (layers.some((layer) => !modelPopup.contains(layer))) {
4833
+ blocked();
4834
+ return;
4835
+ }
4836
+ const focused = page.activeElement;
4837
+ const target = focused !== null && modelPopup.contains(focused) ? focused : trigger;
4838
+ const escape = new KeyboardEvent("keydown", {
4839
+ key: "Escape",
4840
+ code: "Escape",
4841
+ bubbles: true,
4842
+ cancelable: true
4843
+ });
4844
+ target.dispatchEvent(escape);
4845
+ if (!escape.defaultPrevented) blocked();
4846
+ return;
4847
+ }
4022
4848
  if (top.getAttribute("role") === "dialog") {
4023
4849
  const wrapper = top.parentElement;
4024
4850
  const mask = wrapper?.firstElementChild;
@@ -4715,6 +5541,83 @@ window.__ModuleLoader__.load({
4715
5541
  const lanSetupStatus = element("p", "dsh-mobile-control__status");
4716
5542
  lanSetupStatus.setAttribute("role", "status");
4717
5543
  lanSetupStatus.setAttribute("aria-live", "polite");
5544
+ const trustedNetworks = element("details", "dsh-mobile-control__details");
5545
+ const trustedNetworksSummary = element("summary");
5546
+ trustedNetworksSummary.textContent = t("trustedNetworksTitle");
5547
+ const trustedNetworksHelp = element("p", "dsh-mobile-control__component-note");
5548
+ trustedNetworksHelp.textContent = t("trustedNetworksHelp");
5549
+ const trustedNetworksLabel = element("label", "dsh-mobile-control__field");
5550
+ trustedNetworksLabel.textContent = t("trustedNetworksLabel");
5551
+ const trustedNetworksInput = element("textarea");
5552
+ trustedNetworksInput.rows = 2;
5553
+ trustedNetworksInput.placeholder = "11.24.0.0/24\n2001:db8::/32";
5554
+ trustedNetworksInput.autocomplete = "off";
5555
+ trustedNetworksInput.spellcheck = false;
5556
+ const trustedNetworksSave = element("button", "dsh-mobile-control__secondary");
5557
+ trustedNetworksSave.type = "button";
5558
+ trustedNetworksSave.textContent = t("trustedNetworksSave");
5559
+ const trustedNetworksStatus = element("p", "dsh-mobile-control__status");
5560
+ trustedNetworksStatus.setAttribute("role", "status");
5561
+ trustedNetworksStatus.setAttribute("aria-live", "polite");
5562
+ trustedNetworksLabel.append(trustedNetworksInput);
5563
+ trustedNetworks.append(trustedNetworksSummary, trustedNetworksHelp, trustedNetworksLabel, trustedNetworksSave, trustedNetworksStatus);
5564
+ let trustedNetworksLoaded = false;
5565
+ let trustedNetworksLoading = false;
5566
+ let trustedNetworksSupported = false;
5567
+ let trustedNetworksDirty = false;
5568
+ let trustedNetworksBusy = false;
5569
+ const syncTrustedNetworksControls = () => {
5570
+ const enabled = trustedNetworksLoaded && trustedNetworksSupported && !trustedNetworksLoading && !trustedNetworksBusy;
5571
+ trustedNetworksInput.disabled = !enabled;
5572
+ trustedNetworksSave.disabled = !enabled;
5573
+ trustedNetworks.setAttribute("aria-busy", String(trustedNetworksLoading || trustedNetworksBusy));
5574
+ };
5575
+ syncTrustedNetworksControls();
5576
+ const renderTrustedNetworks = (data) => {
5577
+ trustedNetworksLoaded = true;
5578
+ trustedNetworksSupported = data.supported === true;
5579
+ syncTrustedNetworksControls();
5580
+ if (!trustedNetworksDirty) trustedNetworksInput.value = Array.isArray(data.extraAllowedCidrs) ? data.extraAllowedCidrs.filter((value) => typeof value === "string").join("\n") : "";
5581
+ trustedNetworksStatus.textContent = !trustedNetworksSupported ? t("trustedNetworksUnavailable") : data.restartRequired === true ? t("trustedNetworksRestart") : "";
5582
+ trustedNetworksHelp.textContent = `${t("trustedNetworksHelp")}${data.windowsFirewall === true ? ` ${t("trustedNetworksWindows")}` : ""}`;
5583
+ };
5584
+ trustedNetworks.addEventListener("toggle", () => {
5585
+ if (!trustedNetworks.open || trustedNetworksLoaded || trustedNetworksLoading) return;
5586
+ trustedNetworksLoading = true;
5587
+ syncTrustedNetworksControls();
5588
+ trustedNetworksStatus.textContent = t("loadingStatus");
5589
+ controlRequestJson("/api/mobile-access/lan/trusted-networks").then(renderTrustedNetworks, () => {
5590
+ trustedNetworksLoaded = false;
5591
+ trustedNetworksSupported = false;
5592
+ trustedNetworksStatus.textContent = t("trustedNetworksFailed");
5593
+ }).finally(() => {
5594
+ trustedNetworksLoading = false;
5595
+ syncTrustedNetworksControls();
5596
+ });
5597
+ });
5598
+ trustedNetworksInput.addEventListener("input", () => {
5599
+ trustedNetworksDirty = true;
5600
+ trustedNetworksInput.removeAttribute("aria-invalid");
5601
+ });
5602
+ trustedNetworksSave.addEventListener("click", () => {
5603
+ if (trustedNetworksBusy || !trustedNetworksLoaded || !trustedNetworksSupported) return;
5604
+ trustedNetworksBusy = true;
5605
+ syncTrustedNetworksControls();
5606
+ const extraAllowedCidrs = trustedNetworksInput.value.trim().split(/[\s,]+/u).filter(Boolean);
5607
+ controlRequestJson("/api/mobile-access/lan/trusted-networks", {
5608
+ method: "POST",
5609
+ body: JSON.stringify({ extraAllowedCidrs })
5610
+ }).then((data) => {
5611
+ trustedNetworksDirty = false;
5612
+ renderTrustedNetworks(data);
5613
+ }, () => {
5614
+ trustedNetworksStatus.textContent = t("trustedNetworksInvalid");
5615
+ trustedNetworksInput.setAttribute("aria-invalid", "true");
5616
+ }).finally(() => {
5617
+ trustedNetworksBusy = false;
5618
+ syncTrustedNetworksControls();
5619
+ });
5620
+ });
4718
5621
  lanSetupLabel.append(lanSetupSelect);
4719
5622
  lanSetupActions.append(lanSetupRefresh, lanSetupConfigure);
4720
5623
  lanSetupForm.append(lanSetupLabel, lanSetupNote, lanSetupActions);
@@ -5346,6 +6249,89 @@ window.__ModuleLoader__.load({
5346
6249
  originCopyBackend.textContent = t("originCopyBackend");
5347
6250
  originBackend.append(originBackendLabel, originBackendAddress, originCopyBackend);
5348
6251
  originSetup.append(originSetupTitle, originHelp, originWarning, originFields, originFeedback, originFormActions, originBackend);
6252
+ const caddyModeRow = element("div", "dsh-mobile-control__origin-fields");
6253
+ const caddyModeManagedLabel = element("label", "dsh-mobile-control__field");
6254
+ const caddyModeManaged = element("input");
6255
+ caddyModeManaged.type = "radio";
6256
+ caddyModeManaged.name = "caddy-mode";
6257
+ caddyModeManaged.value = "managed";
6258
+ const caddyModeExternalLabel = element("label", "dsh-mobile-control__field");
6259
+ const caddyModeExternal = element("input");
6260
+ caddyModeExternal.type = "radio";
6261
+ caddyModeExternal.name = "caddy-mode";
6262
+ caddyModeExternal.value = "external";
6263
+ const caddyManagedText = element("span");
6264
+ caddyManagedText.textContent = t("caddyModeManaged");
6265
+ const caddyExternalText = element("span");
6266
+ caddyExternalText.textContent = t("caddyModeExternal");
6267
+ caddyModeManagedLabel.append(caddyModeManaged, caddyManagedText);
6268
+ caddyModeExternalLabel.append(caddyModeExternal, caddyExternalText);
6269
+ caddyModeRow.append(caddyModeManagedLabel, caddyModeExternalLabel);
6270
+ const caddyForm = element("div", "dsh-mobile-control__origin-fields");
6271
+ caddyForm.hidden = true;
6272
+ const caddyDomainLabel = element("label", "dsh-mobile-control__field");
6273
+ caddyDomainLabel.textContent = t("caddyDomain");
6274
+ const caddyDomain = element("input");
6275
+ caddyDomain.type = "url";
6276
+ caddyDomain.placeholder = "https://phone.example.com:8443";
6277
+ caddyDomain.maxLength = 512;
6278
+ caddyDomain.autocomplete = "off";
6279
+ caddyDomain.spellcheck = false;
6280
+ const caddyProviderLabel = element("label", "dsh-mobile-control__field");
6281
+ caddyProviderLabel.textContent = t("caddyDnsProvider");
6282
+ const caddyProvider = element("select");
6283
+ const caddyProviderOption = document.createElement("option");
6284
+ caddyProviderOption.value = "tencentcloud";
6285
+ caddyProviderOption.textContent = "Tencent Cloud DNSPod";
6286
+ caddyProvider.append(caddyProviderOption);
6287
+ const caddySecretIdLabel = element("label", "dsh-mobile-control__field");
6288
+ caddySecretIdLabel.textContent = t("caddySecretId");
6289
+ const caddySecretId = element("input");
6290
+ caddySecretId.type = "password";
6291
+ caddySecretId.autocomplete = "off";
6292
+ caddySecretId.spellcheck = false;
6293
+ const caddySecretKeyLabel = element("label", "dsh-mobile-control__field");
6294
+ caddySecretKeyLabel.textContent = t("caddySecretKey");
6295
+ const caddySecretKey = element("input");
6296
+ caddySecretKey.type = "password";
6297
+ caddySecretKey.autocomplete = "off";
6298
+ caddySecretKey.spellcheck = false;
6299
+ const caddyPortLabel = element("label", "dsh-mobile-control__field");
6300
+ caddyPortLabel.textContent = t("caddyListenPort");
6301
+ const caddyPort = element("input");
6302
+ caddyPort.type = "number";
6303
+ caddyPort.inputMode = "numeric";
6304
+ caddyPort.min = "1024";
6305
+ caddyPort.max = "65535";
6306
+ caddyPort.step = "1";
6307
+ caddyPort.value = "8443";
6308
+ caddyDomainLabel.append(caddyDomain);
6309
+ caddyProviderLabel.append(caddyProvider);
6310
+ caddySecretIdLabel.append(caddySecretId);
6311
+ caddySecretKeyLabel.append(caddySecretKey);
6312
+ caddyPortLabel.append(caddyPort);
6313
+ caddyForm.append(caddyDomainLabel, caddyProviderLabel, caddySecretIdLabel, caddySecretKeyLabel, caddyPortLabel);
6314
+ const caddyInstall = element("button", "dsh-mobile-control__primary");
6315
+ caddyInstall.type = "button";
6316
+ caddyInstall.textContent = t("caddyInstall");
6317
+ const caddyInstallNote = element("p", "dsh-mobile-control__component-note");
6318
+ caddyInstallNote.textContent = t("caddyInstallNote");
6319
+ const caddyStatus = element("p", "dsh-mobile-control__component-status");
6320
+ caddyStatus.hidden = true;
6321
+ const caddySave = element("button", "dsh-mobile-control__primary");
6322
+ caddySave.type = "button";
6323
+ caddySave.textContent = t("caddySaveConnect");
6324
+ caddySave.hidden = true;
6325
+ const caddyPurgeButton = element("button", "dsh-mobile-control__danger");
6326
+ caddyPurgeButton.type = "button";
6327
+ caddyPurgeButton.textContent = t("caddyPurge");
6328
+ caddyPurgeButton.hidden = true;
6329
+ const caddyFeedback = element("p", "dsh-mobile-control__origin-feedback");
6330
+ caddyFeedback.setAttribute("role", "status");
6331
+ caddyFeedback.setAttribute("aria-live", "polite");
6332
+ caddyFeedback.hidden = true;
6333
+ caddyForm.append(caddyInstallNote, caddyInstall, caddyStatus, caddySave, caddyPurgeButton, caddyFeedback);
6334
+ originSetup.append(caddyModeRow, caddyForm);
5349
6335
  const tailscaleInfo = element("details", "dsh-mobile-control__details");
5350
6336
  const tailscaleInfoSummary = element("summary");
5351
6337
  tailscaleInfoSummary.textContent = t("tailscaleHelp");
@@ -5671,7 +6657,7 @@ window.__ModuleLoader__.load({
5671
6657
  diagnosticsDetails.append(diagnosticsDetailsSummary, diagnosticsReport);
5672
6658
  header.append(title, headerActions);
5673
6659
  actions.append(toggle, pair, linkPair);
5674
- lanView.append(lanSetup, access, qrBox, status, extensionStatus, actions, manageRow, devicePanel);
6660
+ lanView.append(lanSetup, access, qrBox, status, extensionStatus, actions, manageRow, devicePanel, trustedNetworks);
5675
6661
  remoteView.append(remoteIntro, httpFrameWarning, providerSection, remoteWorkspace);
5676
6662
  diagnosticsView.append(diagnosticsIntro, diagnosticsSummary, diagnosticsToolbar, diagnosticsFeedback, diagnosticsChecks, wsPathsSection, diagnosticsDetails);
5677
6663
  panel.append(header, releaseNotice, updateCard, appDownload, switcher, lanView, remoteView, diagnosticsView);
@@ -5706,6 +6692,23 @@ window.__ModuleLoader__.load({
5706
6692
  let originConfigured = false;
5707
6693
  let originFormDirty = false;
5708
6694
  let originFormBusy = false;
6695
+ let caddyFormBusy = false;
6696
+ let caddyModeManagedSelected = false;
6697
+ let caddyFormDirty = false;
6698
+ let caddyModePreviewDirty = false;
6699
+ const showCaddyFeedback = (message, isError = false) => {
6700
+ caddyFeedback.textContent = message;
6701
+ caddyFeedback.classList.toggle("is-error", isError);
6702
+ caddyFeedback.hidden = false;
6703
+ };
6704
+ const setCaddyFormBusy = (busy) => {
6705
+ caddyFormBusy = busy;
6706
+ remoteProviderBusy = busy;
6707
+ remoteSnapshotEpoch += 1;
6708
+ caddyInstall.setAttribute("aria-busy", String(busy));
6709
+ caddySave.setAttribute("aria-busy", String(busy));
6710
+ renderRemote(latestRemoteState);
6711
+ };
5709
6712
  let remoteSnapshotEpoch = 0;
5710
6713
  let latestRemoteState = {};
5711
6714
  let frpInstalled = false;
@@ -6233,6 +7236,44 @@ window.__ModuleLoader__.load({
6233
7236
  originBackendLabel.textContent = originListening ? t("originBackendListening") : t("originBackendSaved");
6234
7237
  originBackendAddress.textContent = backendOrigin;
6235
7238
  originCopyBackend.disabled = remoteProviderBusy || backendOrigin === "";
7239
+ const caddyComponentInfo = originProvider.component;
7240
+ const caddyInstalled = caddyComponentInfo !== void 0 && caddyComponentInfo.installed === true;
7241
+ const caddySupported = caddyComponentInfo !== void 0 && caddyComponentInfo.supported === true;
7242
+ if (!caddyModePreviewDirty) caddyModeManagedSelected = data.originMode === "managed";
7243
+ const showManaged = origin && caddyModeManagedSelected;
7244
+ const caddyConfiguration = typeof data.caddyConfiguration === "object" && data.caddyConfiguration !== null ? data.caddyConfiguration : {};
7245
+ const caddyState = typeof data.caddyState === "object" && data.caddyState !== null ? data.caddyState : {};
7246
+ caddyModeRow.hidden = !origin;
7247
+ caddyForm.hidden = !showManaged;
7248
+ originFields.hidden = showManaged;
7249
+ originFormActions.hidden = showManaged;
7250
+ originHelp.hidden = showManaged;
7251
+ originWarning.hidden = showManaged;
7252
+ if (showManaged) originBackend.hidden = true;
7253
+ caddyInstall.hidden = !showManaged || caddyInstalled;
7254
+ caddyInstallNote.hidden = !showManaged;
7255
+ caddySave.hidden = !showManaged || !caddyInstalled;
7256
+ caddyPurgeButton.hidden = !showManaged || !caddyInstalled && caddyConfiguration.configured !== true;
7257
+ caddyStatus.hidden = !showManaged;
7258
+ caddyStatus.textContent = !caddySupported && !caddyInstalled ? t("caddyUnavailable") : caddyState.enabled === true && caddyState.state === "ready" ? t("caddyVerified") : caddyState.state === "starting" || caddyState.state === "connecting" ? t("remoteStateConnecting") : caddyState.state === "error" ? t("caddyConnectionFailed") : caddyInstalled ? t("caddyInstalled") : t("caddyInstallNote");
7259
+ caddyModeManaged.checked = caddyModeManagedSelected;
7260
+ caddyModeExternal.checked = !caddyModeManagedSelected;
7261
+ caddyModeManaged.disabled = remoteProviderBusy;
7262
+ caddyModeExternal.disabled = remoteProviderBusy;
7263
+ if (!caddyFormBusy && !caddyFormDirty) {
7264
+ caddyDomain.value = typeof caddyConfiguration.publicOrigin === "string" ? caddyConfiguration.publicOrigin : "";
7265
+ caddyPort.value = typeof caddyConfiguration.listenPort === "number" ? String(caddyConfiguration.listenPort) : "8443";
7266
+ }
7267
+ caddyInstall.disabled = remoteProviderBusy || !caddySupported || caddyInstalled;
7268
+ caddySave.disabled = remoteProviderBusy || !caddySupported || !caddyInstalled;
7269
+ caddyPurgeButton.disabled = remoteProviderBusy;
7270
+ for (const input of [
7271
+ caddyDomain,
7272
+ caddyProvider,
7273
+ caddySecretId,
7274
+ caddySecretKey,
7275
+ caddyPort
7276
+ ]) input.disabled = remoteProviderBusy || !caddySupported;
6236
7277
  selfHostedBadge.textContent = origin ? originListening ? t("originBackendReady") : t("advanced") : frpConfigured && frpInstalled ? t("ready") : t("advanced");
6237
7278
  const state = typeof data.state === "string" ? data.state : "error";
6238
7279
  const errorCode = typeof data.errorCode === "string" ? data.errorCode : "";
@@ -6274,14 +7315,15 @@ window.__ModuleLoader__.load({
6274
7315
  remoteSetup.disabled = remoteSetupUrl === "" || remoteReconnectBusy;
6275
7316
  remoteSetupRetry.disabled = remoteReconnectBusy;
6276
7317
  remoteToggle.textContent = remoteRunning ? t("disableRemote") : t("enableRemote");
6277
- const providerPrepared = cpolar ? cpolarInstalled && cpolarConfigured : cloudflared ? cloudflaredInstalled : frp ? frpInstalled && frpConfigured : origin ? originConfigured : true;
7318
+ const originPrepared = data.originMode === "managed" ? caddySupported && caddyInstalled && caddyConfiguration.configured === true : originConfigured && !caddyModeManagedSelected;
7319
+ const providerPrepared = cpolar ? cpolarInstalled && cpolarConfigured : cloudflared ? cloudflaredInstalled : frp ? frpInstalled && frpConfigured : origin ? originPrepared : true;
6278
7320
  remoteToggle.disabled = remoteProviderBusy || !providerPrepared && !(origin && remoteRunning);
6279
7321
  remoteLogin.hidden = !tailscale || state !== "needs-login" || remoteLoginUrl === "";
6280
7322
  remoteReconnect.hidden = needsFunnelSetup || state !== "error" && state !== "unavailable" || !providerPrepared;
6281
7323
  remoteActions.hidden = !providerPrepared && !(origin && remoteRunning);
6282
- remotePair.disabled = !remoteReady || originFormBusy;
6283
- remoteCopyLink.disabled = !remoteReady || originFormBusy;
6284
- remoteDevices.disabled = !remoteReady || originFormBusy;
7324
+ remotePair.disabled = !remoteReady || remoteProviderBusy;
7325
+ remoteCopyLink.disabled = !remoteReady || remoteProviderBusy;
7326
+ remoteDevices.disabled = !remoteReady || remoteProviderBusy;
6285
7327
  if (origin) {
6286
7328
  remoteReconnect.disabled = remoteProviderBusy || remoteReconnectBusy;
6287
7329
  remoteReset.disabled = remoteProviderBusy;
@@ -6326,7 +7368,7 @@ window.__ModuleLoader__.load({
6326
7368
  });
6327
7369
  let remoteLoadInFlight = false;
6328
7370
  const loadRemote = () => {
6329
- if (remoteLoadInFlight || originFormBusy) return;
7371
+ if (remoteLoadInFlight || originFormBusy || caddyFormBusy) return;
6330
7372
  const epoch = remoteSnapshotEpoch;
6331
7373
  remoteLoadInFlight = true;
6332
7374
  controlRequestJson("/api/mobile-access/remote/control").then((data) => {
@@ -7144,6 +8186,123 @@ window.__ModuleLoader__.load({
7144
8186
  clearOriginValidation();
7145
8187
  originFeedback.hidden = true;
7146
8188
  });
8189
+ const syncCaddyModeVisibility = () => {
8190
+ caddyModeManagedSelected = caddyModeManaged.checked;
8191
+ caddyModePreviewDirty = true;
8192
+ renderRemote(latestRemoteState);
8193
+ };
8194
+ caddyModeManaged.addEventListener("change", () => {
8195
+ if (remoteProviderBusy) return;
8196
+ originFeedback.hidden = true;
8197
+ syncCaddyModeVisibility();
8198
+ });
8199
+ caddyModeExternal.addEventListener("change", () => {
8200
+ if (remoteProviderBusy) return;
8201
+ if (latestRemoteState.originMode === "managed" && !window.confirm(t("caddyModeChangeConfirm"))) {
8202
+ caddyModeManaged.checked = true;
8203
+ caddyModeExternal.checked = false;
8204
+ return;
8205
+ }
8206
+ originFeedback.hidden = true;
8207
+ syncCaddyModeVisibility();
8208
+ if (latestRemoteState.originMode !== "managed") return;
8209
+ setCaddyFormBusy(true);
8210
+ controlRequestJson("/api/mobile-access/remote/origin/mode", {
8211
+ method: "POST",
8212
+ body: JSON.stringify({ mode: "external" })
8213
+ }).then((data) => {
8214
+ caddyModePreviewDirty = false;
8215
+ renderRemote(data);
8216
+ }, () => {
8217
+ caddyModePreviewDirty = false;
8218
+ renderRemote(latestRemoteState);
8219
+ showOriginFeedback(t("caddyConnectionFailed"), true);
8220
+ }).finally(() => {
8221
+ setCaddyFormBusy(false);
8222
+ loadRemote();
8223
+ });
8224
+ });
8225
+ for (const input of [
8226
+ caddyDomain,
8227
+ caddyProvider,
8228
+ caddySecretId,
8229
+ caddySecretKey,
8230
+ caddyPort
8231
+ ]) input.addEventListener("input", () => {
8232
+ caddyFormDirty = true;
8233
+ });
8234
+ caddyProvider.addEventListener("change", () => {
8235
+ caddyFormDirty = true;
8236
+ });
8237
+ caddyInstall.addEventListener("click", () => {
8238
+ if (remoteProviderBusy || caddyInstall.disabled || !window.confirm(t("caddyInstallConfirm"))) return;
8239
+ setCaddyFormBusy(true);
8240
+ showCaddyFeedback(t("checkingComponent"));
8241
+ controlRequestJson("/api/mobile-access/remote/caddy/component/install", {
8242
+ method: "POST",
8243
+ body: JSON.stringify({ confirm: true })
8244
+ }).then((data) => {
8245
+ renderRemote(data);
8246
+ showCaddyFeedback(t("caddyInstalled"));
8247
+ }, () => showCaddyFeedback(t("caddyInstallFailed"), true)).finally(() => {
8248
+ setCaddyFormBusy(false);
8249
+ loadRemote();
8250
+ });
8251
+ });
8252
+ caddySave.addEventListener("click", () => {
8253
+ if (remoteProviderBusy || caddySave.disabled || !caddyModeManagedSelected) return;
8254
+ clearOriginValidation();
8255
+ for (const input of [
8256
+ caddyDomain,
8257
+ caddySecretId,
8258
+ caddySecretKey,
8259
+ caddyPort
8260
+ ]) if (!input.reportValidity()) return;
8261
+ const body = {
8262
+ settings: {
8263
+ version: 1,
8264
+ publicOrigin: caddyDomain.value.trim(),
8265
+ dnsProvider: caddyProvider.value,
8266
+ listenPort: Number(caddyPort.value)
8267
+ },
8268
+ secretId: caddySecretId.value.trim(),
8269
+ secretKey: caddySecretKey.value.trim(),
8270
+ connect: true
8271
+ };
8272
+ setCaddyFormBusy(true);
8273
+ showCaddyFeedback(t("saving"));
8274
+ controlRequestJson("/api/mobile-access/remote/caddy/settings", {
8275
+ method: "POST",
8276
+ body: JSON.stringify(body)
8277
+ }).then((data) => {
8278
+ caddySecretId.value = "";
8279
+ caddySecretKey.value = "";
8280
+ caddyFormDirty = false;
8281
+ caddyModePreviewDirty = false;
8282
+ renderRemote(data);
8283
+ if (data.state === "error") showCaddyFeedback(t("caddyConnectionFailed"), true);
8284
+ }, () => showCaddyFeedback(t("caddyConnectionFailed"), true)).finally(() => {
8285
+ setCaddyFormBusy(false);
8286
+ loadRemote();
8287
+ });
8288
+ });
8289
+ caddyPurgeButton.addEventListener("click", () => {
8290
+ if (remoteProviderBusy || !window.confirm(t("caddyPurgeConfirm"))) return;
8291
+ setCaddyFormBusy(true);
8292
+ controlRequestJson("/api/mobile-access/remote/caddy/component/purge", {
8293
+ method: "POST",
8294
+ body: JSON.stringify({ confirm: true })
8295
+ }).then((data) => {
8296
+ caddySecretId.value = "";
8297
+ caddySecretKey.value = "";
8298
+ caddyFormDirty = false;
8299
+ caddyModePreviewDirty = false;
8300
+ renderRemote(data);
8301
+ }, () => showCaddyFeedback(t("caddyPurgeFailed"), true)).finally(() => {
8302
+ setCaddyFormBusy(false);
8303
+ loadRemote();
8304
+ });
8305
+ });
7147
8306
  originConfigure.addEventListener("click", () => {
7148
8307
  if (remoteProviderBusy || remoteProvider !== "origin") return;
7149
8308
  clearOriginValidation();
@@ -7164,7 +8323,10 @@ window.__ModuleLoader__.load({
7164
8323
  controlRequestJson("/api/mobile-access/remote/origin/configure", {
7165
8324
  method: "POST",
7166
8325
  body: JSON.stringify(form)
7167
- }).then(() => controlRequestJson("/api/mobile-access/remote/control", {
8326
+ }).then(() => controlRequestJson("/api/mobile-access/remote/origin/mode", {
8327
+ method: "POST",
8328
+ body: JSON.stringify({ mode: "external" })
8329
+ })).then(() => controlRequestJson("/api/mobile-access/remote/control", {
7168
8330
  method: "POST",
7169
8331
  body: JSON.stringify({ running: true })
7170
8332
  })).then((data) => {
@@ -8887,10 +10049,29 @@ window.__ModuleLoader__.load({
8887
10049
  const style = element("style");
8888
10050
  style.dataset.plugin = "dsh-mobile";
8889
10051
  style.textContent = desktopAdmin ? CONTROL_STYLES : NATIVE_MOBILE_STYLES;
10052
+ style.textContent += CLIENT_MODULE_STYLES;
8890
10053
  document.head.append(style);
8891
10054
  const removeVoiceSession = installVoiceSession(!desktopAdmin);
10055
+ const removeModuleSettings = ctx.slots.inject("settings.general.item", () => ctx.slots.register({
10056
+ name: "settings.general.item",
10057
+ id: "dsh-mobile-client-modules",
10058
+ order: 120
10059
+ }, () => (0, react.createElement)(ClientModuleSettingsRow, {
10060
+ scope: desktopAdmin ? "computer" : "device",
10061
+ locale: selectedMobileControlLocale()
10062
+ })));
8892
10063
  if (!desktopAdmin) {
10064
+ const localFont = installMobileFontPreference();
8893
10065
  const removeSettingsAction = ctx.slots.inject("settings.general.item", () => {
10066
+ const removeFontSize = ctx.slots.register({
10067
+ name: "settings.general.item",
10068
+ id: "font-size",
10069
+ order: 11,
10070
+ priority: -1
10071
+ }, () => (0, react.createElement)(MobileFontSizeRow, {
10072
+ preference: localFont.preference,
10073
+ locale: selectedMobileControlLocale()
10074
+ }));
8894
10075
  const removeSwitchComputer = ctx.slots.register({
8895
10076
  name: "settings.general.item",
8896
10077
  id: "dsh-mobile-switch-computer",
@@ -8904,6 +10085,7 @@ window.__ModuleLoader__.load({
8904
10085
  return () => {
8905
10086
  removeTaskNotifications();
8906
10087
  removeSwitchComputer();
10088
+ removeFontSize();
8907
10089
  };
8908
10090
  });
8909
10091
  const removeCustom = installCustomAssets();
@@ -8912,10 +10094,13 @@ window.__ModuleLoader__.load({
8912
10094
  getSidebarRight: () => ctx.get("sidebarRight")
8913
10095
  }));
8914
10096
  return () => {
10097
+ disposeClientModuleDialogs();
10098
+ removeModuleSettings();
8915
10099
  removeSettingsAction();
8916
10100
  removeCustom();
8917
10101
  removeSurface();
8918
10102
  removeVoiceSession();
10103
+ localFont.dispose();
8919
10104
  style.remove();
8920
10105
  };
8921
10106
  }
@@ -8960,6 +10145,8 @@ window.__ModuleLoader__.load({
8960
10145
  };
8961
10146
  });
8962
10147
  return () => {
10148
+ disposeClientModuleDialogs();
10149
+ removeModuleSettings();
8963
10150
  removeControl();
8964
10151
  removeVoiceSession();
8965
10152
  style.remove();