dsh-mobile 0.7.0 → 0.8.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/CHANGELOG.md +31 -0
- package/CONTRIBUTORS.md +5 -2
- package/README.en.md +58 -25
- package/README.md +58 -25
- package/SECURITY.md +13 -0
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/apps/mobile/README.md +11 -7
- package/apps/mobile/README.zh-CN.md +11 -7
- package/apps/mobile/release.json +3 -3
- package/assets/mobile-push/icon-180.png +0 -0
- package/assets/mobile-push/icon-192.png +0 -0
- package/assets/mobile-push/icon-512.png +0 -0
- package/docs/CADDY_MANAGED.en.md +1 -1
- package/docs/CADDY_MANAGED.md +1 -1
- package/docs/CLOUDFLARE_TUNNEL.en.md +5 -5
- package/docs/CLOUDFLARE_TUNNEL.md +5 -5
- package/docs/CPOLAR_SUBDOMAIN.en.md +2 -2
- package/docs/CPOLAR_SUBDOMAIN.md +2 -2
- package/docs/IROH.en.md +24 -3
- package/docs/IROH.md +24 -3
- package/docs/README.md +3 -1
- package/docs/SELF_HOSTED_ORIGIN.en.md +2 -2
- package/docs/SELF_HOSTED_ORIGIN.md +3 -3
- package/docs/SLOW_CONNECTIONS.en.md +7 -3
- package/docs/SLOW_CONNECTIONS.md +7 -3
- package/docs/WEB_PUSH.en.md +118 -0
- package/docs/WEB_PUSH.md +118 -0
- package/lib/client.js +1919 -243
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +126 -1
- package/lib/index.mjs +3787 -96
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +15 -2
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +17 -5
- package/packages/question-fixes/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -104,6 +104,11 @@ window.__ModuleLoader__.load({
|
|
|
104
104
|
collapseMobileAccess: "Collapse Mobile access",
|
|
105
105
|
downloadAndroid: "Download latest Android app",
|
|
106
106
|
downloadAndroidVersion: "Download latest Android app · v{version}",
|
|
107
|
+
downloadAndroidIroh: "Download full APK (iroh)",
|
|
108
|
+
downloadAndroidIrohAria: "Download the full Android APK that includes iroh",
|
|
109
|
+
downloadAndroidIrohVersion: "Download full APK (iroh) · v{version}",
|
|
110
|
+
downloadAndroidVersionSize: "Download latest Android app · v{version} · {size} MB",
|
|
111
|
+
downloadAndroidIrohVersionSize: "Download full APK (iroh) · v{version} · {size} MB",
|
|
107
112
|
downloadAndroidAria: "Download the latest Android app from GitHub Releases",
|
|
108
113
|
downloadAndroidVersionAria: "Download Android app v{version} from GitHub Releases",
|
|
109
114
|
updatePlugin: "Update plugin",
|
|
@@ -117,6 +122,7 @@ window.__ModuleLoader__.load({
|
|
|
117
122
|
updateNow: "Update now",
|
|
118
123
|
updateLater: "Not now",
|
|
119
124
|
lan: "Local network",
|
|
125
|
+
lanChoiceDescription: "Same Wi-Fi or hotspot. Fastest path, no internet or account needed.",
|
|
120
126
|
remote: "Remote",
|
|
121
127
|
lanAccess: "Local network access",
|
|
122
128
|
remoteAccess: "Remote access",
|
|
@@ -124,7 +130,7 @@ window.__ModuleLoader__.load({
|
|
|
124
130
|
remoteAddress: "Remote address",
|
|
125
131
|
loadingStatus: "Loading status…",
|
|
126
132
|
loadingRemoteStatus: "Loading remote status…",
|
|
127
|
-
generateCopyKey: "Generate
|
|
133
|
+
generateCopyKey: "Generate pairing QR code",
|
|
128
134
|
copyPairLink: "Copy pairing link",
|
|
129
135
|
managePairedDevices: "Manage paired devices",
|
|
130
136
|
clearAllDevices: "Clear all devices",
|
|
@@ -139,11 +145,11 @@ window.__ModuleLoader__.load({
|
|
|
139
145
|
providerInfoAria: "View remote connection security and network information",
|
|
140
146
|
providerGroupAria: "Remote connection method",
|
|
141
147
|
providerSafeTitle: "You remain protected",
|
|
142
|
-
providerSafeText: "Only paired devices can access DSH.
|
|
148
|
+
providerSafeText: "Only paired devices can access DSH. On mainland China networks prefer iroh (peer-to-peer, no domain or account needed) or cpolar; Tailscale may be slow or unavailable there. Every on-demand component can be removed completely.",
|
|
143
149
|
builtIn: "Built in",
|
|
144
150
|
mainlandPreferred: "Preferred in mainland China",
|
|
145
|
-
tailscaleDescription: "
|
|
146
|
-
cpolarDescription: "Installs the official component on demand and
|
|
151
|
+
tailscaleDescription: "Built in, no install. Broad coverage; sign in to Tailscale and enable Funnel once. Mainland China networks may be unstable.",
|
|
152
|
+
cpolarDescription: "Works well on mainland China networks. Installs the official component on demand and gives you a temporary public address.",
|
|
147
153
|
prepareCpolar: "Prepare cpolar",
|
|
148
154
|
checkingComponent: "Checking component…",
|
|
149
155
|
installOfficial: "Install official component",
|
|
@@ -174,13 +180,35 @@ window.__ModuleLoader__.load({
|
|
|
174
180
|
disableRemote: "Disable remote access",
|
|
175
181
|
continueLogin: "Continue login",
|
|
176
182
|
reconnect: "Reconnect",
|
|
177
|
-
generateRemoteQr: "Generate
|
|
183
|
+
generateRemoteQr: "Generate pairing QR code",
|
|
178
184
|
copyRemoteLink: "Copy remote pairing link",
|
|
179
185
|
manageRemoteDevices: "Manage remote devices",
|
|
180
186
|
resetRemoteLogin: "Sign out and clear remote login",
|
|
181
187
|
resetRemoteDevices: "Disable and clear remote devices",
|
|
182
188
|
lanOn: "Local network access is on.",
|
|
183
189
|
lanOff: "Local network access is off.",
|
|
190
|
+
lanBadgeOn: "On",
|
|
191
|
+
lanBadgeOff: "Off",
|
|
192
|
+
qrGenerated: "LAN QR code generated. Scan it from the app.",
|
|
193
|
+
qrPlaceholder: "Tap “Generate pairing QR code” to show the pairing QR.",
|
|
194
|
+
irohQrPlaceholder: "Tap “Generate iroh pairing QR code” to show the QR.",
|
|
195
|
+
irohLinkCopied: "Pairing link copied. Paste or open it in the full Android app; browsers cannot use iroh.",
|
|
196
|
+
remoteQrPlaceholder: "Tap “Generate pairing QR code” to show the QR.",
|
|
197
|
+
remoteQrPlaceholderIdle: "Turn on remote access to generate the pairing QR.",
|
|
198
|
+
pairingExpires: "Pairing QR valid until {time}. Regenerating invalidates the old code.",
|
|
199
|
+
pairingExpired: "The pairing QR has expired. Generate a new one.",
|
|
200
|
+
cloudflaredEnableNeedsConfig: "Save the named-tunnel settings with “Save and connect” before turning on remote access.",
|
|
201
|
+
cloudflaredShowConfig: "Show settings",
|
|
202
|
+
cloudflaredHideConfig: "Hide settings",
|
|
203
|
+
cloudflaredEditConfig: "Edit",
|
|
204
|
+
cloudflaredSwitchModeDisconnect: "Switching tunnel type closes the current remote connection first. Continue?",
|
|
205
|
+
cloudflaredTunnelSaved: "Settings saved. Check the connection status above.",
|
|
206
|
+
purgeIroh: "Remove the downloaded iroh component",
|
|
207
|
+
irohPurgeConfirm: "Remove the iroh component downloaded by this plugin? Saved identity, pairings, companion packages and other programs are kept.",
|
|
208
|
+
irohComponentRunning: "Turn off iroh remote access before removing its component.",
|
|
209
|
+
purgingIroh: "Removing the iroh component…",
|
|
210
|
+
devicesLanTitle: "LAN devices",
|
|
211
|
+
devicesRemoteTitle: "Remote devices",
|
|
184
212
|
enableLan: "Enable local network access",
|
|
185
213
|
disableLan: "Disable local network access",
|
|
186
214
|
extensionsLoaded: "Extensions: {loaded} loaded",
|
|
@@ -273,6 +301,11 @@ window.__ModuleLoader__.load({
|
|
|
273
301
|
collapseMobileAccess: "Riduci Accesso mobile",
|
|
274
302
|
downloadAndroid: "Scarica l’ultima app Android",
|
|
275
303
|
downloadAndroidVersion: "Scarica l’ultima app Android · v{version}",
|
|
304
|
+
downloadAndroidIroh: "Scarica APK completa (iroh)",
|
|
305
|
+
downloadAndroidIrohAria: "Scarica l’APK Android completo con iroh",
|
|
306
|
+
downloadAndroidIrohVersion: "Scarica APK completa (iroh) · v{version}",
|
|
307
|
+
downloadAndroidVersionSize: "Scarica l’ultima app Android · v{version} · {size} MB",
|
|
308
|
+
downloadAndroidIrohVersionSize: "Scarica APK completa (iroh) · v{version} · {size} MB",
|
|
276
309
|
downloadAndroidAria: "Scarica l’ultima app Android da GitHub Releases",
|
|
277
310
|
downloadAndroidVersionAria: "Scarica l’app Android v{version} da GitHub Releases",
|
|
278
311
|
updatePlugin: "Aggiorna plugin",
|
|
@@ -286,6 +319,7 @@ window.__ModuleLoader__.load({
|
|
|
286
319
|
updateNow: "Aggiorna ora",
|
|
287
320
|
updateLater: "Non ora",
|
|
288
321
|
lan: "Rete locale",
|
|
322
|
+
lanChoiceDescription: "Stessa Wi-Fi o hotspot. Percorso più veloce, senza internet né account.",
|
|
289
323
|
remote: "Remoto",
|
|
290
324
|
lanAccess: "Accesso rete locale",
|
|
291
325
|
remoteAccess: "Accesso remoto",
|
|
@@ -293,7 +327,7 @@ window.__ModuleLoader__.load({
|
|
|
293
327
|
remoteAddress: "Indirizzo remoto",
|
|
294
328
|
loadingStatus: "Lettura stato…",
|
|
295
329
|
loadingRemoteStatus: "Lettura stato remoto…",
|
|
296
|
-
generateCopyKey: "Genera
|
|
330
|
+
generateCopyKey: "Genera codice QR di abbinamento",
|
|
297
331
|
copyPairLink: "Copia link di abbinamento",
|
|
298
332
|
managePairedDevices: "Gestisci dispositivi abbinati",
|
|
299
333
|
clearAllDevices: "Rimuovi tutti i dispositivi",
|
|
@@ -308,11 +342,11 @@ window.__ModuleLoader__.load({
|
|
|
308
342
|
providerInfoAria: "Informazioni su sicurezza e rete della connessione remota",
|
|
309
343
|
providerGroupAria: "Metodo di connessione remota",
|
|
310
344
|
providerSafeTitle: "La protezione resta attiva",
|
|
311
|
-
providerSafeText: "Solo i dispositivi abbinati possono accedere a DSH.
|
|
345
|
+
providerSafeText: "Solo i dispositivi abbinati possono accedere a DSH. Sulle reti della Cina continentale preferisci iroh (peer-to-peer, senza dominio o account) o cpolar; Tailscale può essere lento o non disponibile. Ogni componente installato su richiesta può essere rimosso completamente.",
|
|
312
346
|
builtIn: "Integrato",
|
|
313
347
|
mainlandPreferred: "Preferito in Cina continentale",
|
|
314
|
-
tailscaleDescription: "Copertura
|
|
315
|
-
cpolarDescription: "
|
|
348
|
+
tailscaleDescription: "Integrato, nessuna installazione. Copertura ampia; accedi a Tailscale e abilita Funnel una volta. Le reti della Cina continentale possono essere instabili.",
|
|
349
|
+
cpolarDescription: "Adatto alle reti della Cina continentale. Installa il componente ufficiale su richiesta e fornisce un indirizzo pubblico temporaneo.",
|
|
316
350
|
prepareCpolar: "Prepara cpolar",
|
|
317
351
|
checkingComponent: "Controllo componente…",
|
|
318
352
|
installOfficial: "Installa componente ufficiale",
|
|
@@ -343,13 +377,35 @@ window.__ModuleLoader__.load({
|
|
|
343
377
|
disableRemote: "Disattiva accesso remoto",
|
|
344
378
|
continueLogin: "Continua accesso",
|
|
345
379
|
reconnect: "Riconnetti",
|
|
346
|
-
generateRemoteQr: "Genera QR di abbinamento
|
|
380
|
+
generateRemoteQr: "Genera codice QR di abbinamento",
|
|
347
381
|
copyRemoteLink: "Copia link di abbinamento remoto",
|
|
348
382
|
manageRemoteDevices: "Gestisci dispositivi remoti",
|
|
349
383
|
resetRemoteLogin: "Esci e rimuovi accesso remoto",
|
|
350
384
|
resetRemoteDevices: "Disattiva e rimuovi dispositivi remoti",
|
|
351
385
|
lanOn: "Accesso dalla rete locale attivo.",
|
|
352
386
|
lanOff: "Accesso dalla rete locale disattivato.",
|
|
387
|
+
lanBadgeOn: "Attivo",
|
|
388
|
+
lanBadgeOff: "Disattivo",
|
|
389
|
+
qrGenerated: "Codice QR LAN generato. Scansionalo dall’app.",
|
|
390
|
+
qrPlaceholder: "Tocca «Genera codice QR di abbinamento» per mostrare il QR.",
|
|
391
|
+
irohQrPlaceholder: "Tocca «Genera codice QR iroh» per mostrare il QR.",
|
|
392
|
+
irohLinkCopied: "Link di abbinamento copiato. Incollalo o aprilo nell’app Android completa; i browser non supportano iroh.",
|
|
393
|
+
remoteQrPlaceholder: "Tocca «Genera codice QR di abbinamento» per mostrare il QR.",
|
|
394
|
+
remoteQrPlaceholderIdle: "Attiva l’accesso remoto per generare il QR di abbinamento.",
|
|
395
|
+
pairingExpires: "QR di abbinamento valido fino a {time}. Rigenerare invalida il vecchio codice.",
|
|
396
|
+
pairingExpired: "Il QR di abbinamento è scaduto. Generane uno nuovo.",
|
|
397
|
+
cloudflaredEnableNeedsConfig: "Salva le impostazioni del tunnel con nome («Salva e connetti») prima di attivare l’accesso remoto.",
|
|
398
|
+
cloudflaredShowConfig: "Mostra impostazioni",
|
|
399
|
+
cloudflaredHideConfig: "Nascondi impostazioni",
|
|
400
|
+
cloudflaredEditConfig: "Modifica",
|
|
401
|
+
cloudflaredSwitchModeDisconnect: "Cambiare tipo di tunnel chiude prima la connessione remota attuale. Continuare?",
|
|
402
|
+
cloudflaredTunnelSaved: "Impostazioni salvate. Controlla lo stato della connessione qui sopra.",
|
|
403
|
+
purgeIroh: "Rimuovi il componente iroh scaricato",
|
|
404
|
+
irohPurgeConfirm: "Rimuovere il componente iroh scaricato da questo plugin? Identità, abbinamenti, pacchetti esterni e altri programmi vengono conservati.",
|
|
405
|
+
irohComponentRunning: "Disattiva l’accesso remoto iroh prima di rimuovere il componente.",
|
|
406
|
+
purgingIroh: "Rimozione del componente iroh…",
|
|
407
|
+
devicesLanTitle: "Dispositivi rete locale",
|
|
408
|
+
devicesRemoteTitle: "Dispositivi remoti",
|
|
353
409
|
enableLan: "Attiva accesso rete locale",
|
|
354
410
|
disableLan: "Disattiva accesso rete locale",
|
|
355
411
|
extensionsLoaded: "Estensioni: {loaded} caricate",
|
|
@@ -453,6 +509,11 @@ window.__ModuleLoader__.load({
|
|
|
453
509
|
collapseMobileAccess: "收起移动访问",
|
|
454
510
|
downloadAndroid: "下载最新 Android App",
|
|
455
511
|
downloadAndroidVersion: "下载最新 Android App · v{version}",
|
|
512
|
+
downloadAndroidIroh: "下载完整版 APK(含 iroh)",
|
|
513
|
+
downloadAndroidIrohAria: "下载包含 iroh 的完整版 Android APK",
|
|
514
|
+
downloadAndroidIrohVersion: "下载完整版(含 iroh)· v{version}",
|
|
515
|
+
downloadAndroidVersionSize: "下载最新 Android App · v{version} · {size} MB",
|
|
516
|
+
downloadAndroidIrohVersionSize: "下载完整版(含 iroh)· v{version} · {size} MB",
|
|
456
517
|
downloadAndroidAria: "从 GitHub Releases 下载最新版 Android App",
|
|
457
518
|
downloadAndroidVersionAria: "从 GitHub Releases 下载 Android App v{version}",
|
|
458
519
|
updatePlugin: "更新插件",
|
|
@@ -466,6 +527,7 @@ window.__ModuleLoader__.load({
|
|
|
466
527
|
updateNow: "立即更新",
|
|
467
528
|
updateLater: "暂不",
|
|
468
529
|
lan: "局域网",
|
|
530
|
+
lanChoiceDescription: "同一 Wi-Fi 或热点下直连,最快,不需要外网或账号。",
|
|
469
531
|
remote: "远程",
|
|
470
532
|
lanAccess: "局域网访问",
|
|
471
533
|
remoteAccess: "远程访问",
|
|
@@ -473,7 +535,7 @@ window.__ModuleLoader__.load({
|
|
|
473
535
|
remoteAddress: "远程地址",
|
|
474
536
|
loadingStatus: "正在读取状态…",
|
|
475
537
|
loadingRemoteStatus: "正在读取远程状态…",
|
|
476
|
-
generateCopyKey: "
|
|
538
|
+
generateCopyKey: "生成配对二维码",
|
|
477
539
|
copyPairLink: "复制配对链接",
|
|
478
540
|
managePairedDevices: "管理配对设备",
|
|
479
541
|
clearAllDevices: "清除所有设备",
|
|
@@ -488,11 +550,11 @@ window.__ModuleLoader__.load({
|
|
|
488
550
|
providerInfoAria: "查看远程连接安全与网络说明",
|
|
489
551
|
providerGroupAria: "远程连接方式",
|
|
490
552
|
providerSafeTitle: "你始终可以放心",
|
|
491
|
-
providerSafeText: "只有已配对设备能进入 DSH
|
|
553
|
+
providerSafeText: "只有已配对设备能进入 DSH。国内网络建议优先尝试 iroh(点对点直连,无需域名或账号)或 cpolar;Tailscale 在中国大陆网络下可能连接缓慢、中断或无法使用。所有按需安装的组件都可彻底清理。",
|
|
492
554
|
builtIn: "内置",
|
|
493
555
|
mainlandPreferred: "国内网络优先",
|
|
494
|
-
tailscaleDescription: "
|
|
495
|
-
cpolarDescription: "
|
|
556
|
+
tailscaleDescription: "内置无需安装。覆盖广;首次需登录 Tailscale 并开启 Funnel。中国大陆网络可能不稳定。",
|
|
557
|
+
cpolarDescription: "国内网络友好。按需安装官方组件,提供临时公网地址。",
|
|
496
558
|
prepareCpolar: "准备 cpolar",
|
|
497
559
|
checkingComponent: "正在检查组件…",
|
|
498
560
|
installOfficial: "安装官方组件",
|
|
@@ -523,13 +585,35 @@ window.__ModuleLoader__.load({
|
|
|
523
585
|
disableRemote: "关闭远程访问",
|
|
524
586
|
continueLogin: "继续登录",
|
|
525
587
|
reconnect: "重新连接",
|
|
526
|
-
generateRemoteQr: "
|
|
588
|
+
generateRemoteQr: "生成配对二维码",
|
|
527
589
|
copyRemoteLink: "复制远程配对链接",
|
|
528
590
|
manageRemoteDevices: "管理远程设备",
|
|
529
591
|
resetRemoteLogin: "退出并清除远程登录",
|
|
530
592
|
resetRemoteDevices: "关闭并清除远程设备",
|
|
531
593
|
lanOn: "局域网访问已开启。",
|
|
532
594
|
lanOff: "局域网访问已关闭。",
|
|
595
|
+
lanBadgeOn: "已开启",
|
|
596
|
+
lanBadgeOff: "已关闭",
|
|
597
|
+
qrGenerated: "局域网二维码已生成,用 App 扫描。",
|
|
598
|
+
qrPlaceholder: "点击「生成配对二维码」显示配对二维码。",
|
|
599
|
+
irohQrPlaceholder: "点击「生成 iroh 配对二维码」显示配对二维码。",
|
|
600
|
+
irohLinkCopied: "配对链接已复制。请在完整版 Android App 中粘贴或打开;浏览器无法使用 iroh。",
|
|
601
|
+
remoteQrPlaceholder: "点击「生成配对二维码」显示配对二维码。",
|
|
602
|
+
remoteQrPlaceholderIdle: "开启远程访问后可生成配对二维码。",
|
|
603
|
+
pairingExpires: "配对二维码有效期至 {time}。重新生成会使旧码失效。",
|
|
604
|
+
pairingExpired: "配对二维码已过期,请重新生成。",
|
|
605
|
+
cloudflaredEnableNeedsConfig: "请先保存命名隧道配置(点「保存并连接」)后再启用远程访问。",
|
|
606
|
+
cloudflaredShowConfig: "展开配置",
|
|
607
|
+
cloudflaredHideConfig: "收起配置",
|
|
608
|
+
cloudflaredEditConfig: "修改",
|
|
609
|
+
cloudflaredSwitchModeDisconnect: "切换快速隧道/命名隧道会先关闭当前远程连接,是否继续?",
|
|
610
|
+
cloudflaredTunnelSaved: "配置已保存,连接结果以上方状态为准。",
|
|
611
|
+
purgeIroh: "移除已下载的 iroh 组件",
|
|
612
|
+
irohPurgeConfirm: "移除本插件下载的 iroh 组件?已保存的身份、配对、外部伴随包和其他程序均会保留。",
|
|
613
|
+
irohComponentRunning: "请先关闭 iroh 远程访问,再移除组件。",
|
|
614
|
+
purgingIroh: "正在移除 iroh 组件…",
|
|
615
|
+
devicesLanTitle: "局域网设备",
|
|
616
|
+
devicesRemoteTitle: "远程设备",
|
|
533
617
|
enableLan: "开启局域网访问",
|
|
534
618
|
disableLan: "关闭局域网访问",
|
|
535
619
|
extensionsLoaded: "扩展:{loaded} 个已加载",
|
|
@@ -755,7 +839,7 @@ window.__ModuleLoader__.load({
|
|
|
755
839
|
selfHostedConnections: "Self-hosted connection",
|
|
756
840
|
advanced: "Advanced",
|
|
757
841
|
frpName: "Self-hosted FRP",
|
|
758
|
-
frpDescription: "
|
|
842
|
+
frpDescription: "Needs a server with a public IP. Opens one dedicated channel for this DSH gateway; you own the VPS and port.",
|
|
759
843
|
prepareFrp: "Configure self-hosted FRP",
|
|
760
844
|
frpStep1Title: "1 · Connection details",
|
|
761
845
|
frpStep1Text: "Enter the VPS address, frps control port, shared token, and public HTTPS origin.",
|
|
@@ -845,7 +929,7 @@ window.__ModuleLoader__.load({
|
|
|
845
929
|
selfHostedConnections: "Connessione autogestita",
|
|
846
930
|
advanced: "Avanzato",
|
|
847
931
|
frpName: "FRP autogestito",
|
|
848
|
-
frpDescription: "
|
|
932
|
+
frpDescription: "Richiede un server con IP pubblico. Apre un canale dedicato per questo gateway DSH; VPS e porta restano tuoi.",
|
|
849
933
|
prepareFrp: "Configura FRP autogestito",
|
|
850
934
|
frpStep1Title: "1 · Dati di connessione",
|
|
851
935
|
frpStep1Text: "Inserisci indirizzo VPS, porta di controllo frps, token condiviso e origine HTTPS pubblica.",
|
|
@@ -935,7 +1019,7 @@ window.__ModuleLoader__.load({
|
|
|
935
1019
|
selfHostedConnections: "自建连接",
|
|
936
1020
|
advanced: "高级",
|
|
937
1021
|
frpName: "自建 FRP",
|
|
938
|
-
frpDescription: "
|
|
1022
|
+
frpDescription: "需要一台有公网 IP 的服务器。只为当前 DSH 网关开一条专用通道,VPS 和端口由你掌控。",
|
|
939
1023
|
prepareFrp: "配置自建 FRP",
|
|
940
1024
|
frpStep1Title: "1 · 连接信息",
|
|
941
1025
|
frpStep1Text: "填写中转服务器地址、frps 控制端口、共享 Token 和手机访问地址。没有域名时,可直接使用公网 IPv4。",
|
|
@@ -1031,12 +1115,13 @@ window.__ModuleLoader__.load({
|
|
|
1031
1115
|
enlargeQr: "Enlarge the QR code",
|
|
1032
1116
|
closeEnlargedQr: "Close the enlarged QR code",
|
|
1033
1117
|
irohPairingReady: "iroh pairing code {code} is live for five minutes. Scan it from the iroh pairing screen; opening another code replaces this one.",
|
|
1034
|
-
irohDescription: "
|
|
1118
|
+
irohDescription: "Peer-to-peer encrypted link with relay fallback. No domain, port or account needed; requires the full Android App.",
|
|
1035
1119
|
prepareIroh: "Prepare iroh",
|
|
1036
1120
|
irohInstallNote: "The Install button downloads the pinned sidecar release (dsh-mobile-iroh 0.1.0-alpha.1, about 15 MB on Windows x64, 24 MB on Linux x64) into the plugin private directory and verifies its SHA-256. An installed companion with a verified copy is reused instead, with no download.",
|
|
1037
1121
|
irohComponentNote: "An installed sidecar is re-verified by SHA-256 on every use. It is not bundled with the plugin package; a companion-provided copy can only be removed by removing the companion.",
|
|
1038
1122
|
irohPhaseNote: "Experimental connection. Requires the full Android App 0.7.0+ with iroh; the compatibility APK and mobile browsers cannot use it.",
|
|
1039
1123
|
irohUnavailable: "iroh is not ready: no sidecar is installed yet. Install the official component below to download and verify it.",
|
|
1124
|
+
irohInstalledNotReady: "iroh is installed but not running. Turn on remote access to start it.",
|
|
1040
1125
|
irohStarting: "Resolving the iroh endpoint identity…",
|
|
1041
1126
|
irohConnecting: "Sidecar connected; starting the DSH authentication gateway…",
|
|
1042
1127
|
irohCompanionMissing: "No verified iroh sidecar is available. Use Install official component below; a separate companion plugin is optional.",
|
|
@@ -1064,12 +1149,13 @@ window.__ModuleLoader__.load({
|
|
|
1064
1149
|
enlargeQr: "Ingrandisci il codice QR",
|
|
1065
1150
|
closeEnlargedQr: "Chiudi il codice QR ingrandito",
|
|
1066
1151
|
irohPairingReady: "Il codice iroh {code} è attivo per cinque minuti. Scansionalo dalla schermata di abbinamento iroh; aprirne un altro sostituisce questo.",
|
|
1067
|
-
irohDescription: "
|
|
1152
|
+
irohDescription: "Collegamento cifrato peer-to-peer con ripiego su relay. Nessun dominio, porta o account richiesto; richiede l’App Android completa.",
|
|
1068
1153
|
prepareIroh: "Prepara iroh",
|
|
1069
1154
|
irohInstallNote: "Il pulsante Installa scarica la release pinnata del sidecar (dsh-mobile-iroh 0.1.0-alpha.1, circa 15 MB su Windows x64, 24 MB su Linux x64) nella directory privata del plugin e ne verifica lo SHA-256. Un complemento installato con una copia verificata viene invece riutilizzato senza scaricare nulla.",
|
|
1070
1155
|
irohComponentNote: "Un sidecar installato viene riverificato tramite SHA-256 a ogni uso. Non è incluso nel pacchetto del plugin; una copia fornita dal complemento può essere rimossa solo rimuovendo il complemento.",
|
|
1071
1156
|
irohPhaseNote: "Connessione sperimentale. Richiede l’App Android completa 0.7.0+ con iroh; APK compatibile e browser mobili non sono supportati.",
|
|
1072
1157
|
irohUnavailable: "iroh non è pronto: nessun sidecar è ancora installato. Installa il componente ufficiale qui sotto per scaricarlo e verificarlo.",
|
|
1158
|
+
irohInstalledNotReady: "iroh è installato ma non in esecuzione. Attiva l’accesso remoto per avviarlo.",
|
|
1073
1159
|
irohStarting: "Risoluzione dell’identità endpoint iroh…",
|
|
1074
1160
|
irohConnecting: "Sidecar connesso; avvio del gateway di autenticazione DSH…",
|
|
1075
1161
|
irohCompanionMissing: "Nessun sidecar iroh verificato disponibile. Usa Installa componente ufficiale qui sotto; il plugin complementare è facoltativo.",
|
|
@@ -1097,12 +1183,13 @@ window.__ModuleLoader__.load({
|
|
|
1097
1183
|
enlargeQr: "放大二维码",
|
|
1098
1184
|
closeEnlargedQr: "关闭放大的二维码",
|
|
1099
1185
|
irohPairingReady: "iroh 配对码 {code} 已生效,有效期 5 分钟。请在 App 的 iroh 配对页扫码;再生成一个会替换当前这个。",
|
|
1100
|
-
irohDescription: "
|
|
1186
|
+
irohDescription: "点对点加密直连,跨网自动经中继。无需域名、端口或账号;需完整版 App。",
|
|
1101
1187
|
prepareIroh: "准备 iroh",
|
|
1102
1188
|
irohInstallNote: "点击安装会下载固定的 sidecar 发行版(dsh-mobile-iroh 0.1.0-alpha.1,Windows x64 约 15 MB,Linux x64 约 24 MB)到插件私有目录并校验 SHA-256;已安装的配套插件如有校验通过的副本会直接复用,不再下载。",
|
|
1103
1189
|
irohComponentNote: "已安装的 sidecar 每次使用都会重新校验 SHA-256。它不随插件包发布;配套提供的副本只能通过移除配套插件来删除。",
|
|
1104
1190
|
irohPhaseNote: "实验性连接。需使用含 iroh 的 Android App 0.7.0 及以上完整版;兼容版 APK 和手机浏览器不支持。",
|
|
1105
1191
|
irohUnavailable: "iroh 未就绪:尚未安装 sidecar,点击下方「安装官方组件」下载并校验。",
|
|
1192
|
+
irohInstalledNotReady: "iroh 已安装但未运行,启用远程访问即可启动。",
|
|
1106
1193
|
irohStarting: "正在解析 iroh 端点身份…",
|
|
1107
1194
|
irohConnecting: "Sidecar 已连接,正在启动 DSH 认证网关…",
|
|
1108
1195
|
irohCompanionMissing: "尚无校验通过的 iroh 组件,请点击下方「安装官方组件」;无需另装配套插件。",
|
|
@@ -1122,6 +1209,7 @@ window.__ModuleLoader__.load({
|
|
|
1122
1209
|
});
|
|
1123
1210
|
Object.assign(MOBILE_CONTROL_MESSAGES.en, {
|
|
1124
1211
|
requestTimeout: "The operation timed out. Confirm that DSH is still running, then try again.",
|
|
1212
|
+
invalidResponse: "The server replied with a body that is not valid JSON. Confirm the plugin and DSH versions match, then reload the page.",
|
|
1125
1213
|
diagnostics: "Diagnostics",
|
|
1126
1214
|
openDiagnostics: "Open connection diagnostics",
|
|
1127
1215
|
back: "Back",
|
|
@@ -1170,6 +1258,7 @@ window.__ModuleLoader__.load({
|
|
|
1170
1258
|
});
|
|
1171
1259
|
Object.assign(MOBILE_CONTROL_MESSAGES.it, {
|
|
1172
1260
|
requestTimeout: "Operazione scaduta. Verifica che DSH sia ancora in esecuzione e riprova.",
|
|
1261
|
+
invalidResponse: "Il server ha restituito un contenuto che non è JSON valido. Verifica che le versioni del plugin e di DSH corrispondano, poi ricarica la pagina.",
|
|
1173
1262
|
diagnostics: "Diagnostica",
|
|
1174
1263
|
openDiagnostics: "Apri diagnostica connessione",
|
|
1175
1264
|
back: "Indietro",
|
|
@@ -1218,6 +1307,7 @@ window.__ModuleLoader__.load({
|
|
|
1218
1307
|
});
|
|
1219
1308
|
Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
|
|
1220
1309
|
requestTimeout: "操作超时,请确认 DSH 仍在运行后重试。",
|
|
1310
|
+
invalidResponse: "服务端返回的内容不是有效的 JSON,请确认插件与 DSH 版本匹配后刷新页面。",
|
|
1221
1311
|
diagnostics: "诊断",
|
|
1222
1312
|
openDiagnostics: "打开连接诊断",
|
|
1223
1313
|
back: "返回",
|
|
@@ -1351,11 +1441,11 @@ window.__ModuleLoader__.load({
|
|
|
1351
1441
|
}
|
|
1352
1442
|
};
|
|
1353
1443
|
Object.assign(MOBILE_CONTROL_MESSAGES.en, {
|
|
1354
|
-
selfHostedDescription: "
|
|
1444
|
+
selfHostedDescription: "For your own server or domain. Does not depend on a third-party tunnel service.",
|
|
1355
1445
|
originName: "Own reverse proxy",
|
|
1356
1446
|
caddyName: "Managed Caddy",
|
|
1357
1447
|
caddyModePreviewNote: "HTTPS form preview differs from the current connection.",
|
|
1358
|
-
originDescription: "
|
|
1448
|
+
originDescription: "Already have an HTTPS domain or reverse proxy (Caddy, Nginx)? Point it at the local DSH backend.",
|
|
1359
1449
|
prepareOrigin: "Connect your HTTPS reverse proxy",
|
|
1360
1450
|
originPublicOrigin: "Public HTTPS address",
|
|
1361
1451
|
originListenHost: "Private listen IP (IPv4 / IPv6)",
|
|
@@ -1410,11 +1500,11 @@ window.__ModuleLoader__.load({
|
|
|
1410
1500
|
originControllerUnavailable: "The reverse-proxy provider is not available in this session. Restart DSH and try again."
|
|
1411
1501
|
});
|
|
1412
1502
|
Object.assign(MOBILE_CONTROL_MESSAGES.it, {
|
|
1413
|
-
selfHostedDescription: "
|
|
1503
|
+
selfHostedDescription: "Per un tuo server o dominio. Non dipende da un servizio tunnel di terze parti.",
|
|
1414
1504
|
originName: "Reverse proxy proprio",
|
|
1415
1505
|
caddyName: "Caddy gestito",
|
|
1416
1506
|
caddyModePreviewNote: "L’anteprima del modulo HTTPS differisce dalla connessione attuale.",
|
|
1417
|
-
originDescription: "
|
|
1507
|
+
originDescription: "Hai già un dominio HTTPS o un reverse proxy (Caddy, Nginx)? Puntalo al backend DSH locale.",
|
|
1418
1508
|
prepareOrigin: "Collega il tuo reverse proxy HTTPS",
|
|
1419
1509
|
originPublicOrigin: "Indirizzo HTTPS pubblico",
|
|
1420
1510
|
originListenHost: "IP privato di ascolto (IPv4 / IPv6)",
|
|
@@ -1469,11 +1559,11 @@ window.__ModuleLoader__.load({
|
|
|
1469
1559
|
originControllerUnavailable: "Il provider reverse proxy non è disponibile in questa sessione. Riavvia DSH e riprova."
|
|
1470
1560
|
});
|
|
1471
1561
|
Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
|
|
1472
|
-
selfHostedDescription: "
|
|
1562
|
+
selfHostedDescription: "有自己的服务器或域名时用这里,不依赖第三方隧道服务。",
|
|
1473
1563
|
originName: "自有反向代理",
|
|
1474
1564
|
caddyName: "托管 Caddy",
|
|
1475
1565
|
caddyModePreviewNote: "HTTPS 表单预览与当前连接不同。",
|
|
1476
|
-
originDescription: "
|
|
1566
|
+
originDescription: "已有 HTTPS 域名或反向代理(Caddy、Nginx 等)?直接指向本机 DSH 后端。",
|
|
1477
1567
|
prepareOrigin: "接入自有 HTTPS 反向代理",
|
|
1478
1568
|
originPublicOrigin: "公网 HTTPS 地址",
|
|
1479
1569
|
originListenHost: "私有监听 IP(IPv4 / IPv6)",
|
|
@@ -1529,13 +1619,13 @@ window.__ModuleLoader__.load({
|
|
|
1529
1619
|
});
|
|
1530
1620
|
Object.assign(MOBILE_CONTROL_MESSAGES.en, {
|
|
1531
1621
|
cloudflaredBadge: "Account optional",
|
|
1532
|
-
cloudflaredDescription: "
|
|
1622
|
+
cloudflaredDescription: "Free temporary address without an account; sign in to Cloudflare for a fixed domain. Installs the official client on demand.",
|
|
1533
1623
|
prepareCloudflared: "Prepare cloudflared",
|
|
1534
1624
|
cloudflaredUnsupported: "Windows x64, Linux x64/arm64, and macOS x64/arm64 are supported.",
|
|
1535
1625
|
cloudflaredNotInstalled: "Not installed. A fixed version is downloaded from the official cloudflared release page only after you click the button below.",
|
|
1536
1626
|
cloudflaredReady: "Official component {version} is ready.",
|
|
1537
1627
|
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.",
|
|
1538
|
-
cloudflaredNamedNote: "Named tunnel: the hostname
|
|
1628
|
+
cloudflaredNamedNote: "Named tunnel: the saved hostname stays the same across restarts and Cloudflare routes it to the local forward port. The connector token is stored only in the DSH Mobile private directory, is handed to cloudflared through its environment rather than the command line, and is never shown again once saved.",
|
|
1539
1629
|
cloudflaredComponentNote: "Downloaded from the official cloudflared release page and verified at a fixed version only after you choose Install. The client is started with automatic updates disabled. It does not add a system service, startup item, registry entry, or PATH entry.",
|
|
1540
1630
|
installConfirmCloudflared: "Download and verify cloudflared {size} from the official release page and store it only in the DSH Mobile private directory? No system service, PATH/registry entry, or startup item is added.",
|
|
1541
1631
|
installingCloudflared: "Installing the official cloudflared component. Keep DSH running until it completes.",
|
|
@@ -1597,13 +1687,13 @@ window.__ModuleLoader__.load({
|
|
|
1597
1687
|
});
|
|
1598
1688
|
Object.assign(MOBILE_CONTROL_MESSAGES.it, {
|
|
1599
1689
|
cloudflaredBadge: "Account facoltativo",
|
|
1600
|
-
cloudflaredDescription: "
|
|
1690
|
+
cloudflaredDescription: "Indirizzo temporaneo gratuito senza account; accedi a Cloudflare per un dominio fisso. Installa il client ufficiale su richiesta.",
|
|
1601
1691
|
prepareCloudflared: "Prepara cloudflared",
|
|
1602
1692
|
cloudflaredUnsupported: "Sono supportati Windows x64, Linux x64/arm64 e macOS x64/arm64.",
|
|
1603
1693
|
cloudflaredNotInstalled: "Non installato. Una versione fissa viene scaricata dalla pagina ufficiale di cloudflared solo dopo aver premuto il pulsante.",
|
|
1604
1694
|
cloudflaredReady: "Componente ufficiale {version} pronto.",
|
|
1605
1695
|
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.",
|
|
1606
|
-
cloudflaredNamedNote: "Tunnel con nome: l’hostname
|
|
1696
|
+
cloudflaredNamedNote: "Tunnel con nome: l’hostname salvato resta lo stesso tra i riavvii e Cloudflare lo instrada verso la porta di inoltro locale. Il token del connettore è salvato solo nella directory privata DSH Mobile, viene passato a cloudflared tramite l’ambiente e non tramite la riga di comando, e dopo il salvataggio non viene più mostrato.",
|
|
1607
1697
|
cloudflaredComponentNote: "Viene scaricato dalla pagina ufficiale di cloudflared e verificato a una versione fissa solo dopo aver scelto Installa. Il client viene avviato con gli aggiornamenti automatici disattivati. Non aggiunge servizi di sistema, avvio automatico, voci di registro o PATH.",
|
|
1608
1698
|
installConfirmCloudflared: "Scaricare e verificare cloudflared {size} dalla pagina ufficiale e salvarlo solo nella directory privata DSH Mobile? Non vengono aggiunti servizi, PATH/registro o avvio automatico.",
|
|
1609
1699
|
installingCloudflared: "Installazione del componente cloudflared ufficiale. Mantieni DSH in esecuzione fino al termine.",
|
|
@@ -1665,13 +1755,13 @@ window.__ModuleLoader__.load({
|
|
|
1665
1755
|
});
|
|
1666
1756
|
Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
|
|
1667
1757
|
cloudflaredBadge: "账号可选",
|
|
1668
|
-
cloudflaredDescription: "
|
|
1758
|
+
cloudflaredDescription: "免账号获得临时地址;登录 Cloudflare 可换固定域名。按需安装官方客户端。",
|
|
1669
1759
|
prepareCloudflared: "准备 cloudflared",
|
|
1670
1760
|
cloudflaredUnsupported: "支持 Windows x64、Linux x64/arm64 与 macOS x64/arm64。",
|
|
1671
1761
|
cloudflaredNotInstalled: "尚未安装。只有点击下方按钮后,才会从 cloudflared 官方发布页下载固定版本。",
|
|
1672
1762
|
cloudflaredReady: "官方组件 {version} 已就绪。",
|
|
1673
1763
|
cloudflaredQuickNote: "快速隧道:无需 Cloudflare 账号,但官方定位是测试用途 —— 地址随机、有限流、无可用性保证。请勿用于必须长期可达的访问。",
|
|
1674
|
-
cloudflaredNamedNote: "
|
|
1764
|
+
cloudflaredNamedNote: "命名隧道:保存的公网域名重启后不变,由 Cloudflare 转发到本机转发端口。连接器令牌只保存在 DSH Mobile 私有目录,经环境变量而非命令行传给 cloudflared,保存后不再显示。",
|
|
1675
1765
|
cloudflaredComponentNote: "仅在你点击安装后从 cloudflared 官方发布页下载并校验固定版本,且启动时关闭自动更新。不会写入系统服务、开机启动、注册表或 PATH。",
|
|
1676
1766
|
installConfirmCloudflared: "将从 cloudflared 官方发布页下载并校验固定版本({size}),仅保存到 DSH Mobile 私有目录。不会安装系统服务、写入 PATH/注册表或设置开机启动。是否继续?",
|
|
1677
1767
|
installingCloudflared: "正在安装 cloudflared 官方组件。完成前请保持 DSH 运行。",
|
|
@@ -3544,6 +3634,163 @@ window.__ModuleLoader__.load({
|
|
|
3544
3634
|
});
|
|
3545
3635
|
}
|
|
3546
3636
|
//#endregion
|
|
3637
|
+
//#region src/mobile-sidebar-scroll.ts
|
|
3638
|
+
function sidebarRestoreTop(snapshot, anchorTop) {
|
|
3639
|
+
return Math.max(0, anchorTop === void 0 || snapshot.offset === void 0 ? snapshot.top : anchorTop - snapshot.offset);
|
|
3640
|
+
}
|
|
3641
|
+
const keyedRows = "[data-row-key]";
|
|
3642
|
+
const hasSuffix = (element, suffix) => [...element.classList].some((token) => token.endsWith(suffix));
|
|
3643
|
+
function visible(element) {
|
|
3644
|
+
return element.isConnected && element.clientHeight > 0 && element.getClientRects().length > 0;
|
|
3645
|
+
}
|
|
3646
|
+
function captureSidebarScroll(element) {
|
|
3647
|
+
const top = element.scrollTop;
|
|
3648
|
+
const bounds = element.getBoundingClientRect();
|
|
3649
|
+
const row = [...element.querySelectorAll(keyedRows)].find((candidate) => {
|
|
3650
|
+
const rect = candidate.getBoundingClientRect();
|
|
3651
|
+
return rect.height > 0 && rect.bottom > bounds.top && rect.top < bounds.bottom;
|
|
3652
|
+
});
|
|
3653
|
+
return row === void 0 ? { top } : {
|
|
3654
|
+
top,
|
|
3655
|
+
anchor: row.getAttribute("data-row-key") ?? "",
|
|
3656
|
+
offset: row.getBoundingClientRect().top - bounds.top
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* Uses browser scrollports/keyed rows only; no React internals, DOM reordering,
|
|
3661
|
+
* forced group expansion or persistent storage of session identities. Absolute
|
|
3662
|
+
* position seeds a virtual window before a mounted keyed anchor refines it.
|
|
3663
|
+
*/
|
|
3664
|
+
function installMobileSidebarScrollMemory() {
|
|
3665
|
+
const memories = /* @__PURE__ */ new Map();
|
|
3666
|
+
const stockViewport = window.matchMedia("(max-width:720px)");
|
|
3667
|
+
const dedicatedViewport = window.matchMedia("(max-width:899px)");
|
|
3668
|
+
let root;
|
|
3669
|
+
let open = false;
|
|
3670
|
+
let ports = /* @__PURE__ */ new Map();
|
|
3671
|
+
let pending = /* @__PURE__ */ new Map();
|
|
3672
|
+
let frame = 0;
|
|
3673
|
+
let deadline = 0;
|
|
3674
|
+
let disposed = false;
|
|
3675
|
+
const isOpen = () => root !== void 0 && visible(root) && !hasSuffix(root, "_collapsed") && root.closest("[data-open=\"false\"]") === null && root.querySelector("[data-dsh-mobile-settings]") === null;
|
|
3676
|
+
const cancel = () => {
|
|
3677
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
3678
|
+
frame = 0;
|
|
3679
|
+
pending.clear();
|
|
3680
|
+
};
|
|
3681
|
+
const remember = () => {
|
|
3682
|
+
if (!open || !isOpen()) return;
|
|
3683
|
+
for (const [port, key] of ports) if (!pending.has(port) && visible(port)) memories.set(key, port === root ? { top: port.scrollTop } : captureSidebarScroll(port));
|
|
3684
|
+
};
|
|
3685
|
+
const restore = () => {
|
|
3686
|
+
frame = 0;
|
|
3687
|
+
if (disposed || !open || !isOpen()) return;
|
|
3688
|
+
for (const [port, state] of pending) {
|
|
3689
|
+
if (!visible(port)) continue;
|
|
3690
|
+
const snapshot = state.snapshot;
|
|
3691
|
+
if (snapshot === void 0) {
|
|
3692
|
+
if (port === root) {
|
|
3693
|
+
pending.delete(port);
|
|
3694
|
+
continue;
|
|
3695
|
+
}
|
|
3696
|
+
const selected = port.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]");
|
|
3697
|
+
if (selected !== null && visible(selected)) {
|
|
3698
|
+
const bounds = port.getBoundingClientRect(), rect = selected.getBoundingClientRect();
|
|
3699
|
+
if (rect.top < bounds.top || rect.bottom > bounds.bottom) port.scrollTop += rect.top - bounds.top - (port.clientHeight - rect.height) / 2;
|
|
3700
|
+
pending.delete(port);
|
|
3701
|
+
} else if (port.querySelector(keyedRows) !== null) pending.delete(port);
|
|
3702
|
+
continue;
|
|
3703
|
+
}
|
|
3704
|
+
const anchor = [...port.querySelectorAll(keyedRows)].find((row) => row.dataset.rowKey === snapshot.anchor);
|
|
3705
|
+
const target = sidebarRestoreTop(snapshot, anchor === void 0 ? void 0 : port.scrollTop + anchor.getBoundingClientRect().top - port.getBoundingClientRect().top);
|
|
3706
|
+
const before = port.scrollTop;
|
|
3707
|
+
port.scrollTop = target;
|
|
3708
|
+
if (Math.abs(port.scrollTop - target) <= 1 && Math.abs(before - port.scrollTop) <= 1) state.stable++;
|
|
3709
|
+
else state.stable = 0;
|
|
3710
|
+
if (state.stable >= 2) pending.delete(port);
|
|
3711
|
+
}
|
|
3712
|
+
if (pending.size > 0 && performance.now() < deadline) frame = requestAnimationFrame(restore);
|
|
3713
|
+
else pending.clear();
|
|
3714
|
+
};
|
|
3715
|
+
const sync = () => {
|
|
3716
|
+
if (disposed) return;
|
|
3717
|
+
const next = document.querySelector("[data-dsh-mobile-sidebar-root]") ?? void 0;
|
|
3718
|
+
const mobile = next?.closest("[data-pane=\"sidebar\"]") != null ? dedicatedViewport.matches : stockViewport.matches;
|
|
3719
|
+
if (next !== root) {
|
|
3720
|
+
cancel();
|
|
3721
|
+
ports.clear();
|
|
3722
|
+
root = next;
|
|
3723
|
+
open = false;
|
|
3724
|
+
}
|
|
3725
|
+
if (!(mobile && isOpen())) {
|
|
3726
|
+
cancel();
|
|
3727
|
+
open = false;
|
|
3728
|
+
ports.clear();
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
const reopening = !open;
|
|
3732
|
+
open = true;
|
|
3733
|
+
if (root === void 0) return;
|
|
3734
|
+
const candidates = [root, ...root.querySelectorAll("[class*=\"_regionArea\"] [class*=\"_list\"]")].filter((port) => port === root || hasSuffix(port, "_list"));
|
|
3735
|
+
const nextPorts = /* @__PURE__ */ new Map();
|
|
3736
|
+
for (const port of candidates) {
|
|
3737
|
+
const key = port === root ? "outer" : `workspace:${[...port.classList].sort().join(" ")}:${root.querySelector("[class*=\"_regionArea\"] input")?.value ?? ""}`;
|
|
3738
|
+
nextPorts.set(port, key);
|
|
3739
|
+
if (reopening || ports.get(port) !== key) {
|
|
3740
|
+
pending.set(port, {
|
|
3741
|
+
snapshot: memories.get(key),
|
|
3742
|
+
stable: 0
|
|
3743
|
+
});
|
|
3744
|
+
deadline = performance.now() + 1200;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
for (const port of pending.keys()) if (!nextPorts.has(port)) pending.delete(port);
|
|
3748
|
+
ports = nextPorts;
|
|
3749
|
+
if (pending.size > 0 && frame === 0) frame = requestAnimationFrame(restore);
|
|
3750
|
+
};
|
|
3751
|
+
const onScroll = (event) => {
|
|
3752
|
+
const port = event.target;
|
|
3753
|
+
if (!(port instanceof HTMLElement) || !open || !isOpen() || pending.has(port) || !visible(port)) return;
|
|
3754
|
+
const key = ports.get(port);
|
|
3755
|
+
if (key !== void 0) memories.set(key, port === root ? { top: port.scrollTop } : captureSidebarScroll(port));
|
|
3756
|
+
};
|
|
3757
|
+
const onIntent = (event) => {
|
|
3758
|
+
if (!(event.target instanceof Node) || !root?.contains(event.target)) return;
|
|
3759
|
+
cancel();
|
|
3760
|
+
remember();
|
|
3761
|
+
};
|
|
3762
|
+
stockViewport.addEventListener("change", sync);
|
|
3763
|
+
dedicatedViewport.addEventListener("change", sync);
|
|
3764
|
+
document.addEventListener("scroll", onScroll, true);
|
|
3765
|
+
for (const type of [
|
|
3766
|
+
"pointerdown",
|
|
3767
|
+
"touchstart",
|
|
3768
|
+
"wheel",
|
|
3769
|
+
"keydown",
|
|
3770
|
+
"click"
|
|
3771
|
+
]) document.addEventListener(type, onIntent, true);
|
|
3772
|
+
return {
|
|
3773
|
+
sync,
|
|
3774
|
+
dispose: () => {
|
|
3775
|
+
disposed = true;
|
|
3776
|
+
cancel();
|
|
3777
|
+
stockViewport.removeEventListener("change", sync);
|
|
3778
|
+
dedicatedViewport.removeEventListener("change", sync);
|
|
3779
|
+
document.removeEventListener("scroll", onScroll, true);
|
|
3780
|
+
for (const type of [
|
|
3781
|
+
"pointerdown",
|
|
3782
|
+
"touchstart",
|
|
3783
|
+
"wheel",
|
|
3784
|
+
"keydown",
|
|
3785
|
+
"click"
|
|
3786
|
+
]) document.removeEventListener(type, onIntent, true);
|
|
3787
|
+
ports.clear();
|
|
3788
|
+
memories.clear();
|
|
3789
|
+
root = void 0;
|
|
3790
|
+
}
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
//#endregion
|
|
3547
3794
|
//#region src/native-mobile.ts
|
|
3548
3795
|
/**
|
|
3549
3796
|
* Viewport below which the injected chrome becomes an overlay drawer: the
|
|
@@ -3590,10 +3837,30 @@ window.__ModuleLoader__.load({
|
|
|
3590
3837
|
.dsh-mobile-settings_fontFamily > span { flex:1 1 100px; }
|
|
3591
3838
|
.dsh-mobile-settings_fontFamily input { box-sizing:border-box; flex:1 1 180px; width:100%; min-width:0; min-height:48px; padding:8px 12px; border:0.5px solid var(--dsw-alias-border-l2); border-radius:10px; background:var(--dsw-alias-bg-module-platform); color:var(--dsw-alias-label-primary); font:inherit; }
|
|
3592
3839
|
.dsh-mobile-settings_fontFamily input:focus-visible,.dsh-mobile-settings_fontDetails > summary:focus-visible { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
|
|
3840
|
+
/* Push category rows: compact label-left / native-Switch-right rows. The control is
|
|
3841
|
+
the actual public @deepseek-ai/dsh-client-ui-primitives Switch provided by the DSH
|
|
3842
|
+
web shell: its track/thumb paint, dimensions, focus ring and motion are owned by
|
|
3843
|
+
the shell stylesheet and are never re-created, resized or overridden here. Each
|
|
3844
|
+
row is a label associated with its switch button, so the full 48px row remains
|
|
3845
|
+
one tap target while the button keeps native keyboard semantics. Category names
|
|
3846
|
+
match the General settings row title typography (14px/20px, regular weight). */
|
|
3847
|
+
.dsh-mobile-push-categories { display:flex; flex-direction:column; width:100%; margin:8px 0 4px; border-top:0.5px solid var(--dsw-alias-border-l2,#e8ebef); }
|
|
3848
|
+
.dsh-mobile-push-category { display:flex; align-items:center; justify-content:space-between; gap:12px; box-sizing:border-box; width:100%; min-height:48px; padding:6px 0; border-bottom:0.5px solid var(--dsw-alias-border-l2,#e8ebef); cursor:pointer; }
|
|
3849
|
+
.dsh-mobile-push-categoryName { flex:1 1 auto; min-width:0; color:var(--dsw-alias-label-primary,#171a21); font-size:14px; line-height:20px; font-weight:400; overflow-wrap:anywhere; }
|
|
3593
3850
|
html.dsh-native-mobile-active [data-dsh-mobile-settings-header] { box-sizing:border-box !important; height:56px !important; min-height:56px !important; padding:4px 12px !important; }
|
|
3594
3851
|
html.dsh-native-mobile-active [data-dsh-mobile-settings-header] [class*="_close"] { box-sizing:border-box !important; flex:none !important; width:48px !important; min-width:48px !important; height:48px !important; min-height:48px !important; }
|
|
3595
3852
|
/* The mobile row replaces the Host-backed font control, not the theme selector. */
|
|
3596
3853
|
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; }
|
|
3854
|
+
/* The injected attachment actions exist on every viewport, not only under the
|
|
3855
|
+
narrow overlay: a tablet shows the same Add menu, and with no native item to
|
|
3856
|
+
mirror they fell back to an unstyled UA button box (bordered, centred). Keep
|
|
3857
|
+
this fallback outside the overlay query so the rows read like native ones. */
|
|
3858
|
+
.dsh-mobile-media-action { box-sizing:border-box; display:flex; align-items:center; justify-content:flex-start; gap:8px; width:100%; min-width:0; min-height:40px; padding:8px 10px; border:0; border-radius:10px; background:transparent; color:var(--dsw-alias-label-primary,inherit); cursor:pointer; font:inherit; font-size:14px; line-height:22px; text-align:left; touch-action:manipulation; }
|
|
3859
|
+
.dsh-mobile-media-action:active { opacity:.72; }
|
|
3860
|
+
.dsh-mobile-media-action:focus-visible { outline:2px solid var(--dsw-alias-state-business-primary,#4c82f7); outline-offset:1px; }
|
|
3861
|
+
.dsh-mobile-media-action:disabled { cursor:default; opacity:.38; }
|
|
3862
|
+
.dsh-mobile-media-action svg { flex:none; width:16px; height:16px; color:var(--dsw-alias-label-tertiary,currentColor); }
|
|
3863
|
+
.dsh-mobile-media-action span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
3597
3864
|
@media ${NATIVE_MOBILE_OVERLAY_QUERY} {
|
|
3598
3865
|
html.dsh-native-mobile-active,html.dsh-native-mobile-active body { width:100%; height:100%; overflow:hidden; }
|
|
3599
3866
|
html.dsh-native-mobile-active { --dsh-mobile-motion-duration:200ms; --dsh-mobile-motion-ease:cubic-bezier(.22,1,.36,1); }
|
|
@@ -3649,6 +3916,8 @@ window.__ModuleLoader__.load({
|
|
|
3649
3916
|
[data-dsh-mobile-sidebar][data-open="true"]:not(:has([data-dsh-mobile-settings])) [data-dsh-mobile-sidebar-root] > [class*="_regionArea"] { flex:1 0 50% !important; }
|
|
3650
3917
|
[data-dsh-mobile-sidebar][data-open="true"] [data-dsh-mobile-sidebar-root] [class*="_logoRow"] { height:52px !important; padding:4px 0 4px 4px !important; margin-bottom:4px !important; }
|
|
3651
3918
|
[data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] { width:0 !important; border:0 !important; background:transparent !important; box-shadow:none !important; overflow:visible !important; }
|
|
3919
|
+
[data-dsh-mobile-sidebar][data-open="false"]:not(:has([data-dsh-mobile-settings])) [data-dsh-mobile-sidebar-root] { box-sizing:border-box !important; min-width:0 !important; padding:0 !important; pointer-events:none !important; }
|
|
3920
|
+
[data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-toggle] { pointer-events:auto !important; }
|
|
3652
3921
|
[data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] > :not(:has([data-dsh-mobile-toggle])) { display:none !important; }
|
|
3653
3922
|
[data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] > :has([data-dsh-mobile-toggle]) { position:fixed !important; z-index:244 !important; top:env(safe-area-inset-top) !important; left:0 !important; box-sizing:border-box !important; width:50px !important; height:52px !important; padding:4px !important; border:0 !important; background:transparent !important; }
|
|
3654
3923
|
[data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] > :has([data-dsh-mobile-toggle]) > :not([data-dsh-mobile-toggle]) { display:none !important; }
|
|
@@ -3777,14 +4046,6 @@ window.__ModuleLoader__.load({
|
|
|
3777
4046
|
[data-dsh-mobile-center] [role="dialog"][aria-label*="Context"] :is(dt,dd) { white-space:nowrap !important; word-break:keep-all !important; }
|
|
3778
4047
|
.dsh-mobile-branch-toast,.dsh-mobile-media-toast { display:block; position:fixed; z-index:330; top:max(12px,env(safe-area-inset-top)); left:50%; max-width:calc(100vw - 32px); box-sizing:border-box; padding:7px 14px; border:1px solid rgb(15 23 42 / 10%); border-radius:999px; background:rgb(15 23 42 / 92%); color:#fff; font-size:13px; line-height:20px; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; opacity:0; pointer-events:none; transform:translate(-50%,-8px); transition:opacity 160ms ease,transform 160ms ease; }
|
|
3779
4048
|
.dsh-mobile-branch-toast[data-visible="true"],.dsh-mobile-media-toast[data-visible="true"] { opacity:1; transform:translate(-50%,0); }
|
|
3780
|
-
/* Fallback only: when the native Add group has no item to clone, keep the
|
|
3781
|
-
camera action on the same row metrics and tokens as the native menu. */
|
|
3782
|
-
.dsh-mobile-media-action { box-sizing:border-box; display:flex; align-items:center; justify-content:flex-start; gap:8px; width:100%; min-width:0; min-height:40px; padding:8px 10px; border:0; border-radius:10px; background:transparent; color:var(--dsw-alias-label-primary,inherit); cursor:pointer; font:inherit; font-size:14px; line-height:22px; text-align:left; touch-action:manipulation; }
|
|
3783
|
-
.dsh-mobile-media-action:active { opacity:.72; }
|
|
3784
|
-
.dsh-mobile-media-action:focus-visible { outline:2px solid var(--dsw-alias-state-business-primary,#4c82f7); outline-offset:1px; }
|
|
3785
|
-
.dsh-mobile-media-action:disabled { cursor:default; opacity:.38; }
|
|
3786
|
-
.dsh-mobile-media-action svg { flex:none; width:16px; height:16px; color:var(--dsw-alias-label-tertiary,currentColor); }
|
|
3787
|
-
.dsh-mobile-media-action span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
3788
4049
|
[data-dsh-mobile-center] [class*="_composer"] { padding-left:8px !important; padding-right:8px !important; padding-bottom:max(8px,env(safe-area-inset-bottom)) !important; }
|
|
3789
4050
|
/* Collapse the scroll viewport only; the editor, draft and attachments stay
|
|
3790
4051
|
owned by DSH. Toolbar focus keeps the card expanded during menu taps. */
|
|
@@ -3869,6 +4130,19 @@ window.__ModuleLoader__.load({
|
|
|
3869
4130
|
return classToken(candidate, "_frame") && firstByClassSuffix(candidate, "_sidebarCol") !== void 0 && firstByClassSuffix(candidate, "_centerCol") !== void 0;
|
|
3870
4131
|
});
|
|
3871
4132
|
}
|
|
4133
|
+
/**
|
|
4134
|
+
* Mirror the classes of the stock Add-menu item on an injected attachment action.
|
|
4135
|
+
*
|
|
4136
|
+
* An empty class name is not nullish: trusting it would strip the plugin's own
|
|
4137
|
+
* fallback class and leave a bordered, centred UA button on that Host version.
|
|
4138
|
+
* A stock item that does carry classes still wins, so the injected action keeps
|
|
4139
|
+
* following the native menu's spacing, colours and dark-mode states.
|
|
4140
|
+
*/
|
|
4141
|
+
function mediaActionClassName(nativeItem) {
|
|
4142
|
+
const nativeClass = nativeItem?.className;
|
|
4143
|
+
const trimmed = typeof nativeClass === "string" ? nativeClass.trim() : "";
|
|
4144
|
+
return trimmed === "" ? "dsh-mobile-media-action" : trimmed;
|
|
4145
|
+
}
|
|
3872
4146
|
/** Mark every mounted settings dialog so its mobile layout does not depend on the conversation shell. */
|
|
3873
4147
|
function markNativeMobileSettings(root) {
|
|
3874
4148
|
let marked = 0;
|
|
@@ -4794,7 +5068,7 @@ window.__ModuleLoader__.load({
|
|
|
4794
5068
|
}
|
|
4795
5069
|
})();
|
|
4796
5070
|
for (const button of [cameraButton, galleryButton]) {
|
|
4797
|
-
const className = nativeItem
|
|
5071
|
+
const className = mediaActionClassName(nativeItem);
|
|
4798
5072
|
if (button.className !== className) button.className = className;
|
|
4799
5073
|
const icon = button.firstElementChild;
|
|
4800
5074
|
const caption = button.lastElementChild;
|
|
@@ -5000,6 +5274,7 @@ window.__ModuleLoader__.load({
|
|
|
5000
5274
|
document.addEventListener("focusin", onStripFocus, true);
|
|
5001
5275
|
document.addEventListener("click", onStripClickCapture, true);
|
|
5002
5276
|
document.addEventListener("click", onBranchClick, true);
|
|
5277
|
+
const sidebarScrollMemory = installMobileSidebarScrollMemory();
|
|
5003
5278
|
let frame;
|
|
5004
5279
|
let sidebar;
|
|
5005
5280
|
let sidebarRoot;
|
|
@@ -5232,7 +5507,10 @@ window.__ModuleLoader__.load({
|
|
|
5232
5507
|
const lastColumn = frame?.style.gridTemplateColumns.trim().split(/\s+/).at(-1);
|
|
5233
5508
|
details.dataset.open = String(lastColumn !== void 0 && lastColumn !== "0px" && lastColumn !== "0");
|
|
5234
5509
|
}
|
|
5235
|
-
if (sidebar === void 0)
|
|
5510
|
+
if (sidebar === void 0) {
|
|
5511
|
+
sidebarScrollMemory.sync();
|
|
5512
|
+
return;
|
|
5513
|
+
}
|
|
5236
5514
|
sidebar.dataset.dshMobileSidebar = "true";
|
|
5237
5515
|
toggle = firstByClassSuffix(sidebar, "_toggle");
|
|
5238
5516
|
let candidate = toggle?.parentElement;
|
|
@@ -5244,6 +5522,7 @@ window.__ModuleLoader__.load({
|
|
|
5244
5522
|
if (toggle !== void 0) toggle.dataset.dshMobileToggle = "true";
|
|
5245
5523
|
if (frame !== void 0) sidebar.dataset.open = String(!classToken(sidebarRoot, "_collapsed"));
|
|
5246
5524
|
backdrop.hidden = frame === void 0 || !drawerScrimVisible(classToken(sidebarRoot, "_collapsed"), overlayQuery.matches);
|
|
5525
|
+
sidebarScrollMemory.sync();
|
|
5247
5526
|
};
|
|
5248
5527
|
const schedule = () => {
|
|
5249
5528
|
if (scheduled === 0) scheduled = requestAnimationFrame(sync);
|
|
@@ -5308,6 +5587,7 @@ window.__ModuleLoader__.load({
|
|
|
5308
5587
|
return () => {
|
|
5309
5588
|
disposed = true;
|
|
5310
5589
|
stopStockBack();
|
|
5590
|
+
sidebarScrollMemory.dispose();
|
|
5311
5591
|
disposeTaskWatcher();
|
|
5312
5592
|
mediaRequestGeneration += 1;
|
|
5313
5593
|
mediaPickerAbortController.abort();
|
|
@@ -8414,6 +8694,779 @@ window.__ModuleLoader__.load({
|
|
|
8414
8694
|
};
|
|
8415
8695
|
}
|
|
8416
8696
|
//#endregion
|
|
8697
|
+
//#region src/mobile-push/browser-api.ts
|
|
8698
|
+
function apiError(code) {
|
|
8699
|
+
return Object.assign(/* @__PURE__ */ new Error(code === "stale_generation" ? "Push settings changed elsewhere." : "Push request failed."), { code });
|
|
8700
|
+
}
|
|
8701
|
+
async function check(response) {
|
|
8702
|
+
if (response.ok) return;
|
|
8703
|
+
let code = "request_failed";
|
|
8704
|
+
if (response.status === 409) try {
|
|
8705
|
+
if ((await response.json())?.error === "stale_generation") code = "stale_generation";
|
|
8706
|
+
} catch {}
|
|
8707
|
+
throw apiError(code);
|
|
8708
|
+
}
|
|
8709
|
+
async function json(response) {
|
|
8710
|
+
try {
|
|
8711
|
+
return await response.json();
|
|
8712
|
+
} catch {
|
|
8713
|
+
throw apiError("request_failed");
|
|
8714
|
+
}
|
|
8715
|
+
}
|
|
8716
|
+
async function requestOnce(request, path, init) {
|
|
8717
|
+
try {
|
|
8718
|
+
return await request(path, init);
|
|
8719
|
+
} catch {
|
|
8720
|
+
throw apiError("request_failed");
|
|
8721
|
+
}
|
|
8722
|
+
}
|
|
8723
|
+
function postJson(request, path, value) {
|
|
8724
|
+
return requestOnce(request, path, {
|
|
8725
|
+
method: "POST",
|
|
8726
|
+
headers: { "content-type": "application/json" },
|
|
8727
|
+
body: JSON.stringify(value)
|
|
8728
|
+
});
|
|
8729
|
+
}
|
|
8730
|
+
/** Build the direct authenticated browser API over mobileRequest (single-shot, same-origin, CSRF-aware). */
|
|
8731
|
+
function createMobilePushBrowserApi(request) {
|
|
8732
|
+
return {
|
|
8733
|
+
async getConfig(installationId) {
|
|
8734
|
+
const response = await requestOnce(request, `/mobile-access/push/config?installationId=${encodeURIComponent(installationId)}`, { method: "GET" });
|
|
8735
|
+
await check(response);
|
|
8736
|
+
return json(response);
|
|
8737
|
+
},
|
|
8738
|
+
async enroll(value) {
|
|
8739
|
+
const response = await postJson(request, "/mobile-access/push/subscription", value);
|
|
8740
|
+
await check(response);
|
|
8741
|
+
return json(response);
|
|
8742
|
+
},
|
|
8743
|
+
async updatePreferences(value) {
|
|
8744
|
+
await check(await postJson(request, "/mobile-access/push/preferences", value));
|
|
8745
|
+
},
|
|
8746
|
+
async disable(value) {
|
|
8747
|
+
await check(await postJson(request, "/mobile-access/push/disable", value));
|
|
8748
|
+
},
|
|
8749
|
+
async sendTest(value) {
|
|
8750
|
+
const response = await postJson(request, "/mobile-access/push/test", { installationId: value.installationId });
|
|
8751
|
+
if (response.status === 429) throw apiError("rate_limited");
|
|
8752
|
+
await check(response);
|
|
8753
|
+
},
|
|
8754
|
+
async resolve(reference) {
|
|
8755
|
+
const response = await postJson(request, "/mobile-access/push/resolve", { reference });
|
|
8756
|
+
await check(response);
|
|
8757
|
+
const value = await json(response);
|
|
8758
|
+
if (typeof value !== "object" || value === null || !("sessionId" in value) || typeof value.sessionId !== "string" || value.sessionId === "" || !("interaction" in value) || value.interaction !== "current" && value.interaction !== "resolved") return void 0;
|
|
8759
|
+
return value;
|
|
8760
|
+
}
|
|
8761
|
+
};
|
|
8762
|
+
}
|
|
8763
|
+
//#endregion
|
|
8764
|
+
//#region src/mobile-push/installation-id.ts
|
|
8765
|
+
/** Browser-safe installation-id validation shared by the browser controller and the Gateway. */
|
|
8766
|
+
const INSTALLATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
8767
|
+
/** True only for a UUID-shaped installation id with an allowed version and variant nibble. */
|
|
8768
|
+
function isPushInstallationId(value) {
|
|
8769
|
+
return typeof value === "string" && INSTALLATION_ID.test(value);
|
|
8770
|
+
}
|
|
8771
|
+
//#endregion
|
|
8772
|
+
//#region src/mobile-push/client.ts
|
|
8773
|
+
const ID_KEY = "dsh-mobile-push-installation";
|
|
8774
|
+
const defaults = Object.freeze({
|
|
8775
|
+
question: true,
|
|
8776
|
+
plan: true,
|
|
8777
|
+
approval: true,
|
|
8778
|
+
response: true,
|
|
8779
|
+
goal: true,
|
|
8780
|
+
failure: true
|
|
8781
|
+
});
|
|
8782
|
+
function decodeKey(encoded) {
|
|
8783
|
+
const normalized = encoded.replace(/-/gu, "+").replace(/_/gu, "/");
|
|
8784
|
+
const raw = atob(normalized + "=".repeat((4 - normalized.length % 4) % 4));
|
|
8785
|
+
return Uint8Array.from(raw, (char) => char.charCodeAt(0));
|
|
8786
|
+
}
|
|
8787
|
+
function safeError(error) {
|
|
8788
|
+
return (error && typeof error === "object" && "code" in error ? String(error.code) : "") === "stale_generation" ? "push.error.stale" : "push.error.generic";
|
|
8789
|
+
}
|
|
8790
|
+
function sameKey(subscription, key) {
|
|
8791
|
+
const raw = subscription.options.applicationServerKey;
|
|
8792
|
+
if (!raw) return false;
|
|
8793
|
+
const bytes = new Uint8Array(raw instanceof ArrayBuffer ? raw : raw.buffer, raw instanceof ArrayBuffer ? 0 : raw.byteOffset, raw instanceof ArrayBuffer ? raw.byteLength : raw.byteLength);
|
|
8794
|
+
return bytes.length === key.length && bytes.every((value, index) => value === key[index]);
|
|
8795
|
+
}
|
|
8796
|
+
/** Construct with a caller-owned authenticated API adapter and an explicit public window environment. */
|
|
8797
|
+
function createMobilePushController(api, environment) {
|
|
8798
|
+
let snapshot = Object.freeze({ state: "unsupported" });
|
|
8799
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
8800
|
+
let active = false;
|
|
8801
|
+
let generation = 0;
|
|
8802
|
+
let config;
|
|
8803
|
+
let preparedKey;
|
|
8804
|
+
let existing = null;
|
|
8805
|
+
let preparedInstallId = "";
|
|
8806
|
+
let memoryInstallId = "";
|
|
8807
|
+
let mutationBusy = false;
|
|
8808
|
+
let testInFlight = false;
|
|
8809
|
+
let testOwner = 0;
|
|
8810
|
+
let attempt = 0;
|
|
8811
|
+
let ownedSubscription;
|
|
8812
|
+
let currentHostSubscription;
|
|
8813
|
+
const consentedSubscriptions = /* @__PURE__ */ new WeakSet();
|
|
8814
|
+
const pendingEnrollments = /* @__PURE__ */ new WeakMap();
|
|
8815
|
+
const trackPendingEnrollment = (subscription, operation) => {
|
|
8816
|
+
let pending = pendingEnrollments.get(subscription);
|
|
8817
|
+
if (!pending) {
|
|
8818
|
+
pending = /* @__PURE__ */ new Set();
|
|
8819
|
+
pendingEnrollments.set(subscription, pending);
|
|
8820
|
+
}
|
|
8821
|
+
pending.add(operation);
|
|
8822
|
+
};
|
|
8823
|
+
const finishPendingEnrollment = (subscription, operation) => {
|
|
8824
|
+
const pending = pendingEnrollments.get(subscription);
|
|
8825
|
+
pending?.delete(operation);
|
|
8826
|
+
if (pending?.size === 0) pendingEnrollments.delete(subscription);
|
|
8827
|
+
};
|
|
8828
|
+
const mayUnsubscribeCreatedSubscription = (subscription) => !consentedSubscriptions.has(subscription) && (pendingEnrollments.get(subscription)?.size ?? 0) === 0;
|
|
8829
|
+
const publish = (value) => {
|
|
8830
|
+
snapshot = Object.freeze({
|
|
8831
|
+
...value,
|
|
8832
|
+
testInFlight
|
|
8833
|
+
});
|
|
8834
|
+
for (const listener of listeners) listener();
|
|
8835
|
+
};
|
|
8836
|
+
const current = (token) => active && generation === token;
|
|
8837
|
+
const getInstallationId = () => {
|
|
8838
|
+
if (memoryInstallId) return memoryInstallId;
|
|
8839
|
+
try {
|
|
8840
|
+
const saved = environment.storage?.getItem(ID_KEY);
|
|
8841
|
+
if (isPushInstallationId(saved)) return memoryInstallId = saved;
|
|
8842
|
+
} catch {}
|
|
8843
|
+
const fresh = environment.randomUUID?.() ?? globalThis.crypto?.randomUUID?.();
|
|
8844
|
+
if (!isPushInstallationId(fresh)) throw new Error("uuid unavailable");
|
|
8845
|
+
memoryInstallId = fresh;
|
|
8846
|
+
try {
|
|
8847
|
+
environment.storage?.setItem(ID_KEY, fresh);
|
|
8848
|
+
} catch {}
|
|
8849
|
+
return fresh;
|
|
8850
|
+
};
|
|
8851
|
+
const detect = () => {
|
|
8852
|
+
if (!environment.secureContext || !environment.isTopLevel || !environment.pushManager || environment.permission === "unsupported") return "unsupported";
|
|
8853
|
+
if (!environment.standalone) return "install";
|
|
8854
|
+
if (environment.permission === "denied") return "denied";
|
|
8855
|
+
};
|
|
8856
|
+
const refresh = async (token) => {
|
|
8857
|
+
const blocked = detect();
|
|
8858
|
+
if (blocked) {
|
|
8859
|
+
if (current(token)) publish({ state: blocked });
|
|
8860
|
+
return false;
|
|
8861
|
+
}
|
|
8862
|
+
try {
|
|
8863
|
+
const requestedInstallId = getInstallationId();
|
|
8864
|
+
const [nextConfig, subscription] = await Promise.all([api.getConfig(requestedInstallId), environment.pushManager.getSubscription()]);
|
|
8865
|
+
if (!current(token)) return false;
|
|
8866
|
+
config = nextConfig;
|
|
8867
|
+
existing = subscription;
|
|
8868
|
+
preparedInstallId = nextConfig.installationId || requestedInstallId;
|
|
8869
|
+
memoryInstallId = preparedInstallId;
|
|
8870
|
+
try {
|
|
8871
|
+
environment.storage?.setItem(ID_KEY, preparedInstallId);
|
|
8872
|
+
} catch {}
|
|
8873
|
+
preparedKey = nextConfig.publicKey ? decodeKey(nextConfig.publicKey) : void 0;
|
|
8874
|
+
if (!nextConfig.available || !preparedKey) publish({
|
|
8875
|
+
state: "unsupported",
|
|
8876
|
+
preferences: nextConfig.preferences
|
|
8877
|
+
});
|
|
8878
|
+
else if (nextConfig.status === "enabled") {
|
|
8879
|
+
if (subscription) {
|
|
8880
|
+
currentHostSubscription = subscription;
|
|
8881
|
+
consentedSubscriptions.add(subscription);
|
|
8882
|
+
}
|
|
8883
|
+
publish({
|
|
8884
|
+
state: nextConfig.status,
|
|
8885
|
+
preferences: nextConfig.preferences
|
|
8886
|
+
});
|
|
8887
|
+
} else if (nextConfig.status === "suspended") {
|
|
8888
|
+
if (subscription) {
|
|
8889
|
+
currentHostSubscription = subscription;
|
|
8890
|
+
consentedSubscriptions.add(subscription);
|
|
8891
|
+
}
|
|
8892
|
+
publish({
|
|
8893
|
+
state: "suspended",
|
|
8894
|
+
preferences: nextConfig.preferences
|
|
8895
|
+
});
|
|
8896
|
+
} else {
|
|
8897
|
+
if (nextConfig.status === "disabled" && currentHostSubscription) {
|
|
8898
|
+
consentedSubscriptions.delete(currentHostSubscription);
|
|
8899
|
+
currentHostSubscription = void 0;
|
|
8900
|
+
}
|
|
8901
|
+
publish({
|
|
8902
|
+
state: "ready",
|
|
8903
|
+
preferences: nextConfig.preferences
|
|
8904
|
+
});
|
|
8905
|
+
}
|
|
8906
|
+
return true;
|
|
8907
|
+
} catch (error) {
|
|
8908
|
+
if (current(token)) publish({
|
|
8909
|
+
state: "failed",
|
|
8910
|
+
error: safeError(error)
|
|
8911
|
+
});
|
|
8912
|
+
return false;
|
|
8913
|
+
}
|
|
8914
|
+
};
|
|
8915
|
+
const refreshNow = () => {
|
|
8916
|
+
if (active) refresh(generation);
|
|
8917
|
+
};
|
|
8918
|
+
return {
|
|
8919
|
+
start() {
|
|
8920
|
+
active = true;
|
|
8921
|
+
generation += 1;
|
|
8922
|
+
config = void 0;
|
|
8923
|
+
ownedSubscription = void 0;
|
|
8924
|
+
publish({ state: "pending" });
|
|
8925
|
+
refreshNow();
|
|
8926
|
+
},
|
|
8927
|
+
stop() {
|
|
8928
|
+
active = false;
|
|
8929
|
+
generation += 1;
|
|
8930
|
+
attempt += 1;
|
|
8931
|
+
mutationBusy = false;
|
|
8932
|
+
testOwner += 1;
|
|
8933
|
+
if (testInFlight) {
|
|
8934
|
+
testInFlight = false;
|
|
8935
|
+
publish({ ...snapshot });
|
|
8936
|
+
}
|
|
8937
|
+
config = void 0;
|
|
8938
|
+
preparedKey = void 0;
|
|
8939
|
+
existing = null;
|
|
8940
|
+
ownedSubscription = void 0;
|
|
8941
|
+
},
|
|
8942
|
+
refresh: refreshNow,
|
|
8943
|
+
async enableFromGesture() {
|
|
8944
|
+
if (!active || mutationBusy || snapshot.state !== "ready" && snapshot.state !== "failed" && snapshot.state !== "disabled" || !config || !preparedKey || !environment.pushManager) return;
|
|
8945
|
+
const token = generation, operation = ++attempt, base = config;
|
|
8946
|
+
mutationBusy = true;
|
|
8947
|
+
publish({
|
|
8948
|
+
state: "pending",
|
|
8949
|
+
preferences: base.preferences
|
|
8950
|
+
});
|
|
8951
|
+
let subscription = existing;
|
|
8952
|
+
let created = false;
|
|
8953
|
+
let pendingSubscription;
|
|
8954
|
+
try {
|
|
8955
|
+
if (subscription && !sameKey(subscription, preparedKey)) {
|
|
8956
|
+
publish({
|
|
8957
|
+
state: "failed",
|
|
8958
|
+
error: "push.error.subscriptionConflict",
|
|
8959
|
+
preferences: base.preferences
|
|
8960
|
+
});
|
|
8961
|
+
return;
|
|
8962
|
+
}
|
|
8963
|
+
if (!subscription) {
|
|
8964
|
+
subscription = await environment.pushManager.subscribe({
|
|
8965
|
+
userVisibleOnly: true,
|
|
8966
|
+
applicationServerKey: preparedKey
|
|
8967
|
+
});
|
|
8968
|
+
created = true;
|
|
8969
|
+
}
|
|
8970
|
+
if (created) ownedSubscription = subscription;
|
|
8971
|
+
if (!current(token) || operation !== attempt) {
|
|
8972
|
+
if (created && mayUnsubscribeCreatedSubscription(subscription)) {
|
|
8973
|
+
if (await subscription.unsubscribe().catch(() => false) && ownedSubscription === subscription) ownedSubscription = void 0;
|
|
8974
|
+
}
|
|
8975
|
+
return;
|
|
8976
|
+
}
|
|
8977
|
+
trackPendingEnrollment(subscription, operation);
|
|
8978
|
+
pendingSubscription = subscription;
|
|
8979
|
+
const browserSubscription = subscription.toJSON();
|
|
8980
|
+
const result = await api.enroll({
|
|
8981
|
+
installationId: preparedInstallId || base.installationId,
|
|
8982
|
+
subscription: {
|
|
8983
|
+
endpoint: browserSubscription.endpoint,
|
|
8984
|
+
keys: {
|
|
8985
|
+
p256dh: browserSubscription.keys.p256dh,
|
|
8986
|
+
auth: browserSubscription.keys.auth
|
|
8987
|
+
}
|
|
8988
|
+
},
|
|
8989
|
+
locale: environment.locale ?? "en",
|
|
8990
|
+
preferences: base.preferences ?? defaults,
|
|
8991
|
+
keyFingerprint: base.keyFingerprint ?? "",
|
|
8992
|
+
ownerGeneration: base.ownerGeneration,
|
|
8993
|
+
registrationGeneration: base.registrationGeneration
|
|
8994
|
+
});
|
|
8995
|
+
finishPendingEnrollment(subscription, operation);
|
|
8996
|
+
pendingSubscription = void 0;
|
|
8997
|
+
if (!current(token) || operation !== attempt) {
|
|
8998
|
+
if (result.mutationGeneration !== void 0) await api.disable({
|
|
8999
|
+
installationId: result.registration.installationId,
|
|
9000
|
+
ifGeneration: result.mutationGeneration
|
|
9001
|
+
}).catch(() => void 0);
|
|
9002
|
+
if (created && mayUnsubscribeCreatedSubscription(subscription)) {
|
|
9003
|
+
if (await subscription.unsubscribe().catch(() => false) && ownedSubscription === subscription) ownedSubscription = void 0;
|
|
9004
|
+
}
|
|
9005
|
+
return;
|
|
9006
|
+
}
|
|
9007
|
+
if (created) ownedSubscription = subscription;
|
|
9008
|
+
consentedSubscriptions.add(subscription);
|
|
9009
|
+
currentHostSubscription = subscription;
|
|
9010
|
+
if (result.registration.origin === environment.origin && result.registration.installationId) {
|
|
9011
|
+
memoryInstallId = result.registration.installationId;
|
|
9012
|
+
preparedInstallId = memoryInstallId;
|
|
9013
|
+
try {
|
|
9014
|
+
environment.storage?.setItem(ID_KEY, memoryInstallId);
|
|
9015
|
+
} catch {}
|
|
9016
|
+
}
|
|
9017
|
+
config = {
|
|
9018
|
+
...base,
|
|
9019
|
+
installationId: preparedInstallId || result.registration.installationId,
|
|
9020
|
+
registrationGeneration: result.registration.generation,
|
|
9021
|
+
status: result.registration.health === "suspended" ? "suspended" : "enabled",
|
|
9022
|
+
preferences: result.registration.preferences
|
|
9023
|
+
};
|
|
9024
|
+
publish({
|
|
9025
|
+
state: result.registration.health === "suspended" ? "suspended" : "enabled",
|
|
9026
|
+
preferences: result.registration.preferences
|
|
9027
|
+
});
|
|
9028
|
+
} catch (error) {
|
|
9029
|
+
if (pendingSubscription) finishPendingEnrollment(pendingSubscription, operation);
|
|
9030
|
+
if (created && subscription && mayUnsubscribeCreatedSubscription(subscription)) {
|
|
9031
|
+
if (await subscription.unsubscribe().catch(() => false) && ownedSubscription === subscription) ownedSubscription = void 0;
|
|
9032
|
+
}
|
|
9033
|
+
if (current(token) && operation === attempt) {
|
|
9034
|
+
const isStale = safeError(error) === "push.error.stale";
|
|
9035
|
+
publish({
|
|
9036
|
+
state: "failed",
|
|
9037
|
+
error: safeError(error),
|
|
9038
|
+
preferences: base.preferences
|
|
9039
|
+
});
|
|
9040
|
+
if (isStale) refresh(token);
|
|
9041
|
+
}
|
|
9042
|
+
} finally {
|
|
9043
|
+
if (current(token) && operation === attempt) mutationBusy = false;
|
|
9044
|
+
}
|
|
9045
|
+
},
|
|
9046
|
+
async disable() {
|
|
9047
|
+
if (!active || !config) return;
|
|
9048
|
+
const token = generation, operation = ++attempt, base = config;
|
|
9049
|
+
mutationBusy = true;
|
|
9050
|
+
publish({
|
|
9051
|
+
state: "pending",
|
|
9052
|
+
preferences: base.preferences
|
|
9053
|
+
});
|
|
9054
|
+
try {
|
|
9055
|
+
await api.disable({ installationId: preparedInstallId || base.installationId });
|
|
9056
|
+
if (!current(token) || operation !== attempt) return;
|
|
9057
|
+
config = {
|
|
9058
|
+
...base,
|
|
9059
|
+
status: "disabled"
|
|
9060
|
+
};
|
|
9061
|
+
publish({
|
|
9062
|
+
state: "disabled",
|
|
9063
|
+
preferences: base.preferences
|
|
9064
|
+
});
|
|
9065
|
+
if (currentHostSubscription) consentedSubscriptions.delete(currentHostSubscription);
|
|
9066
|
+
currentHostSubscription = void 0;
|
|
9067
|
+
const sub = ownedSubscription;
|
|
9068
|
+
if (sub) {
|
|
9069
|
+
if (await sub.unsubscribe().catch(() => false) && ownedSubscription === sub) ownedSubscription = void 0;
|
|
9070
|
+
}
|
|
9071
|
+
if (await refresh(token) && current(token) && operation === attempt && config?.status === "disabled") publish({
|
|
9072
|
+
state: "disabled",
|
|
9073
|
+
preferences: config.preferences
|
|
9074
|
+
});
|
|
9075
|
+
} catch (error) {
|
|
9076
|
+
if (current(token) && operation === attempt) {
|
|
9077
|
+
publish({
|
|
9078
|
+
state: "failed",
|
|
9079
|
+
error: safeError(error),
|
|
9080
|
+
preferences: base.preferences
|
|
9081
|
+
});
|
|
9082
|
+
if (safeError(error) === "push.error.stale") refresh(token);
|
|
9083
|
+
}
|
|
9084
|
+
} finally {
|
|
9085
|
+
if (current(token) && operation === attempt) mutationBusy = false;
|
|
9086
|
+
}
|
|
9087
|
+
},
|
|
9088
|
+
async updatePreferences(value) {
|
|
9089
|
+
if (!active || !config || mutationBusy) return;
|
|
9090
|
+
const token = generation, operation = ++attempt, base = config;
|
|
9091
|
+
mutationBusy = true;
|
|
9092
|
+
publish({
|
|
9093
|
+
state: "pending",
|
|
9094
|
+
preferences: value
|
|
9095
|
+
});
|
|
9096
|
+
try {
|
|
9097
|
+
await api.updatePreferences({
|
|
9098
|
+
installationId: preparedInstallId || base.installationId,
|
|
9099
|
+
ownerGeneration: base.ownerGeneration,
|
|
9100
|
+
registrationGeneration: base.registrationGeneration,
|
|
9101
|
+
preferences: value
|
|
9102
|
+
});
|
|
9103
|
+
if (current(token) && operation === attempt) {
|
|
9104
|
+
config = {
|
|
9105
|
+
...base,
|
|
9106
|
+
preferences: value
|
|
9107
|
+
};
|
|
9108
|
+
publish({
|
|
9109
|
+
state: base.status === "enabled" ? "enabled" : "ready",
|
|
9110
|
+
preferences: value
|
|
9111
|
+
});
|
|
9112
|
+
await refresh(token);
|
|
9113
|
+
}
|
|
9114
|
+
} catch (error) {
|
|
9115
|
+
if (current(token) && operation === attempt) {
|
|
9116
|
+
publish({
|
|
9117
|
+
state: "failed",
|
|
9118
|
+
error: safeError(error),
|
|
9119
|
+
preferences: base.preferences
|
|
9120
|
+
});
|
|
9121
|
+
if (safeError(error) === "push.error.stale") refresh(token);
|
|
9122
|
+
}
|
|
9123
|
+
} finally {
|
|
9124
|
+
if (current(token) && operation === attempt) mutationBusy = false;
|
|
9125
|
+
}
|
|
9126
|
+
},
|
|
9127
|
+
async sendTest() {
|
|
9128
|
+
if (!active || !config || config.status !== "enabled" || mutationBusy || testInFlight) return;
|
|
9129
|
+
const token = generation, operation = attempt, base = config;
|
|
9130
|
+
const owner = ++testOwner;
|
|
9131
|
+
testInFlight = true;
|
|
9132
|
+
publish({
|
|
9133
|
+
state: "enabled",
|
|
9134
|
+
preferences: base.preferences
|
|
9135
|
+
});
|
|
9136
|
+
try {
|
|
9137
|
+
await api.sendTest({
|
|
9138
|
+
installationId: preparedInstallId || base.installationId,
|
|
9139
|
+
ownerGeneration: base.ownerGeneration,
|
|
9140
|
+
registrationGeneration: base.registrationGeneration
|
|
9141
|
+
});
|
|
9142
|
+
if (current(token) && operation === attempt) publish({
|
|
9143
|
+
state: "enabled",
|
|
9144
|
+
error: "push.test.accepted",
|
|
9145
|
+
preferences: base.preferences
|
|
9146
|
+
});
|
|
9147
|
+
} catch (error) {
|
|
9148
|
+
if (!current(token) || operation !== attempt) return;
|
|
9149
|
+
const message = error?.code === "rate_limited" ? "push.test.rateLimited" : "push.test.failed";
|
|
9150
|
+
const refreshed = await refresh(token);
|
|
9151
|
+
if (!current(token) || operation !== attempt) return;
|
|
9152
|
+
if (refreshed && config?.status === "enabled") publish({
|
|
9153
|
+
state: "enabled",
|
|
9154
|
+
error: message,
|
|
9155
|
+
preferences: config.preferences
|
|
9156
|
+
});
|
|
9157
|
+
} finally {
|
|
9158
|
+
if (owner === testOwner) {
|
|
9159
|
+
testInFlight = false;
|
|
9160
|
+
publish({ ...snapshot });
|
|
9161
|
+
}
|
|
9162
|
+
}
|
|
9163
|
+
},
|
|
9164
|
+
getSnapshot: () => snapshot,
|
|
9165
|
+
subscribe(listener) {
|
|
9166
|
+
listeners.add(listener);
|
|
9167
|
+
return () => {
|
|
9168
|
+
listeners.delete(listener);
|
|
9169
|
+
};
|
|
9170
|
+
}
|
|
9171
|
+
};
|
|
9172
|
+
}
|
|
9173
|
+
/** Adapter factory uses only the page's public PushManager; it deliberately has no service-worker path. */
|
|
9174
|
+
function browserPushEnvironment(view = window) {
|
|
9175
|
+
const nav = view.navigator;
|
|
9176
|
+
const pushManager = view.pushManager;
|
|
9177
|
+
const storage = (() => {
|
|
9178
|
+
try {
|
|
9179
|
+
return view.localStorage;
|
|
9180
|
+
} catch {
|
|
9181
|
+
return;
|
|
9182
|
+
}
|
|
9183
|
+
})();
|
|
9184
|
+
const isInstalled = () => {
|
|
9185
|
+
try {
|
|
9186
|
+
return nav.standalone === true || view.matchMedia("(display-mode: standalone)").matches || view.matchMedia("(display-mode: fullscreen)").matches;
|
|
9187
|
+
} catch {
|
|
9188
|
+
return nav.standalone === true;
|
|
9189
|
+
}
|
|
9190
|
+
};
|
|
9191
|
+
return {
|
|
9192
|
+
secureContext: view.isSecureContext,
|
|
9193
|
+
isTopLevel: view.top === view.self,
|
|
9194
|
+
get standalone() {
|
|
9195
|
+
return isInstalled();
|
|
9196
|
+
},
|
|
9197
|
+
get permission() {
|
|
9198
|
+
return "Notification" in view ? view.Notification.permission : "unsupported";
|
|
9199
|
+
},
|
|
9200
|
+
pushManager,
|
|
9201
|
+
storage,
|
|
9202
|
+
origin: view.location.origin,
|
|
9203
|
+
randomUUID: () => view.crypto.randomUUID(),
|
|
9204
|
+
locale: view.document.documentElement.lang.toLowerCase().startsWith("zh") ? "zh-CN" : view.document.documentElement.lang.toLowerCase().startsWith("it") ? "it" : "en"
|
|
9205
|
+
};
|
|
9206
|
+
}
|
|
9207
|
+
//#endregion
|
|
9208
|
+
//#region src/mobile-push/switch-component.ts
|
|
9209
|
+
/**
|
|
9210
|
+
* Lazily require the public Switch from the DSH module loader at category render time.
|
|
9211
|
+
*
|
|
9212
|
+
* The plugin client bundle is a `window.__ModuleLoader__.load` factory whose `require` parameter
|
|
9213
|
+
* resolves seeded shell modules synchronously (`rc.2` and `rc.3` both seed the primitives namespace
|
|
9214
|
+
* unconditionally). Requiring lazily — inside the render that needs it, never at module scope —
|
|
9215
|
+
* keeps every Node test and provider-free fixture that mounts the settings row but not the push
|
|
9216
|
+
* toggles free of any browser-only module dependency. Outside a DSH shell there is no loader
|
|
9217
|
+
* `require`, so the categories are withdrawn instead of rendering a broken stand-in; native
|
|
9218
|
+
* rendering itself is verified by the strict packed smoke against the real mounted shell.
|
|
9219
|
+
*/
|
|
9220
|
+
function requirePublicMobilePushSwitch() {
|
|
9221
|
+
if (typeof window === "undefined" || typeof require !== "function") return void 0;
|
|
9222
|
+
try {
|
|
9223
|
+
return require("@deepseek-ai/dsh-client-ui-primitives").Switch;
|
|
9224
|
+
} catch {
|
|
9225
|
+
return;
|
|
9226
|
+
}
|
|
9227
|
+
}
|
|
9228
|
+
//#endregion
|
|
9229
|
+
//#region src/mobile-push/settings.ts
|
|
9230
|
+
const MOBILE_PUSH_SETTINGS_COPY = {
|
|
9231
|
+
en: {
|
|
9232
|
+
title: "Web push notifications",
|
|
9233
|
+
desc: "Receive notifications when this page is closed.",
|
|
9234
|
+
enable: "Enable",
|
|
9235
|
+
disable: "Disable",
|
|
9236
|
+
test: "Send test",
|
|
9237
|
+
testBusy: "Sending…",
|
|
9238
|
+
accepted: "Accepted by the push provider; lock-screen delivery is not confirmed.",
|
|
9239
|
+
install: "Add this site to your Home Screen to enable push.",
|
|
9240
|
+
denied: "Notifications are blocked in browser settings.",
|
|
9241
|
+
unsupported: "Web push is unavailable in this context.",
|
|
9242
|
+
error: "Could not update push settings. Try again.",
|
|
9243
|
+
testFailed: "The test notification could not be sent. Your notification settings are unchanged.",
|
|
9244
|
+
testRateLimited: "Too many test notifications; wait a moment before trying again.",
|
|
9245
|
+
conflict: "This device already has a push subscription for an earlier server key, so enabling cannot complete. Retrying will not fix this; the server operator must check the push key state first.",
|
|
9246
|
+
busy: "Working…",
|
|
9247
|
+
categories: [
|
|
9248
|
+
"Questions",
|
|
9249
|
+
"Plans",
|
|
9250
|
+
"Approvals",
|
|
9251
|
+
"Responses",
|
|
9252
|
+
"Goals",
|
|
9253
|
+
"Failures"
|
|
9254
|
+
]
|
|
9255
|
+
},
|
|
9256
|
+
zh: {
|
|
9257
|
+
title: "网页推送通知",
|
|
9258
|
+
desc: "关闭此页面后仍可接收通知。",
|
|
9259
|
+
enable: "启用",
|
|
9260
|
+
disable: "停用",
|
|
9261
|
+
test: "发送测试",
|
|
9262
|
+
testBusy: "发送中…",
|
|
9263
|
+
accepted: "推送服务商已接受;不代表锁屏一定送达。",
|
|
9264
|
+
install: "请将此网站添加到主屏幕以启用推送。",
|
|
9265
|
+
denied: "浏览器设置已阻止通知。",
|
|
9266
|
+
unsupported: "此环境不支持网页推送。",
|
|
9267
|
+
error: "无法更新推送设置,请重试。",
|
|
9268
|
+
testFailed: "测试通知未能发送。通知设置保持不变。",
|
|
9269
|
+
testRateLimited: "测试通知发送过于频繁,请稍后再试。",
|
|
9270
|
+
conflict: "此设备已存在与旧服务器密钥绑定的推送订阅,因此无法完成启用。重试无法解决;请先由服务器管理员检查推送密钥状态。",
|
|
9271
|
+
busy: "处理中…",
|
|
9272
|
+
categories: [
|
|
9273
|
+
"问题",
|
|
9274
|
+
"计划",
|
|
9275
|
+
"审批",
|
|
9276
|
+
"回复",
|
|
9277
|
+
"目标",
|
|
9278
|
+
"失败"
|
|
9279
|
+
]
|
|
9280
|
+
},
|
|
9281
|
+
it: {
|
|
9282
|
+
title: "Notifiche Web push",
|
|
9283
|
+
desc: "Ricevi notifiche quando la pagina è chiusa.",
|
|
9284
|
+
enable: "Attiva",
|
|
9285
|
+
disable: "Disattiva",
|
|
9286
|
+
test: "Invia test",
|
|
9287
|
+
testBusy: "Invio…",
|
|
9288
|
+
accepted: "Accettata dal provider; la consegna sulla schermata di blocco non è confermata.",
|
|
9289
|
+
install: "Aggiungi il sito alla schermata Home per attivare il push.",
|
|
9290
|
+
denied: "Le notifiche sono bloccate nelle impostazioni del browser.",
|
|
9291
|
+
unsupported: "Web push non disponibile in questo contesto.",
|
|
9292
|
+
error: "Impossibile aggiornare le impostazioni push. Riprova.",
|
|
9293
|
+
testFailed: "Impossibile inviare la notifica di test. Le impostazioni delle notifiche restano invariate.",
|
|
9294
|
+
testRateLimited: "Troppe notifiche di test; attendi qualche istante prima di riprovare.",
|
|
9295
|
+
conflict: "Questo dispositivo ha già una sottoscrizione push associata a una chiave server precedente, quindi l’attivazione non può completarsi. Riprovare non risolve il problema; l’operatore del server deve prima controllare lo stato della chiave push.",
|
|
9296
|
+
busy: "Operazione…",
|
|
9297
|
+
categories: [
|
|
9298
|
+
"Domande",
|
|
9299
|
+
"Piani",
|
|
9300
|
+
"Approvazioni",
|
|
9301
|
+
"Risposte",
|
|
9302
|
+
"Obiettivi",
|
|
9303
|
+
"Errori"
|
|
9304
|
+
]
|
|
9305
|
+
}
|
|
9306
|
+
};
|
|
9307
|
+
const KEYS = [
|
|
9308
|
+
"question",
|
|
9309
|
+
"plan",
|
|
9310
|
+
"approval",
|
|
9311
|
+
"response",
|
|
9312
|
+
"goal",
|
|
9313
|
+
"failure"
|
|
9314
|
+
];
|
|
9315
|
+
function message(snapshot, copy) {
|
|
9316
|
+
if (snapshot.state === "install") return copy.install;
|
|
9317
|
+
if (snapshot.state === "denied") return copy.denied;
|
|
9318
|
+
if (snapshot.state === "unsupported") return copy.unsupported;
|
|
9319
|
+
if (snapshot.state === "enabled" && snapshot.error === "push.test.accepted") return copy.accepted;
|
|
9320
|
+
if (snapshot.state === "enabled" && snapshot.error === "push.test.rateLimited") return copy.testRateLimited;
|
|
9321
|
+
if (snapshot.state === "enabled" && snapshot.error === "push.test.failed") return copy.testFailed;
|
|
9322
|
+
if (snapshot.state === "failed" && snapshot.error === "push.error.subscriptionConflict") return copy.conflict;
|
|
9323
|
+
return snapshot.state === "failed" ? copy.error : copy.desc;
|
|
9324
|
+
}
|
|
9325
|
+
/** Browser-only settings row; mounting it never requests permission or creates a subscription. */
|
|
9326
|
+
function MobilePushSettings({ controller, locale, switchComponent }) {
|
|
9327
|
+
const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
9328
|
+
const copy = MOBILE_PUSH_SETTINGS_COPY[locale];
|
|
9329
|
+
if (snapshot.state === "unsupported") return null;
|
|
9330
|
+
const prefs = snapshot.preferences;
|
|
9331
|
+
const Switch = switchComponent ?? requirePublicMobilePushSwitch();
|
|
9332
|
+
const toggle = (key, checked) => {
|
|
9333
|
+
if (!prefs) return;
|
|
9334
|
+
controller.updatePreferences({
|
|
9335
|
+
...prefs,
|
|
9336
|
+
[key]: checked
|
|
9337
|
+
});
|
|
9338
|
+
};
|
|
9339
|
+
return (0, react.createElement)("div", {
|
|
9340
|
+
className: "dsh-mobile-settings_row",
|
|
9341
|
+
style: { flexWrap: "wrap" },
|
|
9342
|
+
lang: locale === "zh" ? "zh-CN" : locale
|
|
9343
|
+
}, (0, react.createElement)("div", {
|
|
9344
|
+
className: "dsh-mobile-settings_rowText",
|
|
9345
|
+
style: { paddingRight: 8 }
|
|
9346
|
+
}, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
|
|
9347
|
+
className: "dsh-mobile-settings_desc",
|
|
9348
|
+
"aria-live": "polite"
|
|
9349
|
+
}, message(snapshot, copy)), prefs && Switch !== void 0 ? (0, react.createElement)("div", { className: "dsh-mobile-push-categories" }, ...KEYS.map((key, index) => {
|
|
9350
|
+
const name = copy.categories[index];
|
|
9351
|
+
if (name === void 0) return null;
|
|
9352
|
+
return (0, react.createElement)("label", {
|
|
9353
|
+
key,
|
|
9354
|
+
className: "dsh-mobile-push-category"
|
|
9355
|
+
}, (0, react.createElement)("span", { className: "dsh-mobile-push-categoryName" }, name), (0, react.createElement)(Switch, {
|
|
9356
|
+
checked: prefs[key],
|
|
9357
|
+
label: name,
|
|
9358
|
+
onChange: (next) => {
|
|
9359
|
+
toggle(key, next);
|
|
9360
|
+
}
|
|
9361
|
+
}));
|
|
9362
|
+
})) : null), (0, react.createElement)("div", {
|
|
9363
|
+
className: "dsh-mobile-push-actions",
|
|
9364
|
+
style: {
|
|
9365
|
+
display: "flex",
|
|
9366
|
+
flexWrap: "wrap",
|
|
9367
|
+
gap: 8
|
|
9368
|
+
}
|
|
9369
|
+
}, snapshot.state === "enabled" ? (0, react.createElement)("button", {
|
|
9370
|
+
type: "button",
|
|
9371
|
+
className: "dsh-mobile-settings_selector",
|
|
9372
|
+
disabled: snapshot.testInFlight === true,
|
|
9373
|
+
"aria-busy": snapshot.testInFlight === true ? "true" : void 0,
|
|
9374
|
+
onClick: () => void controller.sendTest()
|
|
9375
|
+
}, snapshot.testInFlight ? copy.testBusy : copy.test) : null, snapshot.state === "enabled" || snapshot.state === "suspended" ? (0, react.createElement)("button", {
|
|
9376
|
+
type: "button",
|
|
9377
|
+
className: "dsh-mobile-settings_selector",
|
|
9378
|
+
onClick: () => void controller.disable()
|
|
9379
|
+
}, copy.disable) : snapshot.state === "ready" || snapshot.state === "failed" || snapshot.state === "disabled" ? (0, react.createElement)("button", {
|
|
9380
|
+
type: "button",
|
|
9381
|
+
className: "dsh-mobile-settings_selector",
|
|
9382
|
+
onClick: () => void controller.enableFromGesture()
|
|
9383
|
+
}, copy.enable) : (0, react.createElement)("button", {
|
|
9384
|
+
type: "button",
|
|
9385
|
+
className: "dsh-mobile-settings_selector",
|
|
9386
|
+
disabled: true
|
|
9387
|
+
}, copy.busy)));
|
|
9388
|
+
}
|
|
9389
|
+
//#endregion
|
|
9390
|
+
//#region src/mobile-push/navigation.ts
|
|
9391
|
+
const REF_PARAMETER = "mobile-push";
|
|
9392
|
+
/**
|
|
9393
|
+
* Consume one authenticated tap reference after the public Workspace and Session
|
|
9394
|
+
* list baselines are ready. Return the disposal function for list subscriptions.
|
|
9395
|
+
*/
|
|
9396
|
+
function installPushNavigation(ctx, options) {
|
|
9397
|
+
const context = ctx;
|
|
9398
|
+
const browser = options.browser ?? {
|
|
9399
|
+
location: window.location,
|
|
9400
|
+
history: window.history
|
|
9401
|
+
};
|
|
9402
|
+
const params = new URLSearchParams(browser.location.search);
|
|
9403
|
+
const reference = params.get(REF_PARAMETER);
|
|
9404
|
+
let disposed = false;
|
|
9405
|
+
let consumed = false;
|
|
9406
|
+
let resolving = false;
|
|
9407
|
+
let opened = false;
|
|
9408
|
+
const clearReference = () => {
|
|
9409
|
+
if (!params.has(REF_PARAMETER)) return;
|
|
9410
|
+
params.delete(REF_PARAMETER);
|
|
9411
|
+
const query = params.toString();
|
|
9412
|
+
const url = `${browser.location.pathname}${query ? `?${query}` : ""}${browser.location.hash}`;
|
|
9413
|
+
browser.history.replaceState(null, "", url);
|
|
9414
|
+
};
|
|
9415
|
+
const neutral = (key = "session-unavailable") => {
|
|
9416
|
+
if (disposed || consumed) return;
|
|
9417
|
+
consumed = true;
|
|
9418
|
+
clearReference();
|
|
9419
|
+
options.notify(key);
|
|
9420
|
+
};
|
|
9421
|
+
const check = () => {
|
|
9422
|
+
if (disposed || consumed || resolving) return;
|
|
9423
|
+
if (!reference) {
|
|
9424
|
+
neutral();
|
|
9425
|
+
return;
|
|
9426
|
+
}
|
|
9427
|
+
const sessions = context.sessions.list.getSnapshot();
|
|
9428
|
+
const workspaces = context.workspaces.list.getSnapshot();
|
|
9429
|
+
if (sessions.phase !== "pending" && sessions.phase !== "ready" || workspaces.phase !== "pending" && workspaces.phase !== "ready") {
|
|
9430
|
+
neutral();
|
|
9431
|
+
return;
|
|
9432
|
+
}
|
|
9433
|
+
if (sessions.phase !== "ready" || workspaces.phase !== "ready") return;
|
|
9434
|
+
if (!reference) {
|
|
9435
|
+
neutral();
|
|
9436
|
+
return;
|
|
9437
|
+
}
|
|
9438
|
+
resolving = true;
|
|
9439
|
+
consumed = true;
|
|
9440
|
+
clearReference();
|
|
9441
|
+
options.resolve(reference).then((target) => {
|
|
9442
|
+
if (disposed) return;
|
|
9443
|
+
if (!target || target.interaction !== "current" && target.interaction !== "resolved" || !sessionsAvailable(context, target.sessionId)) {
|
|
9444
|
+
options.notify("session-unavailable");
|
|
9445
|
+
return;
|
|
9446
|
+
}
|
|
9447
|
+
if (opened) return;
|
|
9448
|
+
opened = true;
|
|
9449
|
+
context.uiWorkspace.openSession(target.sessionId);
|
|
9450
|
+
if (target.interaction === "resolved") options.notify("session-resolved");
|
|
9451
|
+
}).catch(() => {
|
|
9452
|
+
if (!disposed) options.notify("session-unavailable");
|
|
9453
|
+
});
|
|
9454
|
+
};
|
|
9455
|
+
const unsubscribeSessions = context.sessions.list.subscribe(check);
|
|
9456
|
+
const unsubscribeWorkspaces = context.workspaces.list.subscribe(check);
|
|
9457
|
+
check();
|
|
9458
|
+
return () => {
|
|
9459
|
+
if (disposed) return;
|
|
9460
|
+
disposed = true;
|
|
9461
|
+
unsubscribeSessions();
|
|
9462
|
+
unsubscribeWorkspaces();
|
|
9463
|
+
};
|
|
9464
|
+
}
|
|
9465
|
+
function sessionsAvailable(ctx, sessionId) {
|
|
9466
|
+
const snapshot = ctx.sessions.list.getSnapshot();
|
|
9467
|
+
return snapshot.phase === "ready" && snapshot.ids.includes(sessionId) && snapshot.byId[sessionId] !== void 0;
|
|
9468
|
+
}
|
|
9469
|
+
//#endregion
|
|
8417
9470
|
//#region src/mobile-directory-browser.ts
|
|
8418
9471
|
/** Expected Gateway or response-validation failure for one read-only listing. */
|
|
8419
9472
|
var MobileDirectoryReadError = class extends Error {
|
|
@@ -9487,6 +10540,12 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
9487
10540
|
function parseMobileRemoteProvider(value) {
|
|
9488
10541
|
return value === "cpolar" || value === "cloudflared" || value === "frp" || value === "origin" || value === "iroh" ? value : "tailscale";
|
|
9489
10542
|
}
|
|
10543
|
+
/** Public client services the authenticated tap consumer follows (scoped child lifecycle, never a required plugin dependency). */
|
|
10544
|
+
const mobilePushNavigationServices = [
|
|
10545
|
+
"sessions",
|
|
10546
|
+
"workspaces",
|
|
10547
|
+
"uiWorkspace"
|
|
10548
|
+
];
|
|
9490
10549
|
/**
|
|
9491
10550
|
* Set a surface's visibility; opening a page or overlay brings it to the front.
|
|
9492
10551
|
* @param container - the registered surface element.
|
|
@@ -9833,11 +10892,15 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
9833
10892
|
const androidVersion = releaseVersion(data.androidVersion);
|
|
9834
10893
|
const expectedAndroidDownloadUrls = androidVersion === void 0 ? [] : ["v", "android-v"].map((prefix) => `${GITHUB_RELEASES_URL}/download/${prefix}${androidVersion}/dsh-mobile-android-v${androidVersion}.apk`);
|
|
9835
10894
|
const androidDownloadUrl = typeof data.androidDownloadUrl === "string" && expectedAndroidDownloadUrls.includes(data.androidDownloadUrl) ? data.androidDownloadUrl : GITHUB_RELEASES_URL;
|
|
10895
|
+
const androidIrohDownloadUrl = androidVersion === void 0 || androidDownloadUrl === GITHUB_RELEASES_URL ? void 0 : `${GITHUB_RELEASES_URL}/download/android-v${androidVersion}/dsh-mobile-android-v${androidVersion}-iroh.apk`;
|
|
9836
10896
|
return {
|
|
9837
10897
|
updateAvailable: data.updateAvailable === true && latestVersion !== void 0,
|
|
9838
10898
|
...latestVersion === void 0 ? {} : { latestVersion },
|
|
9839
10899
|
...androidVersion === void 0 ? {} : { androidVersion },
|
|
9840
10900
|
androidDownloadUrl,
|
|
10901
|
+
...androidIrohDownloadUrl === void 0 ? {} : { androidIrohDownloadUrl },
|
|
10902
|
+
...typeof data.androidCompatBytes === "number" && data.androidCompatBytes > 0 ? { androidCompatBytes: data.androidCompatBytes } : {},
|
|
10903
|
+
...typeof data.androidIrohBytes === "number" && data.androidIrohBytes > 0 ? { androidIrohBytes: data.androidIrohBytes } : {},
|
|
9841
10904
|
...typeof data.releaseNotes === "string" && data.releaseNotes !== "" ? { releaseNotes: data.releaseNotes.slice(0, 4e3) } : {}
|
|
9842
10905
|
};
|
|
9843
10906
|
}
|
|
@@ -9858,7 +10921,18 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
9858
10921
|
signal: controller.signal,
|
|
9859
10922
|
headers: localAdminRequestHeaders(init, location)
|
|
9860
10923
|
});
|
|
9861
|
-
const
|
|
10924
|
+
const text = await response.text();
|
|
10925
|
+
let body = {};
|
|
10926
|
+
if (text.trim() !== "") {
|
|
10927
|
+
let parsed;
|
|
10928
|
+
try {
|
|
10929
|
+
parsed = JSON.parse(text);
|
|
10930
|
+
} catch {
|
|
10931
|
+
throw new Error(!response.ok ? `HTTP ${String(response.status)} (${path})` : `${controlTranslator()("invalidResponse")} (${path})`);
|
|
10932
|
+
}
|
|
10933
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(!response.ok ? `HTTP ${String(response.status)} (${path})` : `${controlTranslator()("invalidResponse")} (${path})`);
|
|
10934
|
+
body = parsed;
|
|
10935
|
+
}
|
|
9862
10936
|
if (!response.ok) throw new Error(typeof body.error === "string" ? body.error : `HTTP ${String(response.status)}`);
|
|
9863
10937
|
return body;
|
|
9864
10938
|
} catch (error) {
|
|
@@ -10048,6 +11122,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10048
11122
|
iroh_exited: "irohExited",
|
|
10049
11123
|
iroh_not_ready: "irohNotReady",
|
|
10050
11124
|
iroh_pairing_failed: "irohPairingFailed",
|
|
11125
|
+
iroh_component_running: "irohComponentRunning",
|
|
10051
11126
|
frp_server_address_invalid: "frpServerAddressInvalid",
|
|
10052
11127
|
frp_server_port_invalid: "frpServerPortInvalid",
|
|
10053
11128
|
frp_token_invalid: "frpTokenInvalid",
|
|
@@ -10261,17 +11336,31 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10261
11336
|
close.type = "button";
|
|
10262
11337
|
close.textContent = "×";
|
|
10263
11338
|
close.setAttribute("aria-label", t("collapseMobileAccess"));
|
|
10264
|
-
|
|
11339
|
+
const devicesEntry = element("button", "dsh-mobile-control__diagnostic-entry");
|
|
11340
|
+
devicesEntry.type = "button";
|
|
11341
|
+
devicesEntry.textContent = t("device");
|
|
11342
|
+
devicesEntry.setAttribute("aria-label", t("managePairedDevices"));
|
|
11343
|
+
devicesEntry.setAttribute("aria-pressed", "false");
|
|
11344
|
+
headerActions.append(updatePlugin, diagnosticsEntry, devicesEntry, close);
|
|
10265
11345
|
const releaseNotice = element("p", "dsh-mobile-control__release-notice");
|
|
10266
11346
|
releaseNotice.hidden = true;
|
|
10267
11347
|
releaseNotice.setAttribute("role", "status");
|
|
10268
11348
|
releaseNotice.setAttribute("aria-live", "polite");
|
|
11349
|
+
const appDownloads = element("div", "dsh-mobile-control__app-downloads");
|
|
10269
11350
|
const appDownload = element("a", "dsh-mobile-control__app-download");
|
|
10270
11351
|
appDownload.href = GITHUB_RELEASES_URL;
|
|
10271
11352
|
appDownload.target = "_blank";
|
|
10272
11353
|
appDownload.rel = "noopener noreferrer";
|
|
10273
11354
|
appDownload.textContent = t("downloadAndroid");
|
|
10274
11355
|
appDownload.setAttribute("aria-label", t("downloadAndroidAria"));
|
|
11356
|
+
const appDownloadIroh = element("a", "dsh-mobile-control__app-download is-iroh");
|
|
11357
|
+
appDownloadIroh.href = GITHUB_RELEASES_URL;
|
|
11358
|
+
appDownloadIroh.target = "_blank";
|
|
11359
|
+
appDownloadIroh.rel = "noopener noreferrer";
|
|
11360
|
+
appDownloadIroh.textContent = t("downloadAndroidIroh");
|
|
11361
|
+
appDownloadIroh.setAttribute("aria-label", t("downloadAndroidIrohAria"));
|
|
11362
|
+
appDownloadIroh.hidden = true;
|
|
11363
|
+
appDownloads.append(appDownload, appDownloadIroh);
|
|
10275
11364
|
const switcher = element("div", "dsh-mobile-control__switcher");
|
|
10276
11365
|
const lanTab = element("button", "dsh-mobile-control__tab is-active");
|
|
10277
11366
|
lanTab.type = "button";
|
|
@@ -10396,7 +11485,12 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10396
11485
|
accessLink.rel = "noreferrer";
|
|
10397
11486
|
access.append(accessLabel, accessLink);
|
|
10398
11487
|
const qrBox = element("div", "dsh-mobile-control__qr");
|
|
10399
|
-
|
|
11488
|
+
const lanPairingValidity = element("p", "dsh-mobile-control__component-status");
|
|
11489
|
+
lanPairingValidity.hidden = true;
|
|
11490
|
+
let activeLanPairUrl = "";
|
|
11491
|
+
let activeLanPairExpiresAt = 0;
|
|
11492
|
+
let lanPairingBusy = false;
|
|
11493
|
+
let lanPairingGeneration = 0;
|
|
10400
11494
|
const status = element("p", "dsh-mobile-control__status");
|
|
10401
11495
|
status.textContent = t("loadingStatus");
|
|
10402
11496
|
const extensionStatus = element("p", "dsh-mobile-control__extensions");
|
|
@@ -10417,11 +11511,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10417
11511
|
const resetAll = element("button", "dsh-mobile-control__manage");
|
|
10418
11512
|
resetAll.type = "button";
|
|
10419
11513
|
resetAll.textContent = t("clearAllDevices");
|
|
10420
|
-
manageRow.append(manageDevices
|
|
11514
|
+
manageRow.append(manageDevices);
|
|
10421
11515
|
const devicePanel = element("div", "dsh-mobile-control__devices");
|
|
10422
|
-
devicePanel.hidden = true;
|
|
10423
11516
|
const remoteView = element("div", "dsh-mobile-control__view is-remote");
|
|
10424
|
-
remoteView.hidden = true;
|
|
10425
11517
|
const remoteIntro = element("p", "dsh-mobile-control__intro");
|
|
10426
11518
|
remoteIntro.textContent = t("remoteIntro");
|
|
10427
11519
|
const httpFrameWarning = element("p", "dsh-mobile-control__http-frame-warning");
|
|
@@ -10431,27 +11523,6 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10431
11523
|
const providerSection = element("section", "dsh-mobile-control__provider-section");
|
|
10432
11524
|
const providerHeading = element("h3", "dsh-mobile-control__section-title");
|
|
10433
11525
|
providerHeading.textContent = t("chooseProvider");
|
|
10434
|
-
const providerInfo = element("div", "dsh-mobile-control__provider-info");
|
|
10435
|
-
const providerInfoButton = element("button", "dsh-mobile-control__provider-info-button");
|
|
10436
|
-
providerInfoButton.type = "button";
|
|
10437
|
-
providerInfoButton.setAttribute("aria-label", t("providerInfoAria"));
|
|
10438
|
-
providerInfoButton.setAttribute("aria-expanded", "false");
|
|
10439
|
-
providerInfoButton.setAttribute("aria-controls", "dsh-mobile-provider-info");
|
|
10440
|
-
providerInfoButton.setAttribute("aria-describedby", "dsh-mobile-provider-info");
|
|
10441
|
-
const providerInfoGlyph = element("span", "dsh-mobile-control__provider-info-glyph");
|
|
10442
|
-
providerInfoGlyph.textContent = "i";
|
|
10443
|
-
providerInfoGlyph.setAttribute("aria-hidden", "true");
|
|
10444
|
-
const providerInfoPopover = element("div", "dsh-mobile-control__provider-info-popover");
|
|
10445
|
-
providerInfoPopover.id = "dsh-mobile-provider-info";
|
|
10446
|
-
providerInfoPopover.setAttribute("role", "tooltip");
|
|
10447
|
-
providerInfoPopover.hidden = true;
|
|
10448
|
-
const providerInfoTitle = element("strong");
|
|
10449
|
-
providerInfoTitle.textContent = t("providerSafeTitle");
|
|
10450
|
-
const providerInfoText = element("span");
|
|
10451
|
-
providerInfoText.textContent = t("providerSafeText");
|
|
10452
|
-
providerInfoButton.append(providerInfoGlyph);
|
|
10453
|
-
providerInfoPopover.append(providerInfoTitle, providerInfoText);
|
|
10454
|
-
providerInfo.append(providerInfoButton, providerInfoPopover);
|
|
10455
11526
|
const providerChoices = element("div", "dsh-mobile-control__provider-choices");
|
|
10456
11527
|
providerChoices.setAttribute("role", "radiogroup");
|
|
10457
11528
|
providerChoices.setAttribute("aria-label", t("providerGroupAria"));
|
|
@@ -10519,6 +11590,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10519
11590
|
const cpolarInstall = element("button", "dsh-mobile-control__primary");
|
|
10520
11591
|
cpolarInstall.type = "button";
|
|
10521
11592
|
cpolarInstall.textContent = t("installOfficial");
|
|
11593
|
+
cpolarInstall.hidden = true;
|
|
10522
11594
|
const cpolarAccount = element("div", "dsh-mobile-control__cpolar-account");
|
|
10523
11595
|
cpolarAccount.hidden = true;
|
|
10524
11596
|
const cpolarAccountText = element("p", "dsh-mobile-control__component-note");
|
|
@@ -10570,7 +11642,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10570
11642
|
cpolarPurge.textContent = t("purgeCpolar");
|
|
10571
11643
|
cpolarDetailsBody.append(cpolarDetailsText, cpolarStorage, cpolarOfficial, cpolarTerms, cpolarPurge);
|
|
10572
11644
|
cpolarDetails.append(cpolarDetailsSummary, cpolarDetailsBody);
|
|
10573
|
-
cpolarSetup.append(cpolarSetupTitle, cpolarComponentStatus,
|
|
11645
|
+
cpolarSetup.append(cpolarSetupTitle, cpolarComponentStatus, cpolarAccount);
|
|
10574
11646
|
const cloudflaredSetup = element("section", "dsh-mobile-control__cpolar-setup dsh-mobile-control__cloudflared-setup");
|
|
10575
11647
|
cloudflaredSetup.hidden = true;
|
|
10576
11648
|
const cloudflaredSetupTitle = element("h3", "dsh-mobile-control__section-title");
|
|
@@ -10587,6 +11659,41 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10587
11659
|
cloudflaredMode.append(cloudflaredModeQuick, cloudflaredModeNamed);
|
|
10588
11660
|
const cloudflaredModeHint = element("p", "dsh-mobile-control__component-status");
|
|
10589
11661
|
cloudflaredModeHint.textContent = t("cloudflaredTunnelQuickHint");
|
|
11662
|
+
const cloudflaredNotesToggle = element("button", "dsh-mobile-control__notes-toggle");
|
|
11663
|
+
cloudflaredNotesToggle.type = "button";
|
|
11664
|
+
cloudflaredNotesToggle.textContent = "i";
|
|
11665
|
+
cloudflaredNotesToggle.setAttribute("aria-label", t("providerInfoAria"));
|
|
11666
|
+
cloudflaredNotesToggle.setAttribute("aria-expanded", "false");
|
|
11667
|
+
const cloudflaredNotesPanel = element("div", "dsh-mobile-control__notes-panel");
|
|
11668
|
+
cloudflaredNotesPanel.hidden = true;
|
|
11669
|
+
const cloudflaredNotesQuick = element("p");
|
|
11670
|
+
cloudflaredNotesQuick.textContent = t("cloudflaredQuickNote");
|
|
11671
|
+
const cloudflaredNotesNamed = element("p");
|
|
11672
|
+
cloudflaredNotesNamed.textContent = t("cloudflaredNamedNote");
|
|
11673
|
+
cloudflaredNotesPanel.append(cloudflaredNotesQuick, cloudflaredNotesNamed);
|
|
11674
|
+
const cloudflaredModeRow = element("div", "dsh-mobile-control__mode-row");
|
|
11675
|
+
cloudflaredModeRow.append(cloudflaredMode, cloudflaredNotesToggle, cloudflaredNotesPanel);
|
|
11676
|
+
const setCloudflaredNotesOpen = (open) => {
|
|
11677
|
+
cloudflaredNotesPanel.hidden = !open;
|
|
11678
|
+
cloudflaredNotesToggle.setAttribute("aria-expanded", String(open));
|
|
11679
|
+
};
|
|
11680
|
+
const hoverCapable = typeof window.matchMedia === "function" ? window.matchMedia("(hover: hover)") : void 0;
|
|
11681
|
+
cloudflaredModeRow.addEventListener("pointerenter", () => {
|
|
11682
|
+
if (hoverCapable?.matches) setCloudflaredNotesOpen(true);
|
|
11683
|
+
});
|
|
11684
|
+
cloudflaredModeRow.addEventListener("pointerleave", () => {
|
|
11685
|
+
if (hoverCapable?.matches) setCloudflaredNotesOpen(false);
|
|
11686
|
+
});
|
|
11687
|
+
cloudflaredNotesToggle.addEventListener("click", (event) => {
|
|
11688
|
+
setCloudflaredNotesOpen(hoverCapable?.matches && event.detail !== 0 ? true : Boolean(cloudflaredNotesPanel.hidden));
|
|
11689
|
+
});
|
|
11690
|
+
cloudflaredModeRow.addEventListener("keydown", (event) => {
|
|
11691
|
+
if (event.key !== "Escape" || cloudflaredNotesPanel.hidden) return;
|
|
11692
|
+
event.preventDefault();
|
|
11693
|
+
event.stopPropagation();
|
|
11694
|
+
setCloudflaredNotesOpen(false);
|
|
11695
|
+
cloudflaredNotesToggle.focus();
|
|
11696
|
+
});
|
|
10590
11697
|
const cloudflaredTunnel = element("div", "dsh-mobile-control__tunnel");
|
|
10591
11698
|
cloudflaredTunnel.hidden = true;
|
|
10592
11699
|
const cloudflaredTunnelFields = element("div", "dsh-mobile-control__frp-fields");
|
|
@@ -10629,12 +11736,23 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10629
11736
|
cloudflaredTunnelForget.type = "button";
|
|
10630
11737
|
cloudflaredTunnelForget.textContent = t("cloudflaredTunnelForget");
|
|
10631
11738
|
cloudflaredTunnelForget.hidden = true;
|
|
10632
|
-
|
|
11739
|
+
const cloudflaredTunnelToggle = element("button", "dsh-mobile-control__tunnel-toggle");
|
|
11740
|
+
cloudflaredTunnelToggle.type = "button";
|
|
11741
|
+
cloudflaredTunnelToggle.textContent = t("cloudflaredShowConfig") + " ▾";
|
|
11742
|
+
cloudflaredTunnelToggle.hidden = true;
|
|
11743
|
+
const cloudflaredEditButton = element("button", "dsh-mobile-control__secondary");
|
|
11744
|
+
cloudflaredEditButton.type = "button";
|
|
11745
|
+
cloudflaredEditButton.textContent = t("cloudflaredEditConfig");
|
|
11746
|
+
cloudflaredEditButton.hidden = true;
|
|
11747
|
+
const cloudflaredTunnelFeedback = element("p", "dsh-mobile-control__component-status");
|
|
11748
|
+
cloudflaredTunnelFeedback.hidden = true;
|
|
11749
|
+
cloudflaredTunnel.append(cloudflaredTunnelFields, cloudflaredTunnelStatus, cloudflaredTunnelHint, cloudflaredEditButton, cloudflaredTunnelSave, cloudflaredTunnelForget, cloudflaredTunnelFeedback);
|
|
10633
11750
|
const cloudflaredComponentStatus = element("p", "dsh-mobile-control__component-status");
|
|
10634
11751
|
cloudflaredComponentStatus.textContent = t("checkingComponent");
|
|
10635
11752
|
const cloudflaredInstall = element("button", "dsh-mobile-control__primary");
|
|
10636
11753
|
cloudflaredInstall.type = "button";
|
|
10637
11754
|
cloudflaredInstall.textContent = t("installOfficial");
|
|
11755
|
+
cloudflaredInstall.hidden = true;
|
|
10638
11756
|
const cloudflaredNote = element("p", "dsh-mobile-control__origin-warning");
|
|
10639
11757
|
cloudflaredNote.textContent = t("cloudflaredQuickNote");
|
|
10640
11758
|
const cloudflaredDetails = element("details", "dsh-mobile-control__details");
|
|
@@ -10660,19 +11778,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10660
11778
|
cloudflaredPurge.textContent = t("purgeCloudflared");
|
|
10661
11779
|
cloudflaredDetailsBody.append(cloudflaredDetailsText, cloudflaredStorage, cloudflaredOfficial, cloudflaredTerms, cloudflaredPurge);
|
|
10662
11780
|
cloudflaredDetails.append(cloudflaredDetailsSummary, cloudflaredDetailsBody);
|
|
10663
|
-
cloudflaredSetup.append(cloudflaredSetupTitle, cloudflaredTunnelTitle,
|
|
10664
|
-
const selfHosted = element("details", "dsh-mobile-control__self-hosted");
|
|
10665
|
-
const selfHostedSummary = element("summary", "dsh-mobile-control__self-hosted-summary");
|
|
10666
|
-
const selfHostedSummaryText = element("span");
|
|
10667
|
-
const selfHostedSummaryTitle = element("strong");
|
|
10668
|
-
selfHostedSummaryTitle.textContent = t("selfHostedConnections");
|
|
10669
|
-
const selfHostedSummaryDescription = element("span");
|
|
10670
|
-
selfHostedSummaryDescription.textContent = t("selfHostedDescription");
|
|
10671
|
-
const selfHostedBadge = element("span", "dsh-mobile-control__provider-badge is-frp");
|
|
10672
|
-
selfHostedBadge.textContent = t("advanced");
|
|
10673
|
-
selfHostedSummaryText.append(selfHostedSummaryTitle, selfHostedSummaryDescription);
|
|
10674
|
-
selfHostedSummary.append(selfHostedSummaryText, selfHostedBadge);
|
|
10675
|
-
const selfHostedBody = element("div", "dsh-mobile-control__self-hosted-body");
|
|
11781
|
+
cloudflaredSetup.append(cloudflaredSetupTitle, cloudflaredTunnelTitle, cloudflaredModeRow, cloudflaredModeHint, cloudflaredTunnelToggle, cloudflaredTunnel, cloudflaredComponentStatus);
|
|
10676
11782
|
const frpChoice = element("button", "dsh-mobile-control__provider is-frp");
|
|
10677
11783
|
frpChoice.type = "button";
|
|
10678
11784
|
frpChoice.setAttribute("aria-pressed", "false");
|
|
@@ -10681,7 +11787,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10681
11787
|
frpChoiceName.textContent = t("frpName");
|
|
10682
11788
|
const frpChoiceDescription = element("span", "dsh-mobile-control__provider-description");
|
|
10683
11789
|
frpChoiceDescription.textContent = t("frpDescription");
|
|
10684
|
-
|
|
11790
|
+
const frpChoiceBadge = element("span", "dsh-mobile-control__provider-badge is-frp");
|
|
11791
|
+
frpChoiceBadge.textContent = t("advanced");
|
|
11792
|
+
frpChoiceTop.append(frpChoiceName, frpChoiceBadge);
|
|
10685
11793
|
frpChoice.append(frpChoiceTop, frpChoiceDescription);
|
|
10686
11794
|
const originChoice = element("button", "dsh-mobile-control__provider is-origin");
|
|
10687
11795
|
originChoice.type = "button";
|
|
@@ -10691,7 +11799,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10691
11799
|
originChoiceName.textContent = t("originName");
|
|
10692
11800
|
const originChoiceDescription = element("span", "dsh-mobile-control__provider-description");
|
|
10693
11801
|
originChoiceDescription.textContent = t("originDescription");
|
|
10694
|
-
|
|
11802
|
+
const originChoiceBadge = element("span", "dsh-mobile-control__provider-badge");
|
|
11803
|
+
originChoiceBadge.textContent = t("advanced");
|
|
11804
|
+
originChoiceTop.append(originChoiceName, originChoiceBadge);
|
|
10695
11805
|
originChoice.append(originChoiceTop, originChoiceDescription);
|
|
10696
11806
|
const irohChoice = element("button", "dsh-mobile-control__provider is-iroh");
|
|
10697
11807
|
irohChoice.type = "button";
|
|
@@ -10705,9 +11815,21 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10705
11815
|
irohChoiceDescription.textContent = t("irohDescription");
|
|
10706
11816
|
irohChoiceTop.append(irohChoiceName, irohChoiceBadge);
|
|
10707
11817
|
irohChoice.append(irohChoiceTop, irohChoiceDescription);
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
11818
|
+
providerChoices.append(irohChoice, frpChoice, originChoice);
|
|
11819
|
+
const lanChoice = element("button", "dsh-mobile-control__provider is-lan");
|
|
11820
|
+
lanChoice.type = "button";
|
|
11821
|
+
lanChoice.setAttribute("role", "radio");
|
|
11822
|
+
lanChoice.setAttribute("aria-checked", "true");
|
|
11823
|
+
const lanChoiceTop = element("span", "dsh-mobile-control__provider-top");
|
|
11824
|
+
const lanChoiceName = element("strong");
|
|
11825
|
+
lanChoiceName.textContent = t("lan");
|
|
11826
|
+
const lanChoiceBadge = element("span", "dsh-mobile-control__provider-badge");
|
|
11827
|
+
lanChoiceBadge.textContent = t("builtIn");
|
|
11828
|
+
const lanChoiceDescription = element("span", "dsh-mobile-control__provider-description");
|
|
11829
|
+
lanChoiceDescription.textContent = t("lanChoiceDescription");
|
|
11830
|
+
lanChoiceTop.append(lanChoiceName, lanChoiceBadge);
|
|
11831
|
+
lanChoice.append(lanChoiceTop, lanChoiceDescription);
|
|
11832
|
+
providerSection.append(providerHeading, lanChoice, providerChoices);
|
|
10711
11833
|
const frpSetup = element("section", "dsh-mobile-control__frp-setup");
|
|
10712
11834
|
frpSetup.hidden = true;
|
|
10713
11835
|
const frpSetupTitle = element("h3", "dsh-mobile-control__section-title");
|
|
@@ -11003,7 +12125,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11003
12125
|
const frpVerifySummary = element("summary");
|
|
11004
12126
|
frpVerifySummary.textContent = t("frpStep4Title");
|
|
11005
12127
|
frpVerifyGroup.append(frpVerifySummary, frpStep4);
|
|
11006
|
-
frpSetup.append(frpSetupTitle, frpOverview, frpConnectionGroup, frpVpsGroup, frpComponentGroup, frpVerifyGroup
|
|
12128
|
+
frpSetup.append(frpSetupTitle, frpOverview, frpConnectionGroup, frpVpsGroup, frpComponentGroup, frpVerifyGroup);
|
|
11007
12129
|
const originSetup = element("section", "dsh-mobile-control__origin-setup");
|
|
11008
12130
|
originSetup.hidden = true;
|
|
11009
12131
|
const originSetupTitle = element("h3", "dsh-mobile-control__section-title");
|
|
@@ -11178,16 +12300,6 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11178
12300
|
tailscaleInfoText.textContent = t("tailscaleHelpText");
|
|
11179
12301
|
tailscaleInfoBody.append(tailscaleInfoText);
|
|
11180
12302
|
tailscaleInfo.append(tailscaleInfoSummary, tailscaleInfoBody);
|
|
11181
|
-
const providerSetupHeader = element("div", "dsh-mobile-control__stage-header");
|
|
11182
|
-
const providerSetupHeading = element("h3", "dsh-mobile-control__section-title");
|
|
11183
|
-
providerSetupHeading.textContent = t("currentProvider");
|
|
11184
|
-
const providerSetupName = element("span", "dsh-mobile-control__stage-value");
|
|
11185
|
-
providerSetupName.textContent = "Tailscale Funnel";
|
|
11186
|
-
const remoteStateBadge = element("span", "dsh-mobile-control__state-badge");
|
|
11187
|
-
remoteStateBadge.textContent = t("remoteStateOff");
|
|
11188
|
-
const providerSetupMeta = element("div", "dsh-mobile-control__stage-meta");
|
|
11189
|
-
providerSetupMeta.append(providerSetupName, remoteStateBadge);
|
|
11190
|
-
providerSetupHeader.append(providerSetupHeading, providerSetupMeta);
|
|
11191
12303
|
const providerSetupBody = element("div", "dsh-mobile-control__provider-setup-body");
|
|
11192
12304
|
const irohSetup = element("section", "dsh-mobile-control__frp-setup");
|
|
11193
12305
|
irohSetup.hidden = true;
|
|
@@ -11205,7 +12317,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11205
12317
|
const irohInstallProgress = element("progress", "dsh-mobile-control__progress");
|
|
11206
12318
|
irohInstallProgress.hidden = true;
|
|
11207
12319
|
irohInstallProgress.setAttribute("aria-hidden", "true");
|
|
11208
|
-
const irohPairing = element("div", "dsh-mobile-
|
|
12320
|
+
const irohPairing = element("div", "dsh-mobile-control__iroh-pairing");
|
|
11209
12321
|
const irohPairingTitle = element("p", "dsh-mobile-control__component-status");
|
|
11210
12322
|
irohPairingTitle.textContent = t("irohPairingTitle");
|
|
11211
12323
|
const irohPair = element("button", "dsh-mobile-control__primary");
|
|
@@ -11218,7 +12330,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11218
12330
|
irohCopyDescriptor.type = "button";
|
|
11219
12331
|
irohCopyDescriptor.textContent = t("copyPairLink");
|
|
11220
12332
|
irohCopyDescriptor.hidden = true;
|
|
11221
|
-
|
|
12333
|
+
irohCopyDescriptor.disabled = true;
|
|
12334
|
+
irohPairing.append(irohPairingTitle, irohQr, irohPairStatus);
|
|
11222
12335
|
const irohDetails = element("details", "dsh-mobile-control__details");
|
|
11223
12336
|
const irohDetailsSummary = element("summary");
|
|
11224
12337
|
irohDetailsSummary.textContent = t("componentDetails");
|
|
@@ -11234,9 +12347,12 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11234
12347
|
irohOfficial.target = "_blank";
|
|
11235
12348
|
irohOfficial.rel = "noopener noreferrer";
|
|
11236
12349
|
irohOfficial.textContent = t("officialDownload");
|
|
11237
|
-
|
|
12350
|
+
const irohPurge = element("button", "dsh-mobile-control__danger");
|
|
12351
|
+
irohPurge.type = "button";
|
|
12352
|
+
irohPurge.textContent = t("purgeIroh");
|
|
12353
|
+
irohDetailsBody.append(irohDetailsText, irohInstallNote, irohStorage, irohOfficial, irohPurge);
|
|
11238
12354
|
irohDetails.append(irohDetailsSummary, irohDetailsBody);
|
|
11239
|
-
irohSetup.append(irohSetupTitle, irohPhaseNote, irohComponentStatus, irohInstall, irohInstallProgress, irohPairing
|
|
12355
|
+
irohSetup.append(irohSetupTitle, irohPhaseNote, irohComponentStatus, irohInstall, irohInstallProgress, irohPairing);
|
|
11240
12356
|
providerSetupBody.append(cpolarSetup, cloudflaredSetup, frpSetup, originSetup, irohSetup, tailscaleInfo);
|
|
11241
12357
|
const irohQrZoom = element("div", "dsh-mobile-control__qr-zoom");
|
|
11242
12358
|
irohQrZoom.hidden = true;
|
|
@@ -11323,11 +12439,16 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11323
12439
|
remoteCopyLink.disabled = true;
|
|
11324
12440
|
const remotePairLink = element("p", "dsh-mobile-control__status");
|
|
11325
12441
|
remotePairLink.hidden = true;
|
|
12442
|
+
const pairingValidity = element("p", "dsh-mobile-control__component-status");
|
|
12443
|
+
pairingValidity.hidden = true;
|
|
11326
12444
|
let activeRemotePairUrl = "";
|
|
11327
12445
|
let activeRemotePairExpiresAt = 0;
|
|
12446
|
+
let remotePairingBusy = false;
|
|
12447
|
+
let remotePairingGeneration = 0;
|
|
11328
12448
|
let activeIrohDescriptor = "";
|
|
12449
|
+
let activeIrohPairExpiresAt = 0;
|
|
11329
12450
|
let activeIrohQrSvg = "";
|
|
11330
|
-
remoteActions.append(remoteToggle, remoteLogin, remoteReconnect, remotePair, remoteCopyLink);
|
|
12451
|
+
remoteActions.append(remoteToggle, irohPair, irohCopyDescriptor, remoteLogin, remoteReconnect, remotePair, remoteCopyLink);
|
|
11331
12452
|
const remoteManageRow = element("div", "dsh-mobile-control__manage-row");
|
|
11332
12453
|
const remoteDevices = element("button", "dsh-mobile-control__manage");
|
|
11333
12454
|
remoteDevices.type = "button";
|
|
@@ -11338,7 +12459,6 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11338
12459
|
remoteReset.textContent = t("resetRemoteLogin");
|
|
11339
12460
|
remoteManageRow.append(remoteDevices, remoteReset);
|
|
11340
12461
|
const remoteDevicePanel = element("div", "dsh-mobile-control__devices");
|
|
11341
|
-
remoteDevicePanel.hidden = true;
|
|
11342
12462
|
const wsPathsSection = element("section", "dsh-mobile-control__ws-paths");
|
|
11343
12463
|
const wsPathsTitle = element("h3", "dsh-mobile-control__section-title");
|
|
11344
12464
|
wsPathsTitle.textContent = t("wsPathsTitle");
|
|
@@ -11522,10 +12642,53 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11522
12642
|
wsPathsInput.value = "";
|
|
11523
12643
|
saveWsPaths([...wsPathsCurrent, value]);
|
|
11524
12644
|
});
|
|
11525
|
-
const remoteWorkspace = element("
|
|
11526
|
-
|
|
12645
|
+
const remoteWorkspace = element("div", "dsh-mobile-control__remote-workspace");
|
|
12646
|
+
const providerInstallRow = element("div", "dsh-mobile-control__provider-install");
|
|
12647
|
+
providerInstallRow.append(cpolarInstall, cloudflaredInstall);
|
|
12648
|
+
const setupDetailsRow = element("div", "dsh-mobile-control__setup-details");
|
|
12649
|
+
for (const disclosure of [
|
|
12650
|
+
cpolarDetails,
|
|
12651
|
+
cloudflaredDetails,
|
|
12652
|
+
frpDetails,
|
|
12653
|
+
irohDetails
|
|
12654
|
+
]) {
|
|
12655
|
+
disclosure.hidden = true;
|
|
12656
|
+
setupDetailsRow.append(disclosure);
|
|
12657
|
+
}
|
|
12658
|
+
remoteWorkspace.append(remoteStatus, remoteAccess, remoteGuide, providerSetupBody, remoteQr, pairingValidity, providerInstallRow, remoteActions, remotePairLink, setupDetailsRow);
|
|
11527
12659
|
const diagnosticsView = element("div", "dsh-mobile-control__view is-diagnostics");
|
|
11528
12660
|
diagnosticsView.hidden = true;
|
|
12661
|
+
const devicesView = element("div", "dsh-mobile-control__view is-devices");
|
|
12662
|
+
devicesView.hidden = true;
|
|
12663
|
+
const devicesSwitcher = element("div", "dsh-mobile-control__switcher");
|
|
12664
|
+
const devicesLanTab = element("button", "dsh-mobile-control__tab is-active");
|
|
12665
|
+
devicesLanTab.type = "button";
|
|
12666
|
+
devicesLanTab.textContent = t("devicesLanTitle");
|
|
12667
|
+
const devicesRemoteTab = element("button", "dsh-mobile-control__tab");
|
|
12668
|
+
devicesRemoteTab.type = "button";
|
|
12669
|
+
devicesRemoteTab.textContent = t("devicesRemoteTitle");
|
|
12670
|
+
devicesSwitcher.append(devicesLanTab, devicesRemoteTab);
|
|
12671
|
+
const devicesLanGroup = element("div");
|
|
12672
|
+
const devicesLanRow = element("div", "dsh-mobile-control__manage-row");
|
|
12673
|
+
devicesLanRow.append(resetAll);
|
|
12674
|
+
devicesLanGroup.append(devicePanel, devicesLanRow);
|
|
12675
|
+
const devicesRemoteGroup = element("div");
|
|
12676
|
+
devicesRemoteGroup.hidden = true;
|
|
12677
|
+
const devicesRemoteRow = element("div", "dsh-mobile-control__manage-row");
|
|
12678
|
+
devicesRemoteRow.append(remoteReset);
|
|
12679
|
+
devicesRemoteGroup.append(remoteDevicePanel, devicesRemoteRow);
|
|
12680
|
+
devicesView.append(devicesSwitcher, devicesLanGroup, devicesRemoteGroup);
|
|
12681
|
+
const showDevicesGroup = (which) => {
|
|
12682
|
+
const lan = which === "lan";
|
|
12683
|
+
devicesLanGroup.hidden = !lan;
|
|
12684
|
+
devicesRemoteGroup.hidden = lan;
|
|
12685
|
+
devicesLanTab.classList.toggle("is-active", lan);
|
|
12686
|
+
devicesRemoteTab.classList.toggle("is-active", !lan);
|
|
12687
|
+
if (lan) loadDevices();
|
|
12688
|
+
else loadRemoteDevices();
|
|
12689
|
+
};
|
|
12690
|
+
devicesLanTab.addEventListener("click", () => showDevicesGroup("lan"));
|
|
12691
|
+
devicesRemoteTab.addEventListener("click", () => showDevicesGroup("remote"));
|
|
11529
12692
|
const diagnosticsIntro = element("p", "dsh-mobile-control__intro");
|
|
11530
12693
|
diagnosticsIntro.textContent = t("diagnosticsIntro");
|
|
11531
12694
|
const diagnosticsSummary = element("section", "dsh-mobile-control__diagnostic-summary is-idle");
|
|
@@ -11567,10 +12730,14 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11567
12730
|
diagnosticsDetails.append(diagnosticsDetailsSummary, diagnosticsReport);
|
|
11568
12731
|
header.append(title, headerActions);
|
|
11569
12732
|
actions.append(toggle, pair, linkPair);
|
|
11570
|
-
lanView.append(lanSetup, access, qrBox, status, extensionStatus, actions,
|
|
11571
|
-
|
|
12733
|
+
lanView.append(lanSetup, access, qrBox, lanPairingValidity, status, extensionStatus, actions, trustedNetworks);
|
|
12734
|
+
const channelPane = element("div", "dsh-mobile-control__channel-pane");
|
|
12735
|
+
channelPane.append(lanView, remoteWorkspace);
|
|
12736
|
+
const remoteColumns = element("div", "dsh-mobile-control__remote-columns");
|
|
12737
|
+
remoteColumns.append(providerSection, channelPane);
|
|
12738
|
+
remoteView.append(remoteIntro, httpFrameWarning, remoteColumns);
|
|
11572
12739
|
diagnosticsView.append(diagnosticsIntro, diagnosticsSummary, diagnosticsToolbar, diagnosticsFeedback, diagnosticsChecks, wsPathsSection, diagnosticsDetails);
|
|
11573
|
-
panel.append(header, releaseNotice, updateCard,
|
|
12740
|
+
panel.append(header, releaseNotice, updateCard, appDownloads, extensionRecovery.section, remoteView, diagnosticsView, devicesView);
|
|
11574
12741
|
root.append(panel);
|
|
11575
12742
|
document.body.append(root);
|
|
11576
12743
|
let running = false;
|
|
@@ -11593,6 +12760,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11593
12760
|
let cloudflaredInstalled = false;
|
|
11594
12761
|
let cloudflaredDownloadSize = "";
|
|
11595
12762
|
let cloudflaredTunnelMode = "quick";
|
|
12763
|
+
let cloudflaredConfigEditing = false;
|
|
11596
12764
|
let cloudflaredModeDraft = null;
|
|
11597
12765
|
let cloudflaredTunnelConfigured = false;
|
|
11598
12766
|
let configuredCloudflaredHostname = "";
|
|
@@ -11641,8 +12809,6 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11641
12809
|
let frpEntryTlsDraft = false;
|
|
11642
12810
|
let frpVhostDraft = false;
|
|
11643
12811
|
let frpAttachVhostExplicit = false;
|
|
11644
|
-
let providerInfoPinned = false;
|
|
11645
|
-
let providerInfoHovered = false;
|
|
11646
12812
|
let previousAccessView = "lan";
|
|
11647
12813
|
let diagnosticsBusy = false;
|
|
11648
12814
|
let copiedDiagnosticReport = "";
|
|
@@ -11659,78 +12825,140 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11659
12825
|
updatePlugin.textContent = t("updatePlugin");
|
|
11660
12826
|
if (pluginLatestVersion !== "") updatePlugin.setAttribute("aria-label", t("updatePluginAria", { version: pluginLatestVersion }));
|
|
11661
12827
|
if (release.androidVersion !== void 0) {
|
|
11662
|
-
|
|
11663
|
-
|
|
12828
|
+
const version = release.androidVersion;
|
|
12829
|
+
const sizeMb = (bytes) => bytes === void 0 ? void 0 : (bytes / 1048576).toFixed(1);
|
|
12830
|
+
const compatSize = sizeMb(release.androidCompatBytes);
|
|
12831
|
+
const irohSize = sizeMb(release.androidIrohBytes);
|
|
12832
|
+
appDownload.textContent = compatSize === void 0 ? t("downloadAndroidVersion", { version }) : t("downloadAndroidVersionSize", {
|
|
12833
|
+
version,
|
|
12834
|
+
size: compatSize
|
|
12835
|
+
});
|
|
12836
|
+
appDownload.setAttribute("aria-label", t("downloadAndroidVersionAria", { version }));
|
|
12837
|
+
appDownloadIroh.textContent = irohSize === void 0 ? t("downloadAndroidIrohVersion", { version }) : t("downloadAndroidIrohVersionSize", {
|
|
12838
|
+
version,
|
|
12839
|
+
size: irohSize
|
|
12840
|
+
});
|
|
12841
|
+
appDownloadIroh.setAttribute("aria-label", t("downloadAndroidIrohAria", { version }));
|
|
11664
12842
|
}
|
|
11665
12843
|
appDownload.href = release.androidDownloadUrl;
|
|
12844
|
+
if (release.androidIrohDownloadUrl === void 0) appDownloadIroh.hidden = true;
|
|
12845
|
+
else {
|
|
12846
|
+
appDownloadIroh.href = release.androidIrohDownloadUrl;
|
|
12847
|
+
appDownloadIroh.hidden = false;
|
|
12848
|
+
}
|
|
11666
12849
|
};
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
12850
|
+
let currentAccessView = "lan";
|
|
12851
|
+
const applyChoiceSelection = () => {
|
|
12852
|
+
const inRemote = currentAccessView === "remote";
|
|
12853
|
+
lanChoice.classList.toggle("is-selected", currentAccessView === "lan");
|
|
12854
|
+
lanChoice.setAttribute("aria-checked", String(currentAccessView === "lan"));
|
|
12855
|
+
tailscaleChoice.classList.toggle("is-selected", inRemote && remoteProvider === "tailscale");
|
|
12856
|
+
cpolarChoice.classList.toggle("is-selected", inRemote && remoteProvider === "cpolar");
|
|
12857
|
+
cloudflaredChoice.classList.toggle("is-selected", inRemote && remoteProvider === "cloudflared");
|
|
12858
|
+
frpChoice.classList.toggle("is-selected", inRemote && remoteProvider === "frp");
|
|
12859
|
+
originChoice.classList.toggle("is-selected", inRemote && remoteProvider === "origin");
|
|
12860
|
+
irohChoice.classList.toggle("is-selected", inRemote && remoteProvider === "iroh");
|
|
11671
12861
|
};
|
|
11672
|
-
providerInfo.addEventListener("pointerenter", () => {
|
|
11673
|
-
providerInfoHovered = true;
|
|
11674
|
-
syncProviderInfo();
|
|
11675
|
-
});
|
|
11676
|
-
providerInfo.addEventListener("pointerleave", () => {
|
|
11677
|
-
providerInfoHovered = false;
|
|
11678
|
-
syncProviderInfo();
|
|
11679
|
-
});
|
|
11680
|
-
providerInfo.addEventListener("focusin", syncProviderInfo);
|
|
11681
|
-
providerInfo.addEventListener("focusout", () => {
|
|
11682
|
-
window.setTimeout(syncProviderInfo, 0);
|
|
11683
|
-
});
|
|
11684
|
-
providerInfoButton.addEventListener("click", () => {
|
|
11685
|
-
providerInfoPinned = !providerInfoPinned;
|
|
11686
|
-
syncProviderInfo();
|
|
11687
|
-
});
|
|
11688
|
-
providerInfoButton.addEventListener("keydown", (event) => {
|
|
11689
|
-
if (event.key !== "Escape") return;
|
|
11690
|
-
providerInfoPinned = false;
|
|
11691
|
-
providerInfoHovered = false;
|
|
11692
|
-
providerInfoPopover.hidden = true;
|
|
11693
|
-
providerInfoButton.setAttribute("aria-expanded", "false");
|
|
11694
|
-
});
|
|
11695
12862
|
const selectView = (view) => {
|
|
11696
12863
|
if (!panel.hidden) extensionRecovery.refresh();
|
|
11697
|
-
|
|
12864
|
+
currentAccessView = view;
|
|
12865
|
+
if (view === "lan" || view === "remote") previousAccessView = view;
|
|
12866
|
+
const subpage = view === "diagnostics" || view === "devices";
|
|
12867
|
+
remoteView.hidden = subpage;
|
|
11698
12868
|
lanView.hidden = view !== "lan";
|
|
11699
|
-
|
|
12869
|
+
remoteWorkspace.hidden = view !== "remote";
|
|
11700
12870
|
diagnosticsView.hidden = view !== "diagnostics";
|
|
12871
|
+
devicesView.hidden = view !== "devices";
|
|
11701
12872
|
lanTab.classList.toggle("is-active", view === "lan");
|
|
11702
12873
|
remoteTab.classList.toggle("is-active", view === "remote");
|
|
12874
|
+
applyChoiceSelection();
|
|
11703
12875
|
lanTab.setAttribute("aria-pressed", String(view === "lan"));
|
|
11704
12876
|
remoteTab.setAttribute("aria-pressed", String(view === "remote"));
|
|
11705
12877
|
diagnosticsEntry.setAttribute("aria-pressed", String(view === "diagnostics"));
|
|
11706
12878
|
diagnosticsEntry.textContent = view === "diagnostics" ? t("back") : t("diagnostics");
|
|
11707
12879
|
diagnosticsEntry.setAttribute("aria-label", view === "diagnostics" ? t("backToMobile") : t("openDiagnostics"));
|
|
12880
|
+
devicesEntry.setAttribute("aria-pressed", String(view === "devices"));
|
|
12881
|
+
devicesEntry.textContent = view === "devices" ? t("back") : t("device");
|
|
12882
|
+
devicesEntry.setAttribute("aria-label", view === "devices" ? t("backToMobile") : t("managePairedDevices"));
|
|
11708
12883
|
if (view === "diagnostics") {
|
|
11709
12884
|
wsPathsSeenAttempts = wsBlockedTotal(wsPathsBlocked);
|
|
11710
12885
|
renderWsDot();
|
|
11711
12886
|
if (!wsPathsLoaded) loadWsPaths();
|
|
11712
12887
|
}
|
|
11713
|
-
updatePlugin.hidden =
|
|
11714
|
-
|
|
11715
|
-
switcher.hidden =
|
|
11716
|
-
title.textContent = view === "lan" ? t("lanAccess") : view === "remote" ? t("remoteAccess") : t("connectionDiagnostics");
|
|
12888
|
+
updatePlugin.hidden = subpage || !pluginUpdateAvailable;
|
|
12889
|
+
appDownloads.hidden = subpage;
|
|
12890
|
+
switcher.hidden = subpage;
|
|
12891
|
+
title.textContent = view === "lan" ? t("lanAccess") : view === "remote" ? t("remoteAccess") : view === "devices" ? t("managePairedDevices") : t("connectionDiagnostics");
|
|
11717
12892
|
};
|
|
11718
12893
|
lanTab.addEventListener("click", () => {
|
|
11719
12894
|
selectView("lan");
|
|
11720
12895
|
});
|
|
12896
|
+
lanChoice.addEventListener("click", () => {
|
|
12897
|
+
selectView("lan");
|
|
12898
|
+
});
|
|
12899
|
+
providerChoices.addEventListener("click", () => {
|
|
12900
|
+
selectView("remote");
|
|
12901
|
+
loadRemote();
|
|
12902
|
+
});
|
|
11721
12903
|
remoteTab.addEventListener("click", () => {
|
|
11722
12904
|
selectView("remote");
|
|
11723
12905
|
loadRemote();
|
|
11724
12906
|
});
|
|
12907
|
+
selectView("lan");
|
|
12908
|
+
let returnFocus;
|
|
11725
12909
|
const setOpen = (open) => {
|
|
12910
|
+
const changed = panel.hidden === open;
|
|
12911
|
+
if (open && changed && document.activeElement instanceof HTMLElement) returnFocus = document.activeElement;
|
|
11726
12912
|
panel.hidden = !open;
|
|
12913
|
+
if (open) {
|
|
12914
|
+
panel.setAttribute("role", "dialog");
|
|
12915
|
+
panel.setAttribute("aria-modal", "true");
|
|
12916
|
+
} else {
|
|
12917
|
+
panel.removeAttribute("role");
|
|
12918
|
+
panel.removeAttribute("aria-modal");
|
|
12919
|
+
}
|
|
11727
12920
|
if (open) extensionRecovery.refresh();
|
|
12921
|
+
if (open && changed) controlRequestJson("/api/mobile-access/release").then(renderRelease, () => {});
|
|
12922
|
+
if (changed) {
|
|
12923
|
+
if (open) close.focus();
|
|
12924
|
+
else if (returnFocus?.isConnected) returnFocus.focus();
|
|
12925
|
+
}
|
|
11728
12926
|
for (const trigger of document.querySelectorAll(".dsh-mobile-control__trigger")) trigger.setAttribute("aria-expanded", String(open));
|
|
11729
12927
|
};
|
|
12928
|
+
panel.addEventListener("keydown", (event) => {
|
|
12929
|
+
if (event.defaultPrevented) return;
|
|
12930
|
+
if (event.key === "Escape") {
|
|
12931
|
+
event.preventDefault();
|
|
12932
|
+
setOpen(false);
|
|
12933
|
+
return;
|
|
12934
|
+
}
|
|
12935
|
+
if (event.key !== "Tab") return;
|
|
12936
|
+
const controls = [...panel.querySelectorAll("button,a[href],input,select,textarea,[tabindex]")].filter((node) => node.tabIndex >= 0 && !node.matches(":disabled") && node.getClientRects().length > 0);
|
|
12937
|
+
const first = controls[0], last = controls.at(-1);
|
|
12938
|
+
if (event.shiftKey && document.activeElement === first && last !== void 0) {
|
|
12939
|
+
event.preventDefault();
|
|
12940
|
+
last.focus();
|
|
12941
|
+
} else if (!event.shiftKey && document.activeElement === last && first !== void 0) {
|
|
12942
|
+
event.preventDefault();
|
|
12943
|
+
first.focus();
|
|
12944
|
+
}
|
|
12945
|
+
});
|
|
12946
|
+
const renderLanPairingExpiry = () => {
|
|
12947
|
+
if (!running || activeLanPairExpiresAt === 0) return;
|
|
12948
|
+
lanPairingValidity.hidden = false;
|
|
12949
|
+
if (Date.now() >= activeLanPairExpiresAt) {
|
|
12950
|
+
activeLanPairUrl = "";
|
|
12951
|
+
showQr("");
|
|
12952
|
+
qrBox.textContent = t("pairingExpired");
|
|
12953
|
+
lanPairingValidity.textContent = t("pairingExpired");
|
|
12954
|
+
} else lanPairingValidity.textContent = t("pairingExpires", { time: formatTime(activeLanPairExpiresAt) });
|
|
12955
|
+
};
|
|
11730
12956
|
const render = (data) => {
|
|
12957
|
+
const previousOrigin = origin;
|
|
11731
12958
|
lanConfigured = data.configured !== false;
|
|
11732
12959
|
lanRestartRequired = data.restartRequired === true;
|
|
11733
12960
|
running = data.running === true;
|
|
12961
|
+
lanChoiceBadge.textContent = running ? t("lanBadgeOn") : t("lanBadgeOff");
|
|
11734
12962
|
origin = running && typeof data.origin === "string" ? data.origin : "";
|
|
11735
12963
|
lanSetup.hidden = lanConfigured && !lanRestartRequired;
|
|
11736
12964
|
lanSetupForm.hidden = lanRestartRequired;
|
|
@@ -11757,12 +12985,18 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11757
12985
|
failed
|
|
11758
12986
|
});
|
|
11759
12987
|
} else extensionStatus.hidden = true;
|
|
11760
|
-
if (!running
|
|
12988
|
+
if (!running || previousOrigin !== origin) {
|
|
12989
|
+
lanPairingGeneration += 1;
|
|
12990
|
+
activeLanPairUrl = "";
|
|
12991
|
+
activeLanPairExpiresAt = 0;
|
|
12992
|
+
lanPairingValidity.hidden = true;
|
|
12993
|
+
showQr("");
|
|
12994
|
+
} else renderLanPairingExpiry();
|
|
11761
12995
|
toggle.textContent = running ? t("disableLan") : t("enableLan");
|
|
11762
12996
|
actions.hidden = !lanConfigured || lanRestartRequired;
|
|
11763
12997
|
manageRow.hidden = !lanConfigured || lanRestartRequired;
|
|
11764
|
-
pair.disabled = !running;
|
|
11765
|
-
linkPair.disabled = !running;
|
|
12998
|
+
pair.disabled = !running || lanPairingBusy;
|
|
12999
|
+
linkPair.disabled = !running || lanPairingBusy;
|
|
11766
13000
|
manageDevices.disabled = !running;
|
|
11767
13001
|
resetAll.disabled = !running;
|
|
11768
13002
|
};
|
|
@@ -11831,10 +13065,25 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11831
13065
|
lanSetupConfigure.disabled = lanRestartRequired || lanSetupSelect.options.length === 0;
|
|
11832
13066
|
});
|
|
11833
13067
|
});
|
|
13068
|
+
const clearQrTarget = (target) => {
|
|
13069
|
+
if (typeof target.replaceChildren === "function") {
|
|
13070
|
+
target.replaceChildren();
|
|
13071
|
+
return;
|
|
13072
|
+
}
|
|
13073
|
+
try {
|
|
13074
|
+
while (target.firstChild) target.removeChild(target.firstChild);
|
|
13075
|
+
} catch {}
|
|
13076
|
+
};
|
|
11834
13077
|
const showQr = (svg, target = qrBox) => {
|
|
11835
|
-
target
|
|
13078
|
+
clearQrTarget(target);
|
|
11836
13079
|
if (svg === "") {
|
|
11837
|
-
|
|
13080
|
+
const placeholderText = target === irohQr ? t("irohQrPlaceholder") : target === remoteQr ? t("remoteQrPlaceholder") : target === qrBox ? t("qrPlaceholder") : void 0;
|
|
13081
|
+
if (placeholderText !== void 0) {
|
|
13082
|
+
const placeholder = element("span", "dsh-mobile-control__qr-placeholder");
|
|
13083
|
+
placeholder.textContent = placeholderText;
|
|
13084
|
+
target.hidden = false;
|
|
13085
|
+
target.append(placeholder);
|
|
13086
|
+
} else target.hidden = true;
|
|
11838
13087
|
return;
|
|
11839
13088
|
}
|
|
11840
13089
|
const image = element("img");
|
|
@@ -11845,23 +13094,68 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11845
13094
|
target.hidden = false;
|
|
11846
13095
|
target.append(image);
|
|
11847
13096
|
};
|
|
13097
|
+
showQr("");
|
|
13098
|
+
const showIrohQrPlaceholder = () => {
|
|
13099
|
+
clearQrTarget(irohQr);
|
|
13100
|
+
const placeholder = element("span", "dsh-mobile-control__qr-placeholder");
|
|
13101
|
+
placeholder.textContent = t("irohQrPlaceholder");
|
|
13102
|
+
irohQr.hidden = false;
|
|
13103
|
+
irohQr.append(placeholder);
|
|
13104
|
+
};
|
|
13105
|
+
const showRemoteQrPlaceholder = (text) => {
|
|
13106
|
+
clearQrTarget(remoteQr);
|
|
13107
|
+
const placeholder = element("span", "dsh-mobile-control__qr-placeholder");
|
|
13108
|
+
placeholder.textContent = text;
|
|
13109
|
+
remoteQr.hidden = false;
|
|
13110
|
+
remoteQr.append(placeholder);
|
|
13111
|
+
};
|
|
11848
13112
|
const openPairing = (target) => {
|
|
11849
|
-
|
|
13113
|
+
if (lanPairingBusy || !running) return;
|
|
13114
|
+
lanPairingBusy = true;
|
|
13115
|
+
pair.disabled = true;
|
|
13116
|
+
linkPair.disabled = true;
|
|
13117
|
+
const pairingOrigin = origin;
|
|
13118
|
+
const generation = lanPairingGeneration;
|
|
13119
|
+
if (target === "key" || activeLanPairExpiresAt <= Date.now()) {
|
|
13120
|
+
activeLanPairUrl = "";
|
|
13121
|
+
activeLanPairExpiresAt = 0;
|
|
13122
|
+
lanPairingValidity.hidden = true;
|
|
13123
|
+
status.classList.remove("is-key");
|
|
13124
|
+
showQr("");
|
|
13125
|
+
}
|
|
13126
|
+
(target === "link" && activeLanPairUrl !== "" && activeLanPairExpiresAt > Date.now() ? Promise.resolve({
|
|
13127
|
+
pairUrl: activeLanPairUrl,
|
|
13128
|
+
expiresAt: activeLanPairExpiresAt
|
|
13129
|
+
}) : controlRequestJson("/api/mobile-access/lan/pairing/open", {
|
|
11850
13130
|
method: "POST",
|
|
11851
13131
|
body: "{}"
|
|
11852
|
-
}).then(async (data) => {
|
|
11853
|
-
|
|
11854
|
-
|
|
13132
|
+
})).then(async (data) => {
|
|
13133
|
+
if (lifecycle.signal.aborted || !running || origin !== pairingOrigin || generation !== lanPairingGeneration) return;
|
|
13134
|
+
const value = typeof data.pairUrl === "string" ? data.pairUrl : "";
|
|
13135
|
+
if (value === "" || typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt) || data.expiresAt <= Date.now()) {
|
|
13136
|
+
status.textContent = t("keyGenerationFailed");
|
|
13137
|
+
return;
|
|
13138
|
+
}
|
|
13139
|
+
activeLanPairUrl = value;
|
|
13140
|
+
activeLanPairExpiresAt = data.expiresAt;
|
|
13141
|
+
lanPairingValidity.hidden = false;
|
|
13142
|
+
lanPairingValidity.textContent = t("pairingExpires", { time: formatTime(data.expiresAt) });
|
|
13143
|
+
if ("qrSvg" in data) showQr(typeof data.qrSvg === "string" ? data.qrSvg : "");
|
|
13144
|
+
if (target === "key") {
|
|
13145
|
+
status.classList.remove("is-key");
|
|
13146
|
+
status.textContent = t("qrGenerated");
|
|
13147
|
+
return;
|
|
13148
|
+
}
|
|
11855
13149
|
if (value === "") {
|
|
11856
13150
|
status.textContent = t("keyGenerationFailed");
|
|
11857
13151
|
return;
|
|
11858
13152
|
}
|
|
11859
13153
|
try {
|
|
11860
13154
|
await navigator.clipboard.writeText(value);
|
|
11861
|
-
status.textContent =
|
|
13155
|
+
status.textContent = t("linkCopied");
|
|
11862
13156
|
} catch {
|
|
11863
13157
|
status.textContent = t("copySecret", {
|
|
11864
|
-
kind:
|
|
13158
|
+
kind: t("pairingLink"),
|
|
11865
13159
|
value
|
|
11866
13160
|
});
|
|
11867
13161
|
status.classList.add("is-key");
|
|
@@ -11869,6 +13163,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11869
13163
|
}, (error) => {
|
|
11870
13164
|
status.textContent = t("requestFailed", { error: String(error) });
|
|
11871
13165
|
}).finally(() => {
|
|
13166
|
+
lanPairingBusy = false;
|
|
11872
13167
|
pair.disabled = !running;
|
|
11873
13168
|
linkPair.disabled = !running;
|
|
11874
13169
|
});
|
|
@@ -11923,12 +13218,15 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11923
13218
|
const loadDevices = () => {
|
|
11924
13219
|
controlRequestJson("/api/mobile-access/lan/devices").then(renderDevices, (error) => {
|
|
11925
13220
|
status.textContent = t("requestFailed", { error: String(error) });
|
|
13221
|
+
devicePanel.replaceChildren();
|
|
13222
|
+
const note = element("p", "dsh-mobile-control__device-empty");
|
|
13223
|
+
note.textContent = t("noDevices");
|
|
13224
|
+
devicePanel.append(note);
|
|
11926
13225
|
});
|
|
11927
13226
|
};
|
|
11928
13227
|
manageDevices.addEventListener("click", () => {
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
if (show) loadDevices();
|
|
13228
|
+
selectView("devices");
|
|
13229
|
+
loadDevices();
|
|
11932
13230
|
});
|
|
11933
13231
|
resetAll.addEventListener("click", () => {
|
|
11934
13232
|
if (!window.confirm(t("confirmResetDevices"))) return;
|
|
@@ -11940,6 +13238,15 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11940
13238
|
});
|
|
11941
13239
|
});
|
|
11942
13240
|
const renderRemote = (data) => {
|
|
13241
|
+
if (data.provider !== latestRemoteState.provider || data.origin !== latestRemoteState.origin) {
|
|
13242
|
+
remotePairingGeneration += 1;
|
|
13243
|
+
activeRemotePairUrl = "";
|
|
13244
|
+
activeRemotePairExpiresAt = 0;
|
|
13245
|
+
remotePairLink.hidden = true;
|
|
13246
|
+
remotePairLink.textContent = "";
|
|
13247
|
+
pairingValidity.hidden = true;
|
|
13248
|
+
showQr("", remoteQr);
|
|
13249
|
+
}
|
|
11943
13250
|
latestRemoteState = data;
|
|
11944
13251
|
remoteRunning = data.running === true;
|
|
11945
13252
|
remoteProvider = parseMobileRemoteProvider(data.provider);
|
|
@@ -11950,11 +13257,6 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11950
13257
|
const iroh = remoteProvider === "iroh";
|
|
11951
13258
|
const managedOrigin = origin && data.originMode === "managed";
|
|
11952
13259
|
const tailscale = remoteProvider === "tailscale";
|
|
11953
|
-
tailscaleChoice.classList.toggle("is-selected", tailscale);
|
|
11954
|
-
cpolarChoice.classList.toggle("is-selected", cpolar);
|
|
11955
|
-
cloudflaredChoice.classList.toggle("is-selected", cloudflared);
|
|
11956
|
-
frpChoice.classList.toggle("is-selected", frp);
|
|
11957
|
-
originChoice.classList.toggle("is-selected", origin);
|
|
11958
13260
|
tailscaleChoice.setAttribute("aria-checked", String(tailscale));
|
|
11959
13261
|
cpolarChoice.setAttribute("aria-checked", String(cpolar));
|
|
11960
13262
|
cloudflaredChoice.setAttribute("aria-checked", String(cloudflared));
|
|
@@ -11964,7 +13266,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11964
13266
|
frpChoice.setAttribute("aria-pressed", String(frp));
|
|
11965
13267
|
originChoice.setAttribute("aria-pressed", String(origin));
|
|
11966
13268
|
irohChoice.setAttribute("aria-pressed", String(iroh));
|
|
11967
|
-
|
|
13269
|
+
applyChoiceSelection();
|
|
11968
13270
|
tailscaleChoice.disabled = remoteProviderBusy;
|
|
11969
13271
|
cpolarChoice.disabled = remoteProviderBusy;
|
|
11970
13272
|
cloudflaredChoice.disabled = remoteProviderBusy;
|
|
@@ -11976,7 +13278,6 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11976
13278
|
frpSetup.hidden = !frp;
|
|
11977
13279
|
originSetup.hidden = !origin;
|
|
11978
13280
|
irohSetup.hidden = !iroh;
|
|
11979
|
-
if (frp || origin || iroh) selfHosted.open = true;
|
|
11980
13281
|
tailscaleInfo.hidden = !tailscale;
|
|
11981
13282
|
remoteReset.textContent = tailscale ? t("resetRemoteLogin") : t("resetRemoteDevices");
|
|
11982
13283
|
const providers = data.providers !== null && typeof data.providers === "object" ? data.providers : {};
|
|
@@ -11991,7 +13292,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11991
13292
|
const componentStorage = typeof component.storagePath === "string" ? component.storagePath : `DSH Mobile ${t("pluginPrivateDirectory")}`;
|
|
11992
13293
|
cpolarStorage.textContent = componentStorage;
|
|
11993
13294
|
cpolarStorage.title = componentStorage;
|
|
11994
|
-
cpolarInstall.hidden = cpolarInstalled || !cpolarSupported;
|
|
13295
|
+
cpolarInstall.hidden = !cpolar || cpolarInstalled || !cpolarSupported;
|
|
11995
13296
|
cpolarInstall.textContent = componentDownloadBytes > 0 ? t("installWithSize", { size: formatMegabytes(componentDownloadBytes) }) : t("installOfficial");
|
|
11996
13297
|
cpolarInstall.disabled = remoteProviderBusy;
|
|
11997
13298
|
cpolarAccount.hidden = !cpolarInstalled || cpolarConfigured;
|
|
@@ -12010,7 +13311,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12010
13311
|
cloudflaredStorage.textContent = cloudflaredStoragePath;
|
|
12011
13312
|
cloudflaredStorage.title = cloudflaredStoragePath;
|
|
12012
13313
|
cloudflaredChoiceBadge.textContent = cloudflaredInstalled ? t("ready") : t("cloudflaredBadge");
|
|
12013
|
-
cloudflaredInstall.hidden = cloudflaredInstalled || !cloudflaredSupported;
|
|
13314
|
+
cloudflaredInstall.hidden = !cloudflared || cloudflaredInstalled || !cloudflaredSupported;
|
|
12014
13315
|
cloudflaredInstall.textContent = cloudflaredDownloadBytes > 0 ? t("installWithSize", { size: formatMegabytes(cloudflaredDownloadBytes) }) : t("installOfficial");
|
|
12015
13316
|
cloudflaredInstall.disabled = remoteProviderBusy;
|
|
12016
13317
|
cloudflaredPurge.hidden = !cloudflaredInstalled;
|
|
@@ -12031,8 +13332,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12031
13332
|
cloudflaredModeNamed.disabled = remoteProviderBusy;
|
|
12032
13333
|
cloudflaredModeHint.textContent = cloudflaredShownMode === "named" ? t("cloudflaredTunnelNamedHint") : t("cloudflaredTunnelQuickHint");
|
|
12033
13334
|
cloudflaredNote.textContent = cloudflaredShownMode === "named" ? t("cloudflaredNamedNote") : t("cloudflaredQuickNote");
|
|
12034
|
-
cloudflaredTunnel.hidden = cloudflaredShownMode !== "named";
|
|
12035
13335
|
cloudflaredTunnelSave.textContent = cloudflaredSaveLabel();
|
|
13336
|
+
syncCloudflaredForm();
|
|
12036
13337
|
if (!cloudflaredHostnameDirty) cloudflaredHostname.value = configuredCloudflaredHostname;
|
|
12037
13338
|
if (!cloudflaredPortDirty) cloudflaredPort.value = String(configuredCloudflaredPort);
|
|
12038
13339
|
cloudflaredToken.placeholder = cloudflaredTunnelConfigured ? t("cloudflaredTunnelTokenSaved") : t("cloudflaredTunnelTokenPlaceholder");
|
|
@@ -12140,6 +13441,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12140
13441
|
const irohDownloadBytes = typeof irohComponent.downloadBytes === "number" ? irohComponent.downloadBytes : 0;
|
|
12141
13442
|
const irohStoragePath = typeof irohComponent.storagePath === "string" ? irohComponent.storagePath : `DSH Mobile ${t("pluginPrivateDirectory")}`;
|
|
12142
13443
|
irohInstall.hidden = irohInstalled || !irohSupported;
|
|
13444
|
+
irohPurge.hidden = !irohInstalled;
|
|
13445
|
+
irohPurge.disabled = iroh && remoteRunning;
|
|
12143
13446
|
if (irohVerifying) {
|
|
12144
13447
|
irohInstallProgress.hidden = false;
|
|
12145
13448
|
irohInstall.disabled = true;
|
|
@@ -12233,11 +13536,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12233
13536
|
}[errorCode] ?? "";
|
|
12234
13537
|
const needsFunnelSetup = state === "error" && remoteSetupUrl !== "";
|
|
12235
13538
|
remoteReady = remoteRunning && state === "ready" && remoteOrigin !== "";
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
remoteStateBadge.classList.toggle("is-attention", state === "error" || state === "unavailable");
|
|
12240
|
-
remoteStateBadge.textContent = remoteReady ? origin && !managedOrigin ? t("originBackendReady") : t("ready") : state === "starting" || state === "connecting" || state === "needs-login" ? t("remoteStateConnecting") : state === "error" || state === "unavailable" ? t("remoteStateAttention") : t("remoteStateOff");
|
|
13539
|
+
frpChoiceBadge.textContent = frpConfigured && frpInstalled ? t("ready") : t("advanced");
|
|
13540
|
+
originChoiceBadge.textContent = managedOrigin ? t(remoteReady ? "ready" : "advanced") : originListening ? t("originBackendReady") : t("advanced");
|
|
13541
|
+
if (remoteStatus.style) remoteStatus.style.display = state === "off" && remoteFailureText === "" ? "none" : "";
|
|
12241
13542
|
remoteAccess.hidden = !remoteReady;
|
|
12242
13543
|
remoteAccessLink.href = remoteOrigin;
|
|
12243
13544
|
remoteAccessLink.textContent = remoteOrigin;
|
|
@@ -12245,7 +13546,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12245
13546
|
remoteStatus.classList.toggle("is-running", remoteReady);
|
|
12246
13547
|
const labels = {
|
|
12247
13548
|
off: t("remoteOff"),
|
|
12248
|
-
unavailable: cpolar ? t("remoteUnavailableCpolar") : cloudflared ? t("remoteUnavailableCloudflared") : frp ? t("remoteUnavailableFrp") : origin ? t("originUnavailable") : iroh ? t("irohUnavailable") : t("remoteUnavailableTailscale"),
|
|
13549
|
+
unavailable: cpolar ? t("remoteUnavailableCpolar") : cloudflared ? t("remoteUnavailableCloudflared") : frp ? t("remoteUnavailableFrp") : origin ? t("originUnavailable") : iroh ? irohInstalled ? t("irohInstalledNotReady") : t("irohUnavailable") : t("remoteUnavailableTailscale"),
|
|
12249
13550
|
starting: cpolar ? t("remoteStartingCpolar") : cloudflared ? t("remoteStartingCloudflared") : frp ? t("remoteStartingFrp") : origin ? t("originStarting") : iroh ? t("irohStarting") : t("remoteStartingTailscale"),
|
|
12250
13551
|
"needs-login": t("remoteNeedsLogin"),
|
|
12251
13552
|
connecting: cpolar ? t("remoteConnectingCpolar") : cloudflared ? t("remoteConnectingCloudflared") : frp ? t("remoteConnectingFrp") : iroh ? t("irohConnecting") : t("remoteConnectingTailscale"),
|
|
@@ -12264,12 +13565,19 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12264
13565
|
remoteToggle.textContent = remoteRunning ? t("disableRemote") : t("enableRemote");
|
|
12265
13566
|
const originPrepared = data.originMode === "managed" ? caddySupported && caddyInstalled && caddyConfiguration.configured === true : originConfigured && !caddyModeManagedSelected;
|
|
12266
13567
|
const providerPrepared = cpolar ? cpolarInstalled && cpolarConfigured : cloudflared ? cloudflaredInstalled : frp ? frpInstalled && frpConfigured : origin ? originPrepared : iroh ? irohInstalled : true;
|
|
12267
|
-
|
|
13568
|
+
const namedUnconfigured = cloudflared && cloudflaredShownMode === "named" && cloudflaredTunnelMode !== "named" && !remoteRunning;
|
|
13569
|
+
remoteToggle.disabled = remoteProviderBusy || !providerPrepared && !(origin && remoteRunning) || namedUnconfigured;
|
|
13570
|
+
if (namedUnconfigured) {
|
|
13571
|
+
remoteStatus.textContent = t("cloudflaredEnableNeedsConfig");
|
|
13572
|
+
if (remoteStatus.style) remoteStatus.style.display = "";
|
|
13573
|
+
}
|
|
12268
13574
|
remoteLogin.hidden = !tailscale || state !== "needs-login" || remoteLoginUrl === "";
|
|
12269
13575
|
remoteReconnect.hidden = needsFunnelSetup || state !== "error" && state !== "unavailable" || !providerPrepared;
|
|
12270
13576
|
remoteActions.hidden = !providerPrepared && !(origin && remoteRunning);
|
|
12271
|
-
|
|
12272
|
-
|
|
13577
|
+
if (!providerPrepared) remoteQr.hidden = true;
|
|
13578
|
+
providerInstallRow.hidden = cpolarInstall.hidden && cloudflaredInstall.hidden;
|
|
13579
|
+
remotePair.disabled = !remoteReady || remoteProviderBusy || remotePairingBusy;
|
|
13580
|
+
remoteCopyLink.disabled = !remoteReady || remoteProviderBusy || remotePairingBusy;
|
|
12273
13581
|
remoteDevices.disabled = !remoteReady || remoteProviderBusy;
|
|
12274
13582
|
remotePair.hidden = iroh;
|
|
12275
13583
|
remoteCopyLink.hidden = iroh;
|
|
@@ -12278,24 +13586,49 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12278
13586
|
remotePairLink.hidden = true;
|
|
12279
13587
|
}
|
|
12280
13588
|
irohPair.disabled = !remoteReady || !iroh || remoteProviderBusy;
|
|
13589
|
+
irohPair.hidden = !iroh;
|
|
13590
|
+
irohCopyDescriptor.hidden = !iroh;
|
|
13591
|
+
if (iroh && irohInstalled && activeIrohQrSvg === "") showIrohQrPlaceholder();
|
|
13592
|
+
else if (!iroh || !irohInstalled) irohQr.hidden = true;
|
|
12281
13593
|
if ((!iroh || !remoteReady) && activeIrohDescriptor !== "") {
|
|
12282
13594
|
activeIrohDescriptor = "";
|
|
13595
|
+
activeIrohPairExpiresAt = 0;
|
|
12283
13596
|
activeIrohQrSvg = "";
|
|
12284
13597
|
showQr("", irohQr);
|
|
12285
13598
|
setIrohQrZoomOpen(false);
|
|
12286
|
-
irohCopyDescriptor.hidden = true;
|
|
12287
13599
|
irohPairStatus.textContent = "";
|
|
12288
13600
|
}
|
|
13601
|
+
irohCopyDescriptor.disabled = !iroh || !remoteReady || remoteProviderBusy || activeIrohDescriptor === "" || activeIrohPairExpiresAt <= Date.now();
|
|
12289
13602
|
if (origin) {
|
|
12290
13603
|
remoteReconnect.disabled = remoteProviderBusy || remoteReconnectBusy;
|
|
12291
13604
|
remoteReset.disabled = remoteProviderBusy;
|
|
12292
13605
|
} else remoteReset.disabled = false;
|
|
12293
13606
|
if (!remoteReady) {
|
|
12294
|
-
|
|
13607
|
+
remotePairingGeneration += 1;
|
|
12295
13608
|
remotePairLink.hidden = true;
|
|
12296
13609
|
activeRemotePairUrl = "";
|
|
12297
13610
|
activeRemotePairExpiresAt = 0;
|
|
13611
|
+
pairingValidity.hidden = true;
|
|
13612
|
+
if (iroh) remoteQr.hidden = true;
|
|
13613
|
+
else showRemoteQrPlaceholder(t("remoteQrPlaceholderIdle"));
|
|
13614
|
+
} else if (!iroh && activeRemotePairExpiresAt > 0 && Date.now() >= activeRemotePairExpiresAt) {
|
|
13615
|
+
activeRemotePairUrl = "";
|
|
13616
|
+
remotePairLink.hidden = true;
|
|
13617
|
+
remotePairLink.textContent = "";
|
|
13618
|
+
showRemoteQrPlaceholder(t("pairingExpired"));
|
|
13619
|
+
pairingValidity.hidden = false;
|
|
13620
|
+
pairingValidity.textContent = t("pairingExpired");
|
|
13621
|
+
} else if (!iroh && activeRemotePairUrl !== "") {
|
|
13622
|
+
pairingValidity.hidden = false;
|
|
13623
|
+
pairingValidity.textContent = t("pairingExpires", { time: formatTime(activeRemotePairExpiresAt) });
|
|
13624
|
+
} else {
|
|
13625
|
+
pairingValidity.hidden = true;
|
|
13626
|
+
if (!iroh) showQr("", remoteQr);
|
|
12298
13627
|
}
|
|
13628
|
+
cpolarDetails.hidden = !cpolar;
|
|
13629
|
+
cloudflaredDetails.hidden = !cloudflared;
|
|
13630
|
+
frpDetails.hidden = !frp;
|
|
13631
|
+
irohDetails.hidden = !iroh;
|
|
12299
13632
|
if (!needsFunnelSetup) remoteSetupPending = false;
|
|
12300
13633
|
};
|
|
12301
13634
|
frpMode.addEventListener("change", () => {
|
|
@@ -12405,6 +13738,26 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12405
13738
|
loadRemote();
|
|
12406
13739
|
});
|
|
12407
13740
|
});
|
|
13741
|
+
irohPurge.addEventListener("click", () => {
|
|
13742
|
+
if (remoteProviderBusy) return;
|
|
13743
|
+
if (!window.confirm(t("irohPurgeConfirm"))) return;
|
|
13744
|
+
remoteProviderBusy = true;
|
|
13745
|
+
irohPurge.disabled = true;
|
|
13746
|
+
irohComponentStatus.textContent = t("purgingIroh");
|
|
13747
|
+
remoteStatus.textContent = t("purgingIroh");
|
|
13748
|
+
controlRequestJson("/api/mobile-access/remote/iroh/component/purge", {
|
|
13749
|
+
method: "POST",
|
|
13750
|
+
body: JSON.stringify({ confirm: true })
|
|
13751
|
+
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderRemote, (error) => {
|
|
13752
|
+
remoteFailureText = remoteFailureTextFor(error, "purgeFailed");
|
|
13753
|
+
remoteStatus.textContent = remoteFailureText;
|
|
13754
|
+
irohComponentStatus.textContent = remoteFailureText;
|
|
13755
|
+
}).finally(() => {
|
|
13756
|
+
remoteProviderBusy = false;
|
|
13757
|
+
irohPurge.disabled = false;
|
|
13758
|
+
loadRemote();
|
|
13759
|
+
});
|
|
13760
|
+
});
|
|
12408
13761
|
const openIrohQrZoom = () => {
|
|
12409
13762
|
if (activeIrohQrSvg === "" || irohQr.hidden) return;
|
|
12410
13763
|
irohQrZoomImage.src = `data:image/svg+xml;base64,${btoa(activeIrohQrSvg)}`;
|
|
@@ -12430,15 +13783,22 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12430
13783
|
if (event.key !== "Escape") return;
|
|
12431
13784
|
closeIrohQrZoom();
|
|
12432
13785
|
});
|
|
12433
|
-
const openIrohPairing = () =>
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
13786
|
+
const openIrohPairing = async () => {
|
|
13787
|
+
const generation = remotePairingGeneration;
|
|
13788
|
+
activeIrohDescriptor = "";
|
|
13789
|
+
activeIrohPairExpiresAt = 0;
|
|
13790
|
+
irohCopyDescriptor.disabled = true;
|
|
13791
|
+
const data = await controlRequestJson("/api/mobile-access/remote/iroh/pairing/begin", {
|
|
13792
|
+
method: "POST",
|
|
13793
|
+
body: "{}"
|
|
13794
|
+
});
|
|
13795
|
+
if (lifecycle.signal.aborted || remoteProvider !== "iroh" || !remoteReady || generation !== remotePairingGeneration) return;
|
|
12437
13796
|
const record = data;
|
|
12438
13797
|
const qrSvg = typeof record.qrSvg === "string" ? record.qrSvg : "";
|
|
12439
13798
|
const code = typeof record.irohCode === "string" ? record.irohCode : "";
|
|
13799
|
+
const expiresAt = typeof record.expiresAt === "number" && Number.isFinite(record.expiresAt) ? record.expiresAt : 0;
|
|
12440
13800
|
showQr(qrSvg, irohQr);
|
|
12441
|
-
if (qrSvg === "" || code === "") {
|
|
13801
|
+
if (qrSvg === "" || code === "" || expiresAt <= Date.now()) {
|
|
12442
13802
|
activeIrohQrSvg = "";
|
|
12443
13803
|
irohPairStatus.textContent = t("keyGenerationFailed");
|
|
12444
13804
|
return;
|
|
@@ -12459,12 +13819,14 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12459
13819
|
const descriptor = {};
|
|
12460
13820
|
for (const field of fields) descriptor[field] = record[field];
|
|
12461
13821
|
activeIrohDescriptor = JSON.stringify(descriptor);
|
|
12462
|
-
|
|
13822
|
+
activeIrohPairExpiresAt = expiresAt;
|
|
12463
13823
|
irohPairStatus.textContent = t("irohPairingReady", { code });
|
|
12464
|
-
}
|
|
13824
|
+
};
|
|
12465
13825
|
irohPair.addEventListener("click", () => {
|
|
12466
|
-
if (remoteProviderBusy) return;
|
|
13826
|
+
if (remoteProviderBusy || !remoteReady || remoteProvider !== "iroh") return;
|
|
12467
13827
|
remoteProviderBusy = true;
|
|
13828
|
+
remoteSnapshotEpoch += 1;
|
|
13829
|
+
renderRemote(latestRemoteState);
|
|
12468
13830
|
irohPair.disabled = true;
|
|
12469
13831
|
irohPairStatus.textContent = t("loadingRemoteStatus");
|
|
12470
13832
|
openIrohPairing().catch((error) => {
|
|
@@ -12476,9 +13838,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12476
13838
|
});
|
|
12477
13839
|
});
|
|
12478
13840
|
irohCopyDescriptor.addEventListener("click", () => {
|
|
12479
|
-
if (activeIrohDescriptor === "") return;
|
|
13841
|
+
if (remoteProviderBusy || !remoteReady || remoteProvider !== "iroh" || activeIrohDescriptor === "" || activeIrohPairExpiresAt <= Date.now()) return;
|
|
12480
13842
|
navigator.clipboard.writeText(activeIrohDescriptor).then(() => {
|
|
12481
|
-
irohPairStatus.textContent = t("
|
|
13843
|
+
irohPairStatus.textContent = t("irohLinkCopied");
|
|
12482
13844
|
}, () => {
|
|
12483
13845
|
irohPairStatus.textContent = t("remoteQrReady");
|
|
12484
13846
|
});
|
|
@@ -12519,6 +13881,19 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12519
13881
|
loadRemote();
|
|
12520
13882
|
});
|
|
12521
13883
|
});
|
|
13884
|
+
const syncCloudflaredForm = () => {
|
|
13885
|
+
const named = (cloudflaredModeDraft ?? cloudflaredTunnelMode) === "named";
|
|
13886
|
+
const locked = named && cloudflaredTunnelConfigured && cloudflaredTunnelMode === "named" && !cloudflaredConfigEditing;
|
|
13887
|
+
cloudflaredHostname.disabled = locked || remoteProviderBusy;
|
|
13888
|
+
cloudflaredPort.disabled = locked || remoteProviderBusy;
|
|
13889
|
+
cloudflaredToken.disabled = locked || remoteProviderBusy;
|
|
13890
|
+
cloudflaredEditButton.disabled = remoteProviderBusy;
|
|
13891
|
+
cloudflaredTunnelForget.disabled = remoteProviderBusy;
|
|
13892
|
+
cloudflaredTunnelToggle.hidden = !named;
|
|
13893
|
+
cloudflaredEditButton.hidden = !locked;
|
|
13894
|
+
cloudflaredTunnelSave.hidden = !named || locked;
|
|
13895
|
+
cloudflaredTunnelForget.hidden = !named || cloudflaredTunnelMode !== "named" || cloudflaredConfigEditing;
|
|
13896
|
+
};
|
|
12522
13897
|
const showCloudflaredMode = (mode) => {
|
|
12523
13898
|
cloudflaredModeDraft = mode;
|
|
12524
13899
|
cloudflaredModeQuick.classList.toggle("is-active", mode === "quick");
|
|
@@ -12527,14 +13902,60 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12527
13902
|
cloudflaredModeNamed.setAttribute("aria-pressed", String(mode === "named"));
|
|
12528
13903
|
cloudflaredModeHint.textContent = mode === "named" ? t("cloudflaredTunnelNamedHint") : t("cloudflaredTunnelQuickHint");
|
|
12529
13904
|
cloudflaredNote.textContent = mode === "named" ? t("cloudflaredNamedNote") : t("cloudflaredQuickNote");
|
|
12530
|
-
cloudflaredTunnel.hidden =
|
|
13905
|
+
cloudflaredTunnel.hidden = true;
|
|
13906
|
+
cloudflaredTunnelToggle.textContent = t("cloudflaredShowConfig") + " ▾";
|
|
12531
13907
|
cloudflaredTunnelSave.textContent = cloudflaredSaveLabel();
|
|
13908
|
+
syncCloudflaredForm();
|
|
13909
|
+
if (mode === "named" && cloudflaredTunnelMode !== "named") {
|
|
13910
|
+
remoteToggle.disabled = true;
|
|
13911
|
+
remoteStatus.textContent = t("cloudflaredEnableNeedsConfig");
|
|
13912
|
+
if (remoteStatus.style) remoteStatus.style.display = "";
|
|
13913
|
+
}
|
|
13914
|
+
};
|
|
13915
|
+
cloudflaredTunnelToggle.addEventListener("click", () => {
|
|
13916
|
+
const open = cloudflaredTunnel.hidden;
|
|
13917
|
+
cloudflaredTunnel.hidden = !open;
|
|
13918
|
+
cloudflaredTunnelToggle.textContent = open ? t("cloudflaredHideConfig") + " ▴" : t("cloudflaredShowConfig") + " ▾";
|
|
13919
|
+
});
|
|
13920
|
+
cloudflaredEditButton.addEventListener("click", () => {
|
|
13921
|
+
cloudflaredConfigEditing = true;
|
|
13922
|
+
syncCloudflaredForm();
|
|
13923
|
+
cloudflaredTunnel.hidden = false;
|
|
13924
|
+
cloudflaredTunnelToggle.textContent = t("cloudflaredHideConfig") + " ▴";
|
|
13925
|
+
});
|
|
13926
|
+
const switchCloudflaredMode = (mode) => {
|
|
13927
|
+
if (remoteProviderBusy || (cloudflaredModeDraft ?? cloudflaredTunnelMode) === mode) return;
|
|
13928
|
+
const activeState = typeof latestRemoteState.state === "string" ? latestRemoteState.state : "off";
|
|
13929
|
+
if (remoteRunning || activeState !== "off") {
|
|
13930
|
+
if (!window.confirm(t("cloudflaredSwitchModeDisconnect"))) return;
|
|
13931
|
+
remoteProviderBusy = true;
|
|
13932
|
+
remoteSnapshotEpoch += 1;
|
|
13933
|
+
renderRemote(latestRemoteState);
|
|
13934
|
+
controlRequestJson("/api/mobile-access/remote/control", {
|
|
13935
|
+
method: "POST",
|
|
13936
|
+
body: JSON.stringify({
|
|
13937
|
+
running: false,
|
|
13938
|
+
provider: "cloudflared"
|
|
13939
|
+
})
|
|
13940
|
+
}).then((data) => {
|
|
13941
|
+
renderRemote(data);
|
|
13942
|
+
showCloudflaredMode(mode);
|
|
13943
|
+
}, (error) => {
|
|
13944
|
+
remoteFailureText = remoteFailureTextFor(error, "requestFailed");
|
|
13945
|
+
remoteStatus.textContent = remoteFailureText;
|
|
13946
|
+
}).finally(() => {
|
|
13947
|
+
remoteProviderBusy = false;
|
|
13948
|
+
loadRemote();
|
|
13949
|
+
});
|
|
13950
|
+
return;
|
|
13951
|
+
}
|
|
13952
|
+
showCloudflaredMode(mode);
|
|
12532
13953
|
};
|
|
12533
13954
|
cloudflaredModeQuick.addEventListener("click", () => {
|
|
12534
|
-
|
|
13955
|
+
switchCloudflaredMode("quick");
|
|
12535
13956
|
});
|
|
12536
13957
|
cloudflaredModeNamed.addEventListener("click", () => {
|
|
12537
|
-
|
|
13958
|
+
switchCloudflaredMode("named");
|
|
12538
13959
|
});
|
|
12539
13960
|
cloudflaredHostname.addEventListener("input", () => {
|
|
12540
13961
|
cloudflaredHostnameDirty = true;
|
|
@@ -12544,6 +13965,11 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12544
13965
|
});
|
|
12545
13966
|
cloudflaredTunnelSave.addEventListener("click", () => {
|
|
12546
13967
|
if (remoteProviderBusy) return;
|
|
13968
|
+
const report = (message) => {
|
|
13969
|
+
remoteStatus.textContent = message;
|
|
13970
|
+
cloudflaredTunnelFeedback.hidden = false;
|
|
13971
|
+
cloudflaredTunnelFeedback.textContent = message;
|
|
13972
|
+
};
|
|
12547
13973
|
const mode = cloudflaredModeDraft ?? cloudflaredTunnelMode;
|
|
12548
13974
|
const hostname = String(cloudflaredHostname.value ?? "").trim();
|
|
12549
13975
|
const portValue = String(cloudflaredPort.value ?? "").trim();
|
|
@@ -12551,48 +13977,57 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12551
13977
|
const token = String(cloudflaredToken.value ?? "");
|
|
12552
13978
|
if (mode === "named") {
|
|
12553
13979
|
if (hostname === "") {
|
|
12554
|
-
|
|
13980
|
+
report(t("cloudflaredTunnelHostnameInvalid"));
|
|
12555
13981
|
return;
|
|
12556
13982
|
}
|
|
12557
13983
|
if (portValue === "" || !Number.isSafeInteger(port) || port < 1024 || port > 65535) {
|
|
12558
|
-
|
|
13984
|
+
report(t("cloudflaredTunnelPortInvalid"));
|
|
12559
13985
|
return;
|
|
12560
13986
|
}
|
|
12561
13987
|
if (port === 3443) {
|
|
12562
|
-
|
|
13988
|
+
report(t("cloudflaredTunnelPortReserved"));
|
|
12563
13989
|
return;
|
|
12564
13990
|
}
|
|
12565
13991
|
if (token === "" && !cloudflaredTunnelConfigured) {
|
|
12566
|
-
|
|
13992
|
+
report(t("cloudflaredTunnelConfigMissing"));
|
|
12567
13993
|
return;
|
|
12568
13994
|
}
|
|
12569
13995
|
if (token !== "" && (token.length < 32 || !/^[A-Za-z0-9_=+/-]+$/u.test(token))) {
|
|
12570
|
-
|
|
13996
|
+
report(t("cloudflaredTunnelTokenInvalid"));
|
|
12571
13997
|
return;
|
|
12572
13998
|
}
|
|
12573
13999
|
}
|
|
12574
14000
|
remoteProviderBusy = true;
|
|
14001
|
+
remoteSnapshotEpoch += 1;
|
|
14002
|
+
renderRemote(latestRemoteState);
|
|
12575
14003
|
cloudflaredTunnelSave.disabled = true;
|
|
12576
14004
|
cloudflaredTunnelSave.textContent = t("saving");
|
|
12577
|
-
|
|
14005
|
+
report(mode === "named" ? t("cloudflaredTunnelSaving") : t("switchingCloudflared"));
|
|
12578
14006
|
controlRequestJson("/api/mobile-access/remote/cloudflared/tunnel", {
|
|
12579
14007
|
method: "POST",
|
|
12580
14008
|
body: JSON.stringify(mode === "named" ? {
|
|
12581
14009
|
mode,
|
|
12582
14010
|
hostname,
|
|
12583
14011
|
port,
|
|
12584
|
-
token
|
|
12585
|
-
|
|
14012
|
+
token,
|
|
14013
|
+
connect: true
|
|
14014
|
+
} : {
|
|
14015
|
+
mode: "quick",
|
|
14016
|
+
connect: true
|
|
14017
|
+
})
|
|
12586
14018
|
}).then((data) => {
|
|
12587
14019
|
cloudflaredToken.value = "";
|
|
12588
14020
|
cloudflaredModeDraft = null;
|
|
14021
|
+
cloudflaredConfigEditing = false;
|
|
12589
14022
|
cloudflaredHostnameDirty = false;
|
|
12590
14023
|
cloudflaredPortDirty = false;
|
|
12591
14024
|
remoteFailureText = "";
|
|
14025
|
+
cloudflaredTunnelFeedback.hidden = false;
|
|
14026
|
+
cloudflaredTunnelFeedback.textContent = t("cloudflaredTunnelSaved");
|
|
12592
14027
|
renderRemote(data);
|
|
12593
14028
|
}, (error) => {
|
|
12594
14029
|
remoteFailureText = remoteFailureTextFor(error, "requestFailed");
|
|
12595
|
-
|
|
14030
|
+
report(remoteFailureText);
|
|
12596
14031
|
}).finally(() => {
|
|
12597
14032
|
remoteProviderBusy = false;
|
|
12598
14033
|
cloudflaredTunnelSave.disabled = false;
|
|
@@ -13457,14 +14892,35 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
13457
14892
|
});
|
|
13458
14893
|
});
|
|
13459
14894
|
remoteToggle.addEventListener("click", () => {
|
|
13460
|
-
|
|
13461
|
-
|
|
14895
|
+
if (remoteProviderBusy) return;
|
|
14896
|
+
const nextRunning = !remoteRunning;
|
|
14897
|
+
const provider = remoteProvider;
|
|
14898
|
+
remoteProviderBusy = true;
|
|
14899
|
+
remoteSnapshotEpoch += 1;
|
|
14900
|
+
renderRemote(latestRemoteState);
|
|
14901
|
+
(nextRunning && provider === "cloudflared" && cloudflaredModeDraft !== null && cloudflaredModeDraft !== cloudflaredTunnelMode ? controlRequestJson("/api/mobile-access/remote/cloudflared/tunnel", {
|
|
13462
14902
|
method: "POST",
|
|
13463
|
-
body: JSON.stringify({
|
|
13464
|
-
|
|
14903
|
+
body: JSON.stringify({
|
|
14904
|
+
mode: cloudflaredModeDraft,
|
|
14905
|
+
connect: true
|
|
14906
|
+
})
|
|
14907
|
+
}) : controlRequestJson("/api/mobile-access/remote/control", {
|
|
14908
|
+
method: "POST",
|
|
14909
|
+
body: JSON.stringify({
|
|
14910
|
+
running: nextRunning,
|
|
14911
|
+
provider
|
|
14912
|
+
})
|
|
14913
|
+
})).then(renderRemote, (error) => {
|
|
13465
14914
|
remoteFailureText = remoteFailureTextFor(error, "requestFailed");
|
|
13466
14915
|
remoteStatus.textContent = remoteFailureText;
|
|
13467
|
-
|
|
14916
|
+
if (remoteProvider === "cloudflared") {
|
|
14917
|
+
cloudflaredTunnelFeedback.hidden = false;
|
|
14918
|
+
cloudflaredTunnelFeedback.textContent = remoteFailureText;
|
|
14919
|
+
}
|
|
14920
|
+
}).finally(() => {
|
|
14921
|
+
remoteProviderBusy = false;
|
|
14922
|
+
loadRemote();
|
|
14923
|
+
});
|
|
13468
14924
|
});
|
|
13469
14925
|
remoteLogin.addEventListener("click", () => {
|
|
13470
14926
|
if (remoteLoginUrl !== "") window.open(remoteLoginUrl, "_blank", "noopener,noreferrer");
|
|
@@ -13516,40 +14972,64 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
13516
14972
|
remoteStatus.textContent = t("remoteQrReady");
|
|
13517
14973
|
}
|
|
13518
14974
|
};
|
|
13519
|
-
const openRemotePairing = () =>
|
|
13520
|
-
|
|
13521
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
|
|
13528
|
-
|
|
13529
|
-
|
|
13530
|
-
|
|
13531
|
-
|
|
13532
|
-
|
|
13533
|
-
|
|
13534
|
-
|
|
13535
|
-
|
|
13536
|
-
|
|
14975
|
+
const openRemotePairing = () => {
|
|
14976
|
+
const provider = remoteProvider;
|
|
14977
|
+
const origin = latestRemoteState.origin;
|
|
14978
|
+
const generation = remotePairingGeneration;
|
|
14979
|
+
activeRemotePairUrl = "";
|
|
14980
|
+
activeRemotePairExpiresAt = 0;
|
|
14981
|
+
pairingValidity.hidden = true;
|
|
14982
|
+
remotePairLink.hidden = true;
|
|
14983
|
+
remotePairLink.textContent = "";
|
|
14984
|
+
showQr("", remoteQr);
|
|
14985
|
+
return controlRequestJson("/api/mobile-access/remote/pairing/open", {
|
|
14986
|
+
method: "POST",
|
|
14987
|
+
body: "{}"
|
|
14988
|
+
}).then(async (data) => {
|
|
14989
|
+
if (lifecycle.signal.aborted || !remoteReady || provider !== remoteProvider || origin !== latestRemoteState.origin || generation !== remotePairingGeneration) return;
|
|
14990
|
+
const pairUrl = typeof data.pairUrl === "string" ? data.pairUrl : "";
|
|
14991
|
+
const expiresAt = typeof data.expiresAt === "number" && Number.isFinite(data.expiresAt) ? data.expiresAt : 0;
|
|
14992
|
+
if (pairUrl === "" || expiresAt <= Date.now()) {
|
|
14993
|
+
remoteStatus.textContent = t("keyGenerationFailed");
|
|
14994
|
+
return;
|
|
14995
|
+
}
|
|
14996
|
+
showQr(typeof data.qrSvg === "string" ? data.qrSvg : "", remoteQr);
|
|
14997
|
+
activeRemotePairUrl = pairUrl;
|
|
14998
|
+
activeRemotePairExpiresAt = expiresAt;
|
|
14999
|
+
pairingValidity.hidden = false;
|
|
15000
|
+
pairingValidity.textContent = t("pairingExpires", { time: formatTime(expiresAt) });
|
|
15001
|
+
remotePairLink.textContent = pairUrl;
|
|
15002
|
+
remotePairLink.classList.add("is-key");
|
|
15003
|
+
remotePairLink.hidden = false;
|
|
15004
|
+
await copyRemotePairUrl();
|
|
15005
|
+
});
|
|
15006
|
+
};
|
|
13537
15007
|
remotePair.addEventListener("click", () => {
|
|
15008
|
+
if (!remoteReady || remoteProviderBusy || remotePairingBusy) return;
|
|
15009
|
+
remotePairingBusy = true;
|
|
13538
15010
|
remotePair.disabled = true;
|
|
15011
|
+
remoteCopyLink.disabled = true;
|
|
13539
15012
|
openRemotePairing().catch((error) => {
|
|
13540
15013
|
remoteFailureText = remoteFailureTextFor(error, "requestFailed");
|
|
13541
15014
|
remoteStatus.textContent = remoteFailureText;
|
|
13542
15015
|
}).finally(() => {
|
|
13543
|
-
|
|
15016
|
+
remotePairingBusy = false;
|
|
15017
|
+
remotePair.disabled = !remoteReady || remoteProviderBusy;
|
|
15018
|
+
remoteCopyLink.disabled = !remoteReady || remoteProviderBusy;
|
|
13544
15019
|
});
|
|
13545
15020
|
});
|
|
13546
15021
|
remoteCopyLink.addEventListener("click", () => {
|
|
15022
|
+
if (!remoteReady || remoteProviderBusy || remotePairingBusy) return;
|
|
15023
|
+
remotePairingBusy = true;
|
|
15024
|
+
remotePair.disabled = true;
|
|
13547
15025
|
remoteCopyLink.disabled = true;
|
|
13548
15026
|
(activeRemotePairUrl !== "" && activeRemotePairExpiresAt > Date.now() ? copyRemotePairUrl() : openRemotePairing()).catch((error) => {
|
|
13549
15027
|
remoteFailureText = remoteFailureTextFor(error, "requestFailed");
|
|
13550
15028
|
remoteStatus.textContent = remoteFailureText;
|
|
13551
15029
|
}).finally(() => {
|
|
13552
|
-
|
|
15030
|
+
remotePairingBusy = false;
|
|
15031
|
+
remotePair.disabled = !remoteReady || remoteProviderBusy;
|
|
15032
|
+
remoteCopyLink.disabled = !remoteReady || remoteProviderBusy;
|
|
13553
15033
|
});
|
|
13554
15034
|
});
|
|
13555
15035
|
const renderRemoteDevices = (data) => {
|
|
@@ -13588,6 +15068,10 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
13588
15068
|
controlRequestJson("/api/mobile-access/remote/devices").then(renderRemoteDevices, (error) => {
|
|
13589
15069
|
remoteFailureText = remoteFailureTextFor(error, "requestFailed");
|
|
13590
15070
|
remoteStatus.textContent = remoteFailureText;
|
|
15071
|
+
remoteDevicePanel.replaceChildren();
|
|
15072
|
+
const note = element("p", "dsh-mobile-control__device-empty");
|
|
15073
|
+
note.textContent = remoteRunning ? t("noRemoteDevices") : t("remoteOff");
|
|
15074
|
+
remoteDevicePanel.append(note);
|
|
13591
15075
|
});
|
|
13592
15076
|
};
|
|
13593
15077
|
remoteDevices.addEventListener("click", () => {
|
|
@@ -13746,6 +15230,14 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
13746
15230
|
selectView("diagnostics");
|
|
13747
15231
|
loadDiagnostics();
|
|
13748
15232
|
});
|
|
15233
|
+
devicesEntry.addEventListener("click", () => {
|
|
15234
|
+
if (!devicesView.hidden) {
|
|
15235
|
+
selectView(previousAccessView);
|
|
15236
|
+
return;
|
|
15237
|
+
}
|
|
15238
|
+
selectView("devices");
|
|
15239
|
+
showDevicesGroup("lan");
|
|
15240
|
+
});
|
|
13749
15241
|
diagnosticsRun.addEventListener("click", loadDiagnostics);
|
|
13750
15242
|
diagnosticsRun.addEventListener("click", () => {
|
|
13751
15243
|
extensionRecovery.refresh();
|
|
@@ -13806,11 +15298,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
13806
15298
|
});
|
|
13807
15299
|
});
|
|
13808
15300
|
pair.addEventListener("click", () => {
|
|
13809
|
-
pair.disabled = true;
|
|
13810
15301
|
openPairing("key");
|
|
13811
15302
|
});
|
|
13812
15303
|
linkPair.addEventListener("click", () => {
|
|
13813
|
-
linkPair.disabled = true;
|
|
13814
15304
|
openPairing("link");
|
|
13815
15305
|
});
|
|
13816
15306
|
close.addEventListener("click", () => {
|
|
@@ -13818,12 +15308,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
13818
15308
|
});
|
|
13819
15309
|
const dismiss = (event) => {
|
|
13820
15310
|
if (panel.hidden || !(event.target instanceof Node)) return;
|
|
13821
|
-
if (!
|
|
13822
|
-
providerInfoPinned = false;
|
|
13823
|
-
providerInfoHovered = false;
|
|
13824
|
-
syncProviderInfo();
|
|
13825
|
-
}
|
|
13826
|
-
if (!panel.contains(event.target) && !document.querySelector(".dsh-mobile-control__trigger")?.contains(event.target)) setOpen(false);
|
|
15311
|
+
if (!panel.contains(event.target) && !irohQrZoom.contains(event.target) && !document.querySelector(".dsh-mobile-control__trigger")?.contains(event.target)) setOpen(false);
|
|
13827
15312
|
};
|
|
13828
15313
|
document.addEventListener("pointerdown", dismiss);
|
|
13829
15314
|
controlRequestJson("/api/mobile-access/release").then(renderRelease, () => {});
|
|
@@ -13832,7 +15317,10 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
13832
15317
|
});
|
|
13833
15318
|
loadRemote();
|
|
13834
15319
|
const remotePoll = window.setInterval(() => {
|
|
13835
|
-
if (!panel.hidden && !remoteView.hidden)
|
|
15320
|
+
if (!panel.hidden && !remoteView.hidden) {
|
|
15321
|
+
renderLanPairingExpiry();
|
|
15322
|
+
loadRemote();
|
|
15323
|
+
}
|
|
13836
15324
|
}, 1500);
|
|
13837
15325
|
const pollWsBlocked = () => {
|
|
13838
15326
|
controlRequestJson("/api/mobile-access/remote/websocket-paths/blocked").then((blocked) => {
|
|
@@ -15095,9 +16583,31 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15095
16583
|
/** Theme-aware desktop Mobile Access panel styles. */
|
|
15096
16584
|
const CONTROL_STYLES = `
|
|
15097
16585
|
.dsh-mobile-control{position:fixed;z-index:1000;left:16px;bottom:112px;font:14px/1.45 system-ui;color:var(--dsw-alias-label-primary,#16181d)}
|
|
15098
|
-
.dsh-mobile-control__panel{box-sizing:border-box;width:min(
|
|
16586
|
+
.dsh-mobile-control__panel{box-sizing:border-box;width:min(704px,calc(100vw - 32px));max-height:calc(100vh - 140px);overflow-y:auto;padding:16px;border:0;border-radius:16px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 12px 34px rgb(15 23 42 / 13%)}.dsh-mobile-control__remote-columns{display:grid;grid-template-columns:minmax(230px,300px) minmax(0,1fr);gap:14px;align-items:start;max-height:calc(100vh - 330px)}
|
|
16587
|
+
/* Floating panel with dense, borderless content. */
|
|
16588
|
+
.dsh-mobile-control{inset:auto;left:16px;bottom:112px;pointer-events:none}
|
|
16589
|
+
.dsh-mobile-control__panel{position:relative;inset:auto;width:min(704px,calc(100vw - 32px));height:auto;max-height:calc(100vh - 140px);border:0;border-radius:16px;box-shadow:0 12px 34px rgb(15 23 42 / 13%);display:flex;flex-direction:column;overflow:hidden;pointer-events:auto;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__panel[hidden]{display:none}
|
|
16590
|
+
.dsh-mobile-control__header{flex:none;margin:0;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#e1e5eb)}
|
|
16591
|
+
.dsh-mobile-control__app-downloads{flex:none;padding:8px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#e1e5eb);margin:0}
|
|
16592
|
+
.dsh-mobile-control__view.is-remote{flex:1;min-height:0;display:flex;flex-direction:column}
|
|
16593
|
+
.dsh-mobile-control__remote-columns{flex:1;min-height:0;display:grid;grid-template-columns:260px minmax(0,1fr);gap:0;max-height:none;align-items:stretch}
|
|
16594
|
+
.dsh-mobile-control__remote-columns>.dsh-mobile-control__provider-section{overflow-y:auto;border-right:1px solid var(--dsw-alias-border-l2,#e1e5eb);background:var(--dsw-alias-bg-layer-1,#f7f8fa);padding:10px 8px}
|
|
16595
|
+
.dsh-mobile-control__channel-pane{overflow-y:auto;padding:14px 20px;min-width:0;scrollbar-gutter:stable}
|
|
16596
|
+
.dsh-mobile-control__provider{position:relative;border:0;border-radius:8px;padding:8px 10px;min-height:0;background:transparent;box-shadow:none}
|
|
16597
|
+
.dsh-mobile-control__provider:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f5f9)}
|
|
16598
|
+
.dsh-mobile-control__provider.is-selected{background:var(--dsw-alias-interactive-bg-active,#f5f8ff);box-shadow:none}
|
|
16599
|
+
.dsh-mobile-control__provider.is-selected::before{content:"";position:absolute;left:0;top:8px;bottom:8px;width:2px;border-radius:2px;background:var(--dsw-alias-label-primary-bluish,#2563eb)}
|
|
16600
|
+
.dsh-mobile-control__remote-workspace section{border:0;border-radius:0;padding:12px 0;background:transparent;box-shadow:none;border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}
|
|
16601
|
+
.dsh-mobile-control__remote-workspace section:first-child{border-top:0}
|
|
16602
|
+
.dsh-mobile-control__remote-workspace .dsh-mobile-control__details{border:0;border-radius:0;background:transparent;box-shadow:none}
|
|
16603
|
+
.dsh-mobile-control__btn,.dsh-mobile-control__primary,.dsh-mobile-control__secondary{min-height:32px;padding:6px 12px;border-radius:8px;font-size:12px}
|
|
16604
|
+
.dsh-mobile-control__self-hosted{border:0;border-radius:0;background:transparent;box-shadow:none}
|
|
16605
|
+
.dsh-mobile-control__self-hosted-summary{border-bottom:1px solid var(--dsw-alias-border-l2,#e1e5eb)}
|
|
16606
|
+
.dsh-mobile-control__self-hosted-body{padding:0}
|
|
16607
|
+
@media (max-width:860px){.dsh-mobile-control__remote-columns{grid-template-columns:1fr}.dsh-mobile-control__remote-columns>.dsh-mobile-control__provider-section{border-right:0;border-bottom:1px solid var(--dsw-alias-border-l2,#e1e5eb);max-height:40vh}}
|
|
16608
|
+
.dsh-mobile-control__channel-pane{min-width:0;overflow-y:auto;padding-right:4px;max-height:100%}.dsh-mobile-control__channel-pane>.dsh-mobile-control__view{display:block}.dsh-mobile-control__provider.is-lan{border-color:var(--dsw-alias-label-primary-bluish,#2563eb);background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-bg-layer-1,#f5f8ff))}.dsh-mobile-control__remote-columns>.dsh-mobile-control__provider-section{overflow-y:auto;padding-right:4px}.dsh-mobile-control__remote-columns>.dsh-mobile-control__remote-workspace{overflow-y:auto;padding-right:4px}.dsh-mobile-control__remote-columns>.dsh-mobile-control__provider-section{margin:0}@media (max-width:760px){.dsh-mobile-control__remote-columns{grid-template-columns:1fr}}
|
|
15099
16609
|
.dsh-mobile-control__header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}.dsh-mobile-control__panel h2{margin:0;font-size:17px;line-height:24px}.dsh-mobile-control__header-actions{display:flex;align-items:center;gap:2px}.dsh-mobile-control__update-plugin,.dsh-mobile-control__diagnostic-entry,.dsh-mobile-control__close{display:inline-flex;align-items:center;justify-content:center;min-width:44px;height:44px;padding:0;border:0;border-radius:10px;background:transparent;color:inherit;cursor:pointer}.dsh-mobile-control__update-plugin,.dsh-mobile-control__diagnostic-entry{padding:0 8px;color:var(--dsw-alias-label-primary-bluish,#2563eb);font:650 12px/1 system-ui;white-space:nowrap}.dsh-mobile-control__update-plugin[hidden]{display:none}.dsh-mobile-control__update-plugin:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__close{font-size:24px;line-height:1}.dsh-mobile-control__update-plugin:hover:not(:disabled),.dsh-mobile-control__diagnostic-entry:hover,.dsh-mobile-control__close:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}
|
|
15100
|
-
.dsh-mobile-control__release-notice{margin:-2px 0 10px;padding:8px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#eff6ff);color:var(--dsw-alias-label-primary,#1d4ed8);font-size:11px;line-height:1.45}.dsh-mobile-control__release-notice.is-error{color:var(--dsw-alias-label-primary,#dc2626);border-left:3px solid var(--dsw-alias-state-error-primary,#dc2626)}.dsh-mobile-control__release-notice[hidden]{display:none}.dsh-mobile-control__update-card{margin:0 0 10px;padding:11px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__update-card[hidden]{display:none}.dsh-mobile-control__update-card-title{margin:0 0 7px;color:var(--dsw-alias-label-primary,#16181d);font:650 13px/1.4 system-ui}.dsh-mobile-control__update-notes{display:block;box-sizing:border-box;max-height:150px;overflow-y:auto;margin:0 0 8px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#e1e5eb);border-radius:9px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#475569);white-space:pre-wrap;word-break:break-word;font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.dsh-mobile-control__update-notes[hidden]{display:none}.dsh-mobile-control__update-notice{margin:0 0 9px;padding:7px 9px;border-radius:8px;background:var(--dsw-alias-state-warn-tertiary,#fff4d6);color:var(--dsw-alias-state-warn-label,#92400e);font-size:11px;line-height:1.5}.dsh-mobile-control__update-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}.dsh-mobile-control__update-actions .dsh-mobile-control__primary,.dsh-mobile-control__update-actions .dsh-mobile-control__secondary{min-height:36px;padding:0 12px}.dsh-mobile-control__app-download{display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;min-height:44px;margin:0 0 10px;padding:8px 11px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:600 12px/1.3 system-ui;text-decoration:none}.dsh-mobile-control__app-download[hidden]{display:none}.dsh-mobile-control__app-download::after{color:var(--dsw-alias-label-primary-bluish,#2563eb);font-size:14px;content:"↗"}.dsh-mobile-control__app-download:hover{border-color:#9fb9e8;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-2,#f5f8ff));color:var(--dsw-alias-label-primary,#1d4ed8)}
|
|
16610
|
+
.dsh-mobile-control__release-notice{margin:-2px 0 10px;padding:8px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#eff6ff);color:var(--dsw-alias-label-primary,#1d4ed8);font-size:11px;line-height:1.45}.dsh-mobile-control__release-notice.is-error{color:var(--dsw-alias-label-primary,#dc2626);border-left:3px solid var(--dsw-alias-state-error-primary,#dc2626)}.dsh-mobile-control__release-notice[hidden]{display:none}.dsh-mobile-control__update-card{margin:0 0 10px;padding:11px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__update-card[hidden]{display:none}.dsh-mobile-control__update-card-title{margin:0 0 7px;color:var(--dsw-alias-label-primary,#16181d);font:650 13px/1.4 system-ui}.dsh-mobile-control__update-notes{display:block;box-sizing:border-box;max-height:150px;overflow-y:auto;margin:0 0 8px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#e1e5eb);border-radius:9px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#475569);white-space:pre-wrap;word-break:break-word;font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.dsh-mobile-control__update-notes[hidden]{display:none}.dsh-mobile-control__update-notice{margin:0 0 9px;padding:7px 9px;border-radius:8px;background:var(--dsw-alias-state-warn-tertiary,#fff4d6);color:var(--dsw-alias-state-warn-label,#92400e);font-size:11px;line-height:1.5}.dsh-mobile-control__update-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}.dsh-mobile-control__primary{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;min-height:40px;padding:9px 14px;border:1px solid var(--dsw-alias-label-primary-bluish,#2563eb);border-radius:10px;background:var(--dsw-alias-label-primary-bluish,#2563eb);color:#fff;font:600 12px/1.3 system-ui;cursor:pointer;touch-action:manipulation}.dsh-mobile-control__primary:hover:not(:disabled){border-color:#1d4ed8;background:#1d4ed8}.dsh-mobile-control__primary:active:not(:disabled){border-color:#1e40af;background:#1e40af}.dsh-mobile-control__primary:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__primary[hidden]{display:none}.dsh-mobile-control__update-actions .dsh-mobile-control__primary,.dsh-mobile-control__update-actions .dsh-mobile-control__secondary{min-height:36px;padding:0 12px}.dsh-mobile-control__app-download{display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;min-height:44px;margin:0 0 10px;padding:8px 11px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:600 12px/1.3 system-ui;text-decoration:none}.dsh-mobile-control__app-downloads{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px}.dsh-mobile-control__app-downloads .dsh-mobile-control__app-download{flex:1 1 240px;margin:0}.dsh-mobile-control__app-download.is-iroh{border-color:var(--dsw-alias-label-primary-bluish,#2563eb);color:var(--dsw-alias-label-primary-bluish,#2563eb)}.dsh-mobile-control__app-download.is-iroh[hidden]{display:none}.dsh-mobile-control__app-download[hidden]{display:none}.dsh-mobile-control__app-download::after{color:var(--dsw-alias-label-primary-bluish,#2563eb);font-size:14px;content:"↗"}.dsh-mobile-control__app-download:hover{border-color:#9fb9e8;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-2,#f5f8ff));color:var(--dsw-alias-label-primary,#1d4ed8)}
|
|
15101
16611
|
.dsh-mobile-control__switcher{display:grid;grid-template-columns:repeat(2,1fr);gap:4px;margin:0 0 14px;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f3f5f8)}.dsh-mobile-control__switcher[hidden]{display:none}.dsh-mobile-control__tab{min-height:36px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-secondary,#606873);font:600 13px/1 system-ui;cursor:pointer}.dsh-mobile-control__tab.is-active{background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);box-shadow:0 1px 3px rgb(15 23 42 / 10%)}.dsh-mobile-control__view[hidden]{display:none}.dsh-mobile-control__intro{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.55}.dsh-mobile-control__http-frame-warning{box-sizing:border-box;margin:-4px 0 12px;padding:8px 10px;border:1px solid #d6a83f;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#fff7df);color:var(--dsw-alias-label-primary,#694b00);font-size:11px;line-height:1.5;overflow-wrap:anywhere}.dsh-mobile-control__http-frame-warning[hidden]{display:none}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions button[hidden]{display:none}
|
|
15102
16612
|
.dsh-mobile-control__provider-section{position:relative;margin:0 0 14px}.dsh-mobile-control__section-title{margin:0 0 8px;color:var(--dsw-alias-label-primary,#16181d);font:650 13px/1.4 system-ui}.dsh-mobile-control__provider-section>.dsh-mobile-control__section-title{padding-right:42px}.dsh-mobile-control__provider-choices{display:grid;grid-template-columns:1fr;gap:8px}.dsh-mobile-control__provider{display:flex;min-width:0;flex-direction:column;gap:6px;min-height:94px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);text-align:left;cursor:pointer;touch-action:manipulation;transition:border-color 160ms ease,background-color 160ms ease,box-shadow 160ms ease}.dsh-mobile-control__provider:hover{border-color:#6f96db;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-1,#f8fbff))}.dsh-mobile-control__provider.is-selected{border-color:var(--dsw-alias-label-primary-bluish,#2563eb);background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-bg-layer-1,#f5f8ff));box-shadow:0 0 0 1px var(--dsw-alias-label-primary-bluish,#2563eb) inset}.dsh-mobile-control__provider:disabled{cursor:wait;opacity:.62}.dsh-mobile-control__provider-top{display:flex;min-width:0;align-items:flex-start;justify-content:space-between;gap:5px}.dsh-mobile-control__provider-top strong{min-width:0;font-size:12px;line-height:1.3}.dsh-mobile-control__provider-badge{flex:none;padding:2px 5px;border-radius:999px;background:var(--dsw-alias-state-business-tertiary,#e8f0ff);color:var(--dsw-alias-label-primary,#1d4ed8);font:650 11px/1.25 system-ui}.dsh-mobile-control__provider-badge.is-cpolar{background:var(--dsw-alias-state-success-tertiary,#eaf8f2);color:var(--dsw-alias-label-primary,#087454)}.dsh-mobile-control__provider-description{color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.45}.dsh-mobile-control__provider-info{position:absolute;z-index:5;top:-13px;right:-8px}.dsh-mobile-control__provider-info-button{display:flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary,#475569);cursor:pointer;touch-action:manipulation}.dsh-mobile-control__provider-info-button:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f5f9);color:var(--dsw-alias-label-primary-bluish,#2563eb)}.dsh-mobile-control__provider-info-glyph{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:18px;height:18px;border:1.5px solid currentColor;border-radius:50%;font:700 12px/1 system-ui}.dsh-mobile-control__provider-info-popover{position:absolute;z-index:6;top:38px;right:4px;box-sizing:border-box;width:min(292px,calc(100vw - 72px));padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 10px 28px rgb(15 23 42 / 16%)}.dsh-mobile-control__provider-info-popover[hidden]{display:none}.dsh-mobile-control__provider-info-popover strong,.dsh-mobile-control__provider-info-popover span{display:block}.dsh-mobile-control__provider-info-popover strong{margin-bottom:3px;font-size:12px}.dsh-mobile-control__provider-info-popover span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}
|
|
15103
16613
|
.dsh-mobile-control__self-hosted{margin:8px 0 0;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__self-hosted-summary{display:flex;box-sizing:border-box;min-height:48px;align-items:center;justify-content:space-between;gap:10px;padding:8px 11px;cursor:pointer;list-style-position:inside}.dsh-mobile-control__self-hosted-summary>span:first-child{display:flex;min-width:0;flex-direction:column;gap:1px}.dsh-mobile-control__self-hosted-summary strong{font-size:11px}.dsh-mobile-control__self-hosted-summary span span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.35}.dsh-mobile-control__provider-badge.is-frp{background:var(--dsw-alias-bg-layer-1,#eef0f3);color:var(--dsw-alias-label-secondary,#475569)}.dsh-mobile-control__self-hosted-body{padding:0 8px 8px}.dsh-mobile-control__provider.is-frp{width:100%;min-height:64px;background:var(--dsw-alias-bg-layer-2,#fff)}
|
|
@@ -15121,7 +16631,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15121
16631
|
.dsh-mobile-control__diagnostic-checks{display:grid;gap:12px;margin-top:12px;animation:dsh-diagnostic-reveal 160ms ease-out both}.dsh-mobile-control__diagnostic-checks[hidden]{display:none}.dsh-mobile-control__diagnostic-group{overflow:hidden;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__diagnostic-group-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 11px;border-bottom:1px solid var(--dsw-alias-border-l2,#e1e5eb);background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__diagnostic-group-header h3{margin:0;font:650 11px/1.4 system-ui}.dsh-mobile-control__diagnostic-group-header span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__diagnostic-list{display:flex;flex-direction:column}.dsh-mobile-control__diagnostic-check{display:grid;grid-template-columns:26px minmax(0,1fr);gap:9px;padding:11px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__diagnostic-check + .dsh-mobile-control__diagnostic-check{border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}.dsh-mobile-control__diagnostic-marker{position:relative;width:26px;height:26px;border-radius:50%;background:var(--dsw-alias-bg-layer-1,#edf1f5);color:var(--dsh-diagnostic-info)}.dsh-mobile-control__diagnostic-marker::before,.dsh-mobile-control__diagnostic-marker::after{position:absolute;content:""}.dsh-mobile-control__diagnostic-check.is-ok .dsh-mobile-control__diagnostic-marker{background:var(--dsw-alias-state-success-tertiary,#e6f7f0);color:var(--dsh-diagnostic-ok)}.dsh-mobile-control__diagnostic-check.is-ok .dsh-mobile-control__diagnostic-marker::before{top:7px;left:7px;width:9px;height:5px;border-bottom:1.8px solid currentColor;border-left:1.8px solid currentColor;transform:rotate(-45deg)}.dsh-mobile-control__diagnostic-check.is-warning .dsh-mobile-control__diagnostic-marker{background:var(--dsw-alias-state-warn-tertiary,#fff4dc);color:var(--dsh-diagnostic-warning)}.dsh-mobile-control__diagnostic-check.is-error .dsh-mobile-control__diagnostic-marker{background:var(--dsw-alias-state-error-secondary,#fdecec);color:var(--dsh-diagnostic-error)}.dsh-mobile-control__diagnostic-check.is-warning .dsh-mobile-control__diagnostic-marker::before,.dsh-mobile-control__diagnostic-check.is-error .dsh-mobile-control__diagnostic-marker::before{top:6px;left:12px;width:2px;height:9px;border-radius:2px;background:currentColor}.dsh-mobile-control__diagnostic-check.is-warning .dsh-mobile-control__diagnostic-marker::after,.dsh-mobile-control__diagnostic-check.is-error .dsh-mobile-control__diagnostic-marker::after{bottom:6px;left:12px;width:2px;height:2px;border-radius:50%;background:currentColor}.dsh-mobile-control__diagnostic-check.is-info .dsh-mobile-control__diagnostic-marker::before{top:6px;left:12px;width:2px;height:2px;border-radius:50%;background:currentColor}.dsh-mobile-control__diagnostic-check.is-info .dsh-mobile-control__diagnostic-marker::after{top:10px;left:12px;width:2px;height:9px;border-radius:2px;background:currentColor}.dsh-mobile-control__diagnostic-check-body{min-width:0}.dsh-mobile-control__diagnostic-check-header{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.dsh-mobile-control__diagnostic-check-header strong{min-width:0;font-size:12px;line-height:1.4}.dsh-mobile-control__diagnostic-badge{flex:none;padding:2px 6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#edf1f5);color:var(--dsh-diagnostic-info);font:650 11px/1.3 system-ui}.dsh-mobile-control__diagnostic-check.is-ok .dsh-mobile-control__diagnostic-badge{background:var(--dsw-alias-state-success-tertiary,#e6f7f0);color:var(--dsh-diagnostic-ok)}.dsh-mobile-control__diagnostic-check.is-warning .dsh-mobile-control__diagnostic-badge{background:var(--dsw-alias-state-warn-tertiary,#fff4dc);color:var(--dsh-diagnostic-warning)}.dsh-mobile-control__diagnostic-check.is-error .dsh-mobile-control__diagnostic-badge{background:var(--dsw-alias-state-error-secondary,#fdecec);color:var(--dsh-diagnostic-error)}.dsh-mobile-control__diagnostic-check p{margin:4px 0 0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5;overflow-wrap:anywhere}.dsh-mobile-control__diagnostic-check .dsh-mobile-control__diagnostic-action{margin-top:7px;padding:7px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-1,#f8fafc);color:var(--dsw-alias-label-primary,#16181d)}.dsh-mobile-control__diagnostic-action span{display:inline-block;margin-right:6px;color:var(--dsw-alias-label-primary-bluish,#2563eb);font-weight:700}.dsh-mobile-control__diagnostic-details{margin-top:12px}.dsh-mobile-control__diagnostic-details[hidden]{display:none}.dsh-mobile-control__diagnostic-details>summary{box-sizing:border-box;min-height:44px;line-height:44px}.dsh-mobile-control__diagnostic-report{box-sizing:border-box;max-height:220px;margin:4px 0 0;overflow:auto;padding:10px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-secondary,#606873);font:11px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
15122
16632
|
.dsh-mobile-control__diagnostic-checks{transition:opacity 150ms ease}.dsh-mobile-control__diagnostic-checks.is-refreshing{opacity:.52}
|
|
15123
16633
|
@keyframes dsh-diagnostic-spin{to{transform:rotate(360deg)}}@keyframes dsh-diagnostic-reveal{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
|
|
15124
|
-
.dsh-mobile-control__actions{display:flex;flex-wrap:nowrap;gap:6px}.dsh-mobile-control__actions[hidden],.dsh-mobile-control__manage-row[hidden]{display:none}.dsh-mobile-control__actions button{flex:1 1 0;min-width:0;min-height:44px;padding:8px 4px;border-radius:10px;font:12px/1.2 system-ui;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dsh-mobile-control__secondary{border:1px solid var(--dsw-alias-border-l3,#cfd5dd);background:transparent;color:inherit}.dsh-mobile-control__primary{border:1px solid #2563eb;background:#2563eb;color:#fff}.dsh-mobile-control__actions button:disabled{cursor:not-allowed;opacity:.45}
|
|
16634
|
+
.dsh-mobile-control__actions{display:flex;flex-wrap:nowrap;gap:6px}.dsh-mobile-control__actions[hidden],.dsh-mobile-control__manage-row[hidden]{display:none}.dsh-mobile-control__actions button{flex:1 1 0;min-width:0;min-height:44px;padding:8px 4px;border-radius:10px;font:12px/1.2 system-ui;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dsh-mobile-control__secondary{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;min-height:40px;padding:9px 14px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:transparent;color:inherit;font:600 12px/1.3 system-ui;cursor:pointer;touch-action:manipulation}.dsh-mobile-control__secondary:hover:not(:disabled){border-color:#6f96db;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-1,#f8fbff))}.dsh-mobile-control__secondary:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__secondary[hidden]{display:none}.dsh-mobile-control__primary{border:1px solid #2563eb;background:#2563eb;color:#fff}.dsh-mobile-control__actions button:disabled{cursor:not-allowed;opacity:.45}
|
|
15125
16635
|
.dsh-mobile-control button:focus-visible,.dsh-mobile-control a:focus-visible,.dsh-mobile-control input:focus-visible,.dsh-mobile-control select:focus-visible,.dsh-mobile-control summary:focus-visible{outline:3px solid rgb(37 99 235 / 28%);outline-offset:2px}
|
|
15126
16636
|
/* 42px matches the DSH sidebar footer row; it is not one of the panel target tiers. */
|
|
15127
16637
|
/* Other footer entries share the shell's row; only our own action becomes compact. */
|
|
@@ -15135,7 +16645,167 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15135
16645
|
.dsh-mobile-control__origin-modes{min-width:0;margin:0 0 14px;padding:0;border:0}.dsh-mobile-control__origin-modes legend{margin:0 0 8px;padding:0;color:var(--dsw-alias-label-secondary,#606873);font:600 12px/1.4 system-ui}.dsh-mobile-control__origin-mode{display:flex;box-sizing:border-box;min-height:44px;align-items:center;gap:10px;padding:10px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);font:12px/1.5 system-ui;cursor:pointer}.dsh-mobile-control__origin-mode+.dsh-mobile-control__origin-mode{margin-top:8px}.dsh-mobile-control__origin-mode:has(input:checked){border-color:var(--dsw-alias-label-primary-bluish,#2563eb)}.dsh-mobile-control__origin-mode:focus-within{outline:2px solid var(--dsw-alias-state-business-primary,#2563eb);outline-offset:2px}.dsh-mobile-control__origin-mode:has(input:disabled){cursor:wait;opacity:.55}.dsh-mobile-control__origin-mode input[type=radio]{flex:0 0 18px;box-sizing:border-box;width:18px;height:18px;min-height:18px;margin:0;padding:0;accent-color:var(--dsw-alias-label-primary-bluish,#2563eb)}.dsh-mobile-control__origin-mode span{min-width:0;overflow-wrap:anywhere}
|
|
15136
16646
|
/* Base the columns on the card width, not the desktop viewport behind the popup. */
|
|
15137
16647
|
.dsh-mobile-control__origin-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,140px),1fr));gap:12px}.dsh-mobile-control__origin-fields>.dsh-mobile-control__field{min-width:0;grid-column:auto;font-size:12px}.dsh-mobile-control__origin-fields>.dsh-mobile-control__field--full{grid-column:1/-1}.dsh-mobile-control__origin-fields select{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__origin-fields select:disabled{cursor:not-allowed;opacity:.55}.dsh-mobile-control__caddy-form{display:grid;gap:12px}.dsh-mobile-control__caddy-form>.dsh-mobile-control__component-note,.dsh-mobile-control__caddy-form>.dsh-mobile-control__component-status,.dsh-mobile-control__caddy-form>.dsh-mobile-control__origin-feedback{margin:0;overflow-wrap:anywhere}.dsh-mobile-control__origin-setup .dsh-mobile-control__actions.dsh-mobile-control__caddy-actions{grid-template-columns:minmax(0,1fr);margin:0}.dsh-mobile-control__caddy-actions button{box-sizing:border-box;min-width:0;min-height:44px;padding:10px 12px;border-radius:10px;font:600 12px/1.4 system-ui;cursor:pointer}.dsh-mobile-control__origin-warning{margin:12px 0;padding:10px 12px;border-left:3px solid var(--dsw-alias-state-warn-primary,#a56810);border-radius:4px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-primary,#384152);font-size:12px;line-height:1.6}.dsh-mobile-control__origin-feedback{font-size:12px;line-height:1.6;overflow-wrap:anywhere}.dsh-mobile-control__origin-feedback.is-error{color:var(--dsw-alias-state-error-primary,#bc3030)}.dsh-mobile-control__origin-fields input[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary,#bc3030)}.dsh-mobile-control__origin-backend{display:flex;min-width:0;flex-direction:column;align-items:stretch;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2,#dbe1e8)}.dsh-mobile-control__origin-backend button{align-self:flex-start;min-height:44px}.dsh-mobile-control__self-hosted-body .is-origin{margin-top:8px}
|
|
16648
|
+
/* Final overrides: dense borderless layout must beat the base rules above.
|
|
16649
|
+
The earlier dense block sits before those base rules, so these repeat the
|
|
16650
|
+
essential de-boxing here at the end of the sheet where they win. */
|
|
16651
|
+
.dsh-mobile-control__provider{position:relative;border:0;border-radius:8px;padding:8px 10px;min-height:0;background:transparent;box-shadow:none}
|
|
16652
|
+
.dsh-mobile-control__provider:hover{border-color:transparent;background:var(--dsw-alias-interactive-bg-hover,#f1f5f9)}
|
|
16653
|
+
.dsh-mobile-control__provider.is-selected{border-color:transparent;background:var(--dsw-alias-interactive-bg-active,#f5f8ff);box-shadow:none}
|
|
16654
|
+
.dsh-mobile-control__provider.is-lan:not(.is-selected){border-color:transparent;background:transparent}
|
|
16655
|
+
.dsh-mobile-control__view:not(.is-remote)>.dsh-mobile-control__actions{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
|
|
16656
|
+
.dsh-mobile-control__view.is-diagnostics,.dsh-mobile-control__view.is-devices{flex:1;min-height:0;overflow-y:auto;padding:14px 20px}
|
|
16657
|
+
.dsh-mobile-control__view.is-devices .dsh-mobile-control__devices{max-height:none;margin-top:0}
|
|
16658
|
+
.dsh-mobile-control__actions button{white-space:normal;overflow:visible;text-overflow:clip;line-height:1.25;height:auto;padding-top:8px;padding-bottom:8px;word-break:break-word}
|
|
16659
|
+
.dsh-mobile-control__qr-placeholder{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:192px;height:192px;padding:12px;border:1px dashed var(--dsw-alias-border-l2,#dbe1e8);border-radius:12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.5;text-align:center}
|
|
16660
|
+
.dsh-mobile-control__lan-setup .dsh-mobile-control__actions,.dsh-mobile-control__cpolar-setup .dsh-mobile-control__actions,.dsh-mobile-control__frp-setup .dsh-mobile-control__actions,.dsh-mobile-control__origin-setup .dsh-mobile-control__actions{margin-top:12px}
|
|
16661
|
+
.dsh-mobile-control__origin-setup .dsh-mobile-control__actions.dsh-mobile-control__caddy-actions{margin-top:12px}
|
|
16662
|
+
.dsh-mobile-control__qr-zoom{pointer-events:auto}
|
|
16663
|
+
.dsh-mobile-control__iroh-pairing{border:0;border-radius:0;background:transparent;padding:0;margin:12px 0 0}
|
|
16664
|
+
/* Config setup panels: drop the rounded card frame so provider configuration
|
|
16665
|
+
reads as one flat page with hairline dividers instead of nested boxes. */
|
|
16666
|
+
.dsh-mobile-control__lan-setup,.dsh-mobile-control__cpolar-setup,.dsh-mobile-control__frp-setup,.dsh-mobile-control__origin-setup{border:0;border-radius:0;background:transparent;padding:0;margin:0 0 12px}
|
|
16667
|
+
.dsh-mobile-control__frp-setup>.dsh-mobile-control__frp-step,.dsh-mobile-control__frp-group,.dsh-mobile-control__frp-overview{border:0;border-radius:0;background:transparent}
|
|
16668
|
+
.dsh-mobile-control__ws-paths{border:0;border-radius:0;background:transparent;padding:0;margin:10px 0 0}
|
|
16669
|
+
.dsh-mobile-control__view.is-remote .dsh-mobile-control__remote-workspace>.dsh-mobile-control__actions{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
|
|
16670
|
+
.dsh-mobile-control__view.is-remote .dsh-mobile-control__remote-workspace>.dsh-mobile-control__actions[hidden]{display:none}
|
|
16671
|
+
.dsh-mobile-control__view.is-remote .dsh-mobile-control__origin-setup .dsh-mobile-control__actions.dsh-mobile-control__caddy-actions{display:grid;grid-template-columns:minmax(0,1fr)}
|
|
16672
|
+
.dsh-mobile-control__remote-workspace{border:0;border-radius:0;background:transparent;box-shadow:none}
|
|
16673
|
+
.dsh-mobile-control__stage-header{border:0;border-radius:0;background:transparent;box-shadow:none}
|
|
16674
|
+
.dsh-mobile-control__app-downloads{display:flex;gap:8px;padding:6px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#e1e5eb)}
|
|
16675
|
+
.dsh-mobile-control__app-download{flex:1 1 0;display:flex;align-items:center;justify-content:space-between;gap:6px;box-sizing:border-box;min-height:34px;padding:6px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:8px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font-size:12px;text-decoration:none;white-space:normal}
|
|
16676
|
+
.dsh-mobile-control__app-download.is-iroh{border-color:var(--dsw-alias-label-primary-bluish,#2563eb)}
|
|
16677
|
+
.dsh-mobile-control__setup-details{margin-top:12px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}
|
|
16678
|
+
.dsh-mobile-control__remote-workspace>.dsh-mobile-control__qr{margin-bottom:18px}
|
|
16679
|
+
.dsh-mobile-control__stage-header{display:none}
|
|
16680
|
+
.dsh-mobile-control__provider-install{margin:0 0 12px}
|
|
16681
|
+
.dsh-mobile-control__provider-install>.dsh-mobile-control__primary{width:100%;min-height:44px}
|
|
16682
|
+
.dsh-mobile-control__tunnel-toggle{display:inline-flex;align-items:center;gap:4px;box-sizing:border-box;margin:0 0 10px;padding:6px 12px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary-bluish,#2563eb);font:600 12px/1 system-ui;cursor:pointer}
|
|
16683
|
+
.dsh-mobile-control__tunnel-toggle:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}
|
|
16684
|
+
.dsh-mobile-control__tunnel>.dsh-mobile-control__primary,.dsh-mobile-control__tunnel>.dsh-mobile-control__secondary{width:100%;min-height:44px;margin-top:10px}
|
|
16685
|
+
.dsh-mobile-control__provider-info-popover p{margin:0 0 8px;font-size:11px;line-height:1.5}
|
|
16686
|
+
.dsh-mobile-control__provider-info-popover p:last-child{margin-bottom:0}
|
|
16687
|
+
.dsh-mobile-control__provider-info-popover{max-width:240px;max-height:44vh;overflow:auto;white-space:normal}
|
|
16688
|
+
.dsh-mobile-control__mode-row{position:relative;display:flex;align-items:center;gap:8px;margin:0 0 8px}
|
|
16689
|
+
.dsh-mobile-control__mode-row>.dsh-mobile-control__switcher{flex:1 1 auto;margin:0}
|
|
16690
|
+
.dsh-mobile-control__notes-toggle{flex:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary,#606873);font:600 13px/1 system-ui;cursor:pointer}
|
|
16691
|
+
.dsh-mobile-control__notes-toggle:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}
|
|
16692
|
+
.dsh-mobile-control__notes-panel{position:absolute;top:calc(100% + 6px);right:0;z-index:40;width:min(320px,78vw);max-height:50vh;overflow:auto;box-sizing:border-box;margin:0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:8px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 6px 20px rgb(15 23 42 / 12%);font-size:11px;line-height:1.5;color:var(--dsw-alias-label-secondary,#475569)}
|
|
16693
|
+
.dsh-mobile-control__notes-panel p{margin:0 0 8px}.dsh-mobile-control__notes-panel p:last-child{margin:0}
|
|
16694
|
+
.dsh-mobile-control__notes-panel[hidden]{display:none}
|
|
16695
|
+
.dsh-mobile-control__tunnel-toggle[hidden]{display:none}
|
|
16696
|
+
.dsh-mobile-control__provider-setup-body .dsh-mobile-control__switcher{border-radius:0;background:transparent;padding:0;gap:8px}
|
|
16697
|
+
.dsh-mobile-control__provider-setup-body .dsh-mobile-control__tab{border-radius:0;box-shadow:none;border-bottom:2px solid transparent}
|
|
16698
|
+
.dsh-mobile-control__provider-setup-body .dsh-mobile-control__tab.is-active{background:transparent;box-shadow:none;border-bottom-color:var(--dsw-alias-label-primary-bluish,#2563eb)}
|
|
16699
|
+
.dsh-mobile-control__origin-setup .dsh-mobile-control__origin-mode{border-radius:0;background:transparent}
|
|
16700
|
+
.dsh-mobile-control__devices{border:0;border-radius:0;padding:0;margin:0;background:transparent;max-height:none}
|
|
16701
|
+
.dsh-mobile-control__provider.is-frp:not(.is-selected),.dsh-mobile-control__provider.is-origin:not(.is-selected),.dsh-mobile-control__provider.is-iroh:not(.is-selected){border-color:transparent;background:transparent}
|
|
16702
|
+
.dsh-mobile-control__provider.is-selected::before{content:"";position:absolute;left:0;top:8px;bottom:8px;width:2px;border-radius:2px;background:var(--dsw-alias-label-primary-bluish,#2563eb)}
|
|
16703
|
+
.dsh-mobile-control__remote-workspace section{border:0;border-radius:0;padding:12px 0;background:transparent;box-shadow:none;border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}
|
|
16704
|
+
.dsh-mobile-control__remote-workspace section:first-child{border-top:0}
|
|
16705
|
+
.dsh-mobile-control__remote-workspace .dsh-mobile-control__details{border:0;border-radius:0;background:transparent;box-shadow:none}
|
|
16706
|
+
.dsh-mobile-control__self-hosted{border:0;border-radius:0;background:transparent;box-shadow:none}
|
|
15138
16707
|
` + EXTENSION_RECOVERY_STYLES;
|
|
16708
|
+
/** Resolve safe, locale-owned copy for tap-navigation results. */
|
|
16709
|
+
function mobilePushNavigationNotice(locale, key) {
|
|
16710
|
+
return {
|
|
16711
|
+
en: {
|
|
16712
|
+
"session-unavailable": "This conversation is no longer available.",
|
|
16713
|
+
"session-resolved": "Opened the conversation."
|
|
16714
|
+
},
|
|
16715
|
+
it: {
|
|
16716
|
+
"session-unavailable": "Questa conversazione non è più disponibile.",
|
|
16717
|
+
"session-resolved": "Conversazione aperta."
|
|
16718
|
+
},
|
|
16719
|
+
zh: {
|
|
16720
|
+
"session-unavailable": "此会话已不可用。",
|
|
16721
|
+
"session-resolved": "已打开会话。"
|
|
16722
|
+
}
|
|
16723
|
+
}[locale][key];
|
|
16724
|
+
}
|
|
16725
|
+
function showMobilePushNavigationNotice(page, view, key) {
|
|
16726
|
+
const node = page.createElement("div");
|
|
16727
|
+
node.className = "dsh-mobile-push-navigation-notice";
|
|
16728
|
+
node.setAttribute("role", "status");
|
|
16729
|
+
node.setAttribute("aria-live", "polite");
|
|
16730
|
+
node.lang = selectedMobileControlLocale();
|
|
16731
|
+
node.textContent = mobilePushNavigationNotice(selectedMobileControlLocale(), key);
|
|
16732
|
+
node.style.cssText = "position:fixed;z-index:1201;left:50%;bottom:max(24px,env(safe-area-inset-bottom));transform:translateX(-50%);max-width:calc(100vw - 32px);padding:10px 16px;border-radius:12px;background:var(--dsw-alias-bg-layer-3,#20242b);color:var(--dsw-alias-label-primary,#fff);font:14px/1.45 system-ui,sans-serif;box-shadow:0 8px 24px #0003;pointer-events:none";
|
|
16733
|
+
page.body.append(node);
|
|
16734
|
+
view.setTimeout(() => node.remove(), 3e3);
|
|
16735
|
+
}
|
|
16736
|
+
/** Own the browser-only push controller, settings row, and optional authenticated tap navigation. */
|
|
16737
|
+
function installMobilePushBrowserClient(ctx, runtime) {
|
|
16738
|
+
const { page, view } = runtime;
|
|
16739
|
+
if (view.__DSH_MOBILE_FRONTEND__ !== "dedicated" || view.__DSH_MOBILE_NATIVE__ !== void 0 || page.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") === null) return () => void 0;
|
|
16740
|
+
const api = runtime.api ?? createMobilePushBrowserApi(mobileRequest);
|
|
16741
|
+
const controller = createMobilePushController(api, runtime.environment ?? browserPushEnvironment(view));
|
|
16742
|
+
controller.start();
|
|
16743
|
+
const removeSettings = ctx.slots.inject("settings.general.item", () => ctx.slots.register({
|
|
16744
|
+
name: "settings.general.item",
|
|
16745
|
+
id: "dsh-mobile-push-browser",
|
|
16746
|
+
order: 120
|
|
16747
|
+
}, () => (0, react.createElement)(MobilePushSettings, {
|
|
16748
|
+
controller,
|
|
16749
|
+
locale: selectedMobileControlLocale()
|
|
16750
|
+
})));
|
|
16751
|
+
const refresh = () => {
|
|
16752
|
+
controller.refresh();
|
|
16753
|
+
};
|
|
16754
|
+
const onVisible = () => {
|
|
16755
|
+
if (page.visibilityState === "visible") refresh();
|
|
16756
|
+
};
|
|
16757
|
+
view.addEventListener("focus", refresh);
|
|
16758
|
+
page.addEventListener("visibilitychange", onVisible);
|
|
16759
|
+
const connection = ctx.get("connection");
|
|
16760
|
+
const unbindConnection = connection?.state?.subscribe(() => {
|
|
16761
|
+
if (connection.state?.getSnapshot() === "connected") refresh();
|
|
16762
|
+
}) ?? (() => void 0);
|
|
16763
|
+
let removeNavigation = () => void 0;
|
|
16764
|
+
let disposeNavigationLifecycle = () => void 0;
|
|
16765
|
+
const reference = new URLSearchParams(view.location.search).get("mobile-push");
|
|
16766
|
+
if (reference !== null && reference !== "" && ctx.inject !== void 0) {
|
|
16767
|
+
let navigationClosed = false;
|
|
16768
|
+
const navigationFiber = ctx.inject(mobilePushNavigationServices, (navigationContext) => {
|
|
16769
|
+
if (navigationClosed) return () => void 0;
|
|
16770
|
+
const pending = new URLSearchParams(view.location.search).get("mobile-push");
|
|
16771
|
+
if (pending === null || pending === "") return () => void 0;
|
|
16772
|
+
const remove = installPushNavigation(navigationContext, {
|
|
16773
|
+
resolve: (value) => api.resolve(value),
|
|
16774
|
+
notify: (key) => {
|
|
16775
|
+
if (key !== "session-unavailable" && key !== "session-resolved") return;
|
|
16776
|
+
if (runtime.notify) runtime.notify(key);
|
|
16777
|
+
else showMobilePushNavigationNotice(page, view, key);
|
|
16778
|
+
},
|
|
16779
|
+
browser: {
|
|
16780
|
+
location: view.location,
|
|
16781
|
+
history: view.history
|
|
16782
|
+
}
|
|
16783
|
+
});
|
|
16784
|
+
removeNavigation = remove;
|
|
16785
|
+
return () => {
|
|
16786
|
+
if (removeNavigation === remove) removeNavigation = () => void 0;
|
|
16787
|
+
remove();
|
|
16788
|
+
};
|
|
16789
|
+
});
|
|
16790
|
+
disposeNavigationLifecycle = () => {
|
|
16791
|
+
if (navigationClosed) return;
|
|
16792
|
+
navigationClosed = true;
|
|
16793
|
+
removeNavigation();
|
|
16794
|
+
navigationFiber.dispose();
|
|
16795
|
+
};
|
|
16796
|
+
}
|
|
16797
|
+
let disposed = false;
|
|
16798
|
+
return () => {
|
|
16799
|
+
if (disposed) return;
|
|
16800
|
+
disposed = true;
|
|
16801
|
+
view.removeEventListener("focus", refresh);
|
|
16802
|
+
page.removeEventListener("visibilitychange", onVisible);
|
|
16803
|
+
unbindConnection();
|
|
16804
|
+
disposeNavigationLifecycle();
|
|
16805
|
+
removeSettings();
|
|
16806
|
+
controller.stop();
|
|
16807
|
+
};
|
|
16808
|
+
}
|
|
15139
16809
|
/** Mount the desktop control or mobile feature enhancements. */
|
|
15140
16810
|
function apply(ctx) {
|
|
15141
16811
|
let gatewayRejectionSink;
|
|
@@ -15170,6 +16840,10 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15170
16840
|
return recovery();
|
|
15171
16841
|
};
|
|
15172
16842
|
}, "dsh-mobile: connection recovery");
|
|
16843
|
+
ctx.effect(() => installMobilePushBrowserClient(ctx, {
|
|
16844
|
+
page: document,
|
|
16845
|
+
view: window
|
|
16846
|
+
}), "dsh-mobile: authenticated browser push lifecycle");
|
|
15173
16847
|
ctx.effect(() => {
|
|
15174
16848
|
const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
|
|
15175
16849
|
const gatewayCustomization = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
|
|
@@ -15338,8 +17012,10 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15338
17012
|
exports.installCustomAssets = installCustomAssets;
|
|
15339
17013
|
exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
|
|
15340
17014
|
exports.installMobileNativeBack = installMobileNativeBack;
|
|
17015
|
+
exports.installMobilePushBrowserClient = installMobilePushBrowserClient;
|
|
15341
17016
|
exports.localizedDiagnosticEntryCopy = localizedDiagnosticEntryCopy;
|
|
15342
17017
|
exports.mobileBackBlockedMessage = mobileBackBlockedMessage;
|
|
17018
|
+
exports.mobilePushNavigationNotice = mobilePushNavigationNotice;
|
|
15343
17019
|
exports.normalizeDiagnosticOverall = normalizeDiagnosticOverall;
|
|
15344
17020
|
exports.normalizeDiagnosticStatus = normalizeDiagnosticStatus;
|
|
15345
17021
|
exports.parseMobileExtensionManifest = parseMobileExtensionManifest;
|