dsh-mobile 0.3.0 → 0.3.2
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 +14 -0
- package/README.en.md +13 -8
- package/README.md +13 -8
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/bin/dsh-mobile-funnel-win32-x64.exe +0 -0
- package/lib/client.js +2321 -412
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +32 -9
- package/lib/index.mjs +500 -114
- package/lib/mobile-layout.js +54 -10
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -5,6 +5,674 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
+
//#region src/client-messages.ts
|
|
9
|
+
const MOBILE_CONTROL_MESSAGES = {
|
|
10
|
+
en: {
|
|
11
|
+
mobileAccess: "Mobile access",
|
|
12
|
+
collapseMobileAccess: "Collapse Mobile access",
|
|
13
|
+
downloadAndroid: "Download Android app",
|
|
14
|
+
downloadAndroidAria: "Download the latest Android app from GitHub Releases",
|
|
15
|
+
lan: "Local network",
|
|
16
|
+
remote: "Remote",
|
|
17
|
+
lanAccess: "Local network access",
|
|
18
|
+
remoteAccess: "Remote access",
|
|
19
|
+
browserAccess: "Browser access",
|
|
20
|
+
remoteAddress: "Remote address",
|
|
21
|
+
loadingStatus: "Loading status…",
|
|
22
|
+
loadingRemoteStatus: "Loading remote status…",
|
|
23
|
+
generateCopyKey: "Generate and copy key",
|
|
24
|
+
copyPairLink: "Copy pairing link",
|
|
25
|
+
managePairedDevices: "Manage paired devices",
|
|
26
|
+
clearAllDevices: "Clear all devices",
|
|
27
|
+
pairingQr: "Pairing QR code",
|
|
28
|
+
remoteIntro: "Choose the remote channel that suits you. Switching or disabling remote access does not affect the local network.",
|
|
29
|
+
chooseProvider: "Choose connection method",
|
|
30
|
+
providerInfoAria: "View remote connection security and network information",
|
|
31
|
+
providerGroupAria: "Remote connection method",
|
|
32
|
+
providerSafeTitle: "You remain protected",
|
|
33
|
+
providerSafeText: "Only paired devices can access DSH. cpolar is installed on demand and can be removed completely; Tailscale may be slow or unavailable on mainland China networks, where cpolar is recommended.",
|
|
34
|
+
builtIn: "Built in",
|
|
35
|
+
mainlandPreferred: "Preferred in mainland China",
|
|
36
|
+
tailscaleDescription: "Wider coverage; mainland China networks may be unstable. The first connection requires login and Funnel authorization.",
|
|
37
|
+
cpolarDescription: "Installs the official component on demand and is suitable for mainland China networks.",
|
|
38
|
+
prepareCpolar: "Prepare cpolar",
|
|
39
|
+
checkingComponent: "Checking component…",
|
|
40
|
+
installOfficial: "Install official component",
|
|
41
|
+
cpolarAccountNote: "Sign in to the cpolar website and copy the Authtoken. The token is stored only in the plugin private directory and is never shown on the page or in logs.",
|
|
42
|
+
registerCpolar: "Register with cpolar",
|
|
43
|
+
openDashboard: "Open dashboard to get token",
|
|
44
|
+
tokenPlaceholder: "Paste cpolar Authtoken",
|
|
45
|
+
saveConnect: "Save and connect",
|
|
46
|
+
saving: "Saving…",
|
|
47
|
+
componentDetails: "Component source and cleanup",
|
|
48
|
+
componentDetailsText: "Downloaded from the official cpolar website and verified at a fixed version only after you choose Install. It does not add a system service, startup item, registry entry, or PATH entry.",
|
|
49
|
+
pluginPrivateDirectory: "Plugin private directory",
|
|
50
|
+
officialDownload: "Official download page",
|
|
51
|
+
terms: "Terms of service",
|
|
52
|
+
purgeCpolar: "Completely remove cpolar component and configuration",
|
|
53
|
+
tailscaleHelp: "How to use Tailscale",
|
|
54
|
+
tailscaleHelpText: "The runtime component is included with the plugin. The first connection opens the official Tailscale login and Funnel authorization pages; the plugin never accesses your account password.",
|
|
55
|
+
funnelGuideAria: "Steps to enable Tailscale Funnel",
|
|
56
|
+
funnelGuideTitle: "Remote access setup · Step 2",
|
|
57
|
+
funnelGuideSummary: "Tailscale login is complete. Funnel must still be allowed for this computer; the official page also enables HTTPS.",
|
|
58
|
+
funnelStep1: "Open the official Tailscale authorization page for this node.",
|
|
59
|
+
funnelStep2: "Confirm Funnel; you do not need to sign in to DSH again.",
|
|
60
|
+
funnelStep3: "Return to DSH; the plugin checks and connects automatically.",
|
|
61
|
+
funnelGuideNote: "Requires an Owner, Admin, or Network admin account.",
|
|
62
|
+
continueFunnel: "Continue Funnel authorization",
|
|
63
|
+
retryNow: "Done, retry now",
|
|
64
|
+
enableRemote: "Enable remote access",
|
|
65
|
+
disableRemote: "Disable remote access",
|
|
66
|
+
continueLogin: "Continue login",
|
|
67
|
+
reconnect: "Reconnect",
|
|
68
|
+
generateRemoteQr: "Generate remote pairing QR code",
|
|
69
|
+
manageRemoteDevices: "Manage remote devices",
|
|
70
|
+
resetRemoteLogin: "Sign out and clear remote login",
|
|
71
|
+
resetRemoteDevices: "Disable and clear remote devices",
|
|
72
|
+
lanOn: "Local network access is on.",
|
|
73
|
+
lanOff: "Local network access is off.",
|
|
74
|
+
enableLan: "Enable local network access",
|
|
75
|
+
disableLan: "Disable local network access",
|
|
76
|
+
extensionsLoaded: "Extensions: {loaded} loaded",
|
|
77
|
+
extensionsFailed: "Extensions: {loaded} loaded, {failed} failed",
|
|
78
|
+
keyGenerationFailed: "Could not generate a pairing key.",
|
|
79
|
+
keyCopied: "Pairing key copied. Paste it into the Android app.",
|
|
80
|
+
linkCopied: "Pairing link copied. Send it to the phone and paste it into the app or open it in a browser.",
|
|
81
|
+
copySecret: "Copy {kind}: {value}",
|
|
82
|
+
pairingKey: "pairing key",
|
|
83
|
+
pairingLink: "pairing link",
|
|
84
|
+
noDevices: "No paired devices.",
|
|
85
|
+
noRemoteDevices: "No remote paired devices.",
|
|
86
|
+
device: "Device",
|
|
87
|
+
expires: "Expires {time}",
|
|
88
|
+
revoke: "Revoke",
|
|
89
|
+
confirmResetDevices: "Remove all paired devices? Connected devices will be disconnected immediately.",
|
|
90
|
+
ready: "Ready",
|
|
91
|
+
installed: "Installed",
|
|
92
|
+
installWithSize: "Install official component · {size} MB",
|
|
93
|
+
cpolarUnsupported: "Only Windows x64 is currently supported. You can still use the built-in Tailscale Funnel.",
|
|
94
|
+
cpolarNotInstalled: "Not installed. A fixed version is downloaded from the official cpolar website only after you click the button below.",
|
|
95
|
+
cpolarNeedsToken: "Official component {version} is verified. Save the account token to continue.",
|
|
96
|
+
cpolarReady: "Official component {version} and the local account configuration are ready.",
|
|
97
|
+
remoteOff: "Remote access is disabled. Local network access is unaffected.",
|
|
98
|
+
remoteUnavailableCpolar: "cpolar is not installed or its local account is not configured.",
|
|
99
|
+
remoteUnavailableTailscale: "This computer is missing the Funnel runtime component. Reinstall the complete plugin package.",
|
|
100
|
+
remoteStartingCpolar: "Connecting to a cpolar node…",
|
|
101
|
+
remoteStartingTailscale: "Starting the secure Tailscale channel…",
|
|
102
|
+
remoteNeedsLogin: "Complete a one-time Tailscale login in the browser. The plugin never reads your password.",
|
|
103
|
+
remoteConnectingCpolar: "Public address allocated; starting the DSH authentication gateway…",
|
|
104
|
+
remoteConnectingTailscale: "Login complete; creating the public HTTPS address…",
|
|
105
|
+
remoteReady: "Remote access is ready. Only paired devices can access DSH.",
|
|
106
|
+
remoteError: "The remote connection was not established. Reconnect; local network access still works.",
|
|
107
|
+
funnelPermission: "Login is complete. Authorize Funnel to establish the remote connection automatically.",
|
|
108
|
+
funnelHttps: "Login is complete. Authorize Funnel; the official page also enables HTTPS.",
|
|
109
|
+
funnelStart: "Login is complete. Finish the initial Tailscale Funnel authorization.",
|
|
110
|
+
tailscaleDnsMissing: "Tailscale has not provided a remote address. Reconnect and confirm login is complete.",
|
|
111
|
+
gatewayStartFailed: "The remote gateway failed to start. Reconnect; local network access is unaffected.",
|
|
112
|
+
controlChannelFailed: "The remote component connection was interrupted. Reconnect.",
|
|
113
|
+
cpolarMissing: "The official cpolar component is not installed. Complete the preparation steps above.",
|
|
114
|
+
cpolarInvalid: "cpolar component verification failed. Remove it completely and reinstall.",
|
|
115
|
+
cpolarConfigMissing: "No cpolar account token is saved. Complete the preparation steps above.",
|
|
116
|
+
cpolarConfigInvalid: "The local cpolar configuration is invalid. Save the account token again.",
|
|
117
|
+
cpolarPortUnavailable: "Could not allocate a local remote-gateway port. Try again.",
|
|
118
|
+
cpolarLaunchFailed: "The cpolar client failed to start.",
|
|
119
|
+
cpolarTimeout: "Timed out connecting to a cpolar node. Reconnect.",
|
|
120
|
+
cpolarStopped: "The cpolar connection stopped.",
|
|
121
|
+
cpolarExited: "The cpolar connection exited unexpectedly. Reconnect.",
|
|
122
|
+
cpolarOutputInvalid: "cpolar returned an unrecognized status.",
|
|
123
|
+
cpolarOriginInvalid: "The public address returned by cpolar failed validation.",
|
|
124
|
+
setupOpened: "The official Tailscale page is open. Return to DSH after enabling Funnel; reconnection is automatic.",
|
|
125
|
+
switchProviderConfirm: "Switching connection method first disables the current remote channel. Local network access and paired devices are unaffected. Continue?",
|
|
126
|
+
switchingCpolar: "Switching to cpolar…",
|
|
127
|
+
switchingTailscale: "Switching to Tailscale Funnel…",
|
|
128
|
+
installConfirm: "Download and verify a fixed version from the official cpolar website (about 7.3 MB) and extract it only to the DSH Mobile private directory? No system service, PATH/registry entry, or startup item is added.",
|
|
129
|
+
downloading: "Downloading and verifying…",
|
|
130
|
+
installingCpolar: "Installing the official cpolar component. Keep DSH running until it completes.",
|
|
131
|
+
installFailed: "Component installation failed: {error}",
|
|
132
|
+
invalidToken: "Paste the complete Authtoken from the cpolar dashboard.",
|
|
133
|
+
configuredConnecting: "Account configuration saved; connecting through cpolar…",
|
|
134
|
+
configureFailed: "Configuration failed: {error}",
|
|
135
|
+
purgeConfirm: "Completely remove the cpolar component, token configuration, and runtime logs from the DSH Mobile private directory? Local network access, DSH data, and other system programs are unaffected.",
|
|
136
|
+
purging: "Stopping the channel and removing cpolar files managed by DSH Mobile…",
|
|
137
|
+
purgeFailed: "Cleanup failed: {error}",
|
|
138
|
+
reconnectingCpolar: "Reconnecting to a cpolar node…",
|
|
139
|
+
reconnectingTailscale: "Checking Tailscale settings and reconnecting…",
|
|
140
|
+
remoteQrReady: "Remote pairing QR code generated. Scan it from “Remote access” in the app.",
|
|
141
|
+
resetCpolarConfirm: "Disable the cpolar remote channel and remove all remote paired devices? Your cpolar account and other tunnels are unchanged.",
|
|
142
|
+
resetTailscaleConfirm: "Sign out of Tailscale on this computer and remove all remote paired devices? Local network configuration is unchanged.",
|
|
143
|
+
requestFailed: "Request failed: {error}"
|
|
144
|
+
},
|
|
145
|
+
it: {
|
|
146
|
+
mobileAccess: "Accesso mobile",
|
|
147
|
+
collapseMobileAccess: "Riduci Accesso mobile",
|
|
148
|
+
downloadAndroid: "Scarica app Android",
|
|
149
|
+
downloadAndroidAria: "Scarica l’ultima app Android da GitHub Releases",
|
|
150
|
+
lan: "Rete locale",
|
|
151
|
+
remote: "Remoto",
|
|
152
|
+
lanAccess: "Accesso rete locale",
|
|
153
|
+
remoteAccess: "Accesso remoto",
|
|
154
|
+
browserAccess: "Accesso browser",
|
|
155
|
+
remoteAddress: "Indirizzo remoto",
|
|
156
|
+
loadingStatus: "Lettura stato…",
|
|
157
|
+
loadingRemoteStatus: "Lettura stato remoto…",
|
|
158
|
+
generateCopyKey: "Genera e copia chiave",
|
|
159
|
+
copyPairLink: "Copia link di abbinamento",
|
|
160
|
+
managePairedDevices: "Gestisci dispositivi abbinati",
|
|
161
|
+
clearAllDevices: "Rimuovi tutti i dispositivi",
|
|
162
|
+
pairingQr: "Codice QR di abbinamento",
|
|
163
|
+
remoteIntro: "Scegli il canale remoto più adatto. Cambiare o disattivare l’accesso remoto non influisce sulla rete locale.",
|
|
164
|
+
chooseProvider: "Scegli metodo di connessione",
|
|
165
|
+
providerInfoAria: "Informazioni su sicurezza e rete della connessione remota",
|
|
166
|
+
providerGroupAria: "Metodo di connessione remota",
|
|
167
|
+
providerSafeTitle: "La protezione resta attiva",
|
|
168
|
+
providerSafeText: "Solo i dispositivi abbinati possono accedere a DSH. cpolar viene installato su richiesta e può essere rimosso completamente; Tailscale può essere lento o non disponibile nelle reti della Cina continentale, dove è consigliato cpolar.",
|
|
169
|
+
builtIn: "Integrato",
|
|
170
|
+
mainlandPreferred: "Preferito in Cina continentale",
|
|
171
|
+
tailscaleDescription: "Copertura più ampia; le reti della Cina continentale possono essere instabili. Il primo collegamento richiede accesso e autorizzazione Funnel.",
|
|
172
|
+
cpolarDescription: "Installa su richiesta il componente ufficiale, adatto alle reti della Cina continentale.",
|
|
173
|
+
prepareCpolar: "Prepara cpolar",
|
|
174
|
+
checkingComponent: "Controllo componente…",
|
|
175
|
+
installOfficial: "Installa componente ufficiale",
|
|
176
|
+
cpolarAccountNote: "Accedi al sito cpolar e copia l’Authtoken. Il token resta solo nella directory privata del plugin e non viene mostrato nella pagina o nei log.",
|
|
177
|
+
registerCpolar: "Registrati su cpolar",
|
|
178
|
+
openDashboard: "Apri la dashboard per il token",
|
|
179
|
+
tokenPlaceholder: "Incolla Authtoken cpolar",
|
|
180
|
+
saveConnect: "Salva e connetti",
|
|
181
|
+
saving: "Salvataggio…",
|
|
182
|
+
componentDetails: "Origine e rimozione del componente",
|
|
183
|
+
componentDetailsText: "Viene scaricato dal sito ufficiale cpolar e verificato a una versione fissa solo dopo aver scelto Installa. Non aggiunge servizi di sistema, avvio automatico, voci di registro o PATH.",
|
|
184
|
+
pluginPrivateDirectory: "Directory privata del plugin",
|
|
185
|
+
officialDownload: "Pagina download ufficiale",
|
|
186
|
+
terms: "Termini di servizio",
|
|
187
|
+
purgeCpolar: "Rimuovi completamente componente e configurazione cpolar",
|
|
188
|
+
tailscaleHelp: "Guida a Tailscale",
|
|
189
|
+
tailscaleHelpText: "Il componente runtime è incluso nel plugin. Il primo collegamento apre le pagine ufficiali di accesso Tailscale e autorizzazione Funnel; il plugin non accede mai alla password.",
|
|
190
|
+
funnelGuideAria: "Passaggi per abilitare Tailscale Funnel",
|
|
191
|
+
funnelGuideTitle: "Configurazione accesso remoto · Passaggio 2",
|
|
192
|
+
funnelGuideSummary: "L’accesso Tailscale è completato. Devi ancora consentire Funnel per questo computer; la pagina ufficiale abilita anche HTTPS.",
|
|
193
|
+
funnelStep1: "Apri la pagina ufficiale di autorizzazione Tailscale per questo nodo.",
|
|
194
|
+
funnelStep2: "Conferma Funnel; non serve accedere di nuovo a DSH.",
|
|
195
|
+
funnelStep3: "Torna in DSH; il plugin controlla e si connette automaticamente.",
|
|
196
|
+
funnelGuideNote: "Richiede un account Owner, Admin o Network admin.",
|
|
197
|
+
continueFunnel: "Continua autorizzazione Funnel",
|
|
198
|
+
retryNow: "Fatto, riprova ora",
|
|
199
|
+
enableRemote: "Attiva accesso remoto",
|
|
200
|
+
disableRemote: "Disattiva accesso remoto",
|
|
201
|
+
continueLogin: "Continua accesso",
|
|
202
|
+
reconnect: "Riconnetti",
|
|
203
|
+
generateRemoteQr: "Genera QR di abbinamento remoto",
|
|
204
|
+
manageRemoteDevices: "Gestisci dispositivi remoti",
|
|
205
|
+
resetRemoteLogin: "Esci e rimuovi accesso remoto",
|
|
206
|
+
resetRemoteDevices: "Disattiva e rimuovi dispositivi remoti",
|
|
207
|
+
lanOn: "Accesso dalla rete locale attivo.",
|
|
208
|
+
lanOff: "Accesso dalla rete locale disattivato.",
|
|
209
|
+
enableLan: "Attiva accesso rete locale",
|
|
210
|
+
disableLan: "Disattiva accesso rete locale",
|
|
211
|
+
extensionsLoaded: "Estensioni: {loaded} caricate",
|
|
212
|
+
extensionsFailed: "Estensioni: {loaded} caricate, {failed} non riuscite",
|
|
213
|
+
keyGenerationFailed: "Impossibile generare la chiave di abbinamento.",
|
|
214
|
+
keyCopied: "Chiave di abbinamento copiata. Incollala nell’app Android.",
|
|
215
|
+
linkCopied: "Link di abbinamento copiato. Invialo al telefono e incollalo nell’app oppure aprilo nel browser.",
|
|
216
|
+
copySecret: "Copia {kind}: {value}",
|
|
217
|
+
pairingKey: "chiave di abbinamento",
|
|
218
|
+
pairingLink: "link di abbinamento",
|
|
219
|
+
noDevices: "Nessun dispositivo abbinato.",
|
|
220
|
+
noRemoteDevices: "Nessun dispositivo remoto abbinato.",
|
|
221
|
+
device: "Dispositivo",
|
|
222
|
+
expires: "Scade {time}",
|
|
223
|
+
revoke: "Revoca",
|
|
224
|
+
confirmResetDevices: "Rimuovere tutti i dispositivi abbinati? I dispositivi connessi verranno disconnessi subito.",
|
|
225
|
+
ready: "Pronto",
|
|
226
|
+
installed: "Installato",
|
|
227
|
+
installWithSize: "Installa componente ufficiale · {size} MB",
|
|
228
|
+
cpolarUnsupported: "Attualmente è supportato solo Windows x64. Puoi comunque usare Tailscale Funnel integrato.",
|
|
229
|
+
cpolarNotInstalled: "Non installato. Una versione fissa viene scaricata dal sito ufficiale cpolar solo dopo aver premuto il pulsante.",
|
|
230
|
+
cpolarNeedsToken: "Componente ufficiale {version} verificato. Salva il token account per continuare.",
|
|
231
|
+
cpolarReady: "Componente ufficiale {version} e configurazione account locale pronti.",
|
|
232
|
+
remoteOff: "Accesso remoto disattivato. La rete locale non è interessata.",
|
|
233
|
+
remoteUnavailableCpolar: "cpolar non è installato o l’account locale non è configurato.",
|
|
234
|
+
remoteUnavailableTailscale: "Su questo computer manca il componente runtime Funnel. Reinstalla il pacchetto completo del plugin.",
|
|
235
|
+
remoteStartingCpolar: "Connessione a un nodo cpolar…",
|
|
236
|
+
remoteStartingTailscale: "Avvio del canale sicuro Tailscale…",
|
|
237
|
+
remoteNeedsLogin: "Completa una volta l’accesso Tailscale nel browser. Il plugin non legge mai la password.",
|
|
238
|
+
remoteConnectingCpolar: "Indirizzo pubblico assegnato; avvio del gateway di autenticazione DSH…",
|
|
239
|
+
remoteConnectingTailscale: "Accesso completato; creazione dell’indirizzo HTTPS pubblico…",
|
|
240
|
+
remoteReady: "Accesso remoto pronto. Solo i dispositivi abbinati possono accedere a DSH.",
|
|
241
|
+
remoteError: "Connessione remota non stabilita. Riconnettiti; la rete locale continua a funzionare.",
|
|
242
|
+
funnelPermission: "Accesso completato. Autorizza Funnel per stabilire automaticamente la connessione remota.",
|
|
243
|
+
funnelHttps: "Accesso completato. Autorizza Funnel; la pagina ufficiale abilita anche HTTPS.",
|
|
244
|
+
funnelStart: "Accesso completato. Termina la prima autorizzazione Tailscale Funnel.",
|
|
245
|
+
tailscaleDnsMissing: "Tailscale non ha fornito un indirizzo remoto. Riconnettiti e verifica di aver completato l’accesso.",
|
|
246
|
+
gatewayStartFailed: "Avvio del gateway remoto non riuscito. Riconnettiti; la rete locale non è interessata.",
|
|
247
|
+
controlChannelFailed: "Connessione al componente remoto interrotta. Riconnettiti.",
|
|
248
|
+
cpolarMissing: "Il componente ufficiale cpolar non è installato. Completa i passaggi sopra.",
|
|
249
|
+
cpolarInvalid: "Verifica del componente cpolar non riuscita. Rimuovilo completamente e reinstallalo.",
|
|
250
|
+
cpolarConfigMissing: "Nessun token account cpolar salvato. Completa i passaggi sopra.",
|
|
251
|
+
cpolarConfigInvalid: "Configurazione locale cpolar non valida. Salva nuovamente il token.",
|
|
252
|
+
cpolarPortUnavailable: "Impossibile assegnare una porta locale al gateway remoto. Riprova.",
|
|
253
|
+
cpolarLaunchFailed: "Avvio del client cpolar non riuscito.",
|
|
254
|
+
cpolarTimeout: "Connessione al nodo cpolar scaduta. Riconnettiti.",
|
|
255
|
+
cpolarStopped: "Connessione cpolar arrestata.",
|
|
256
|
+
cpolarExited: "Connessione cpolar terminata inaspettatamente. Riconnettiti.",
|
|
257
|
+
cpolarOutputInvalid: "cpolar ha restituito uno stato non riconosciuto.",
|
|
258
|
+
cpolarOriginInvalid: "L’indirizzo pubblico restituito da cpolar non ha superato la verifica.",
|
|
259
|
+
setupOpened: "La pagina ufficiale Tailscale è aperta. Torna in DSH dopo aver abilitato Funnel; la riconnessione è automatica.",
|
|
260
|
+
switchProviderConfirm: "Il cambio di metodo disattiva prima il canale remoto corrente. Rete locale e dispositivi abbinati non sono interessati. Continuare?",
|
|
261
|
+
switchingCpolar: "Passaggio a cpolar…",
|
|
262
|
+
switchingTailscale: "Passaggio a Tailscale Funnel…",
|
|
263
|
+
installConfirm: "Scaricare e verificare una versione fissa dal sito ufficiale cpolar (circa 7,3 MB), estraendola solo nella directory privata DSH Mobile? Non vengono aggiunti servizi, PATH/registro o avvio automatico.",
|
|
264
|
+
downloading: "Download e verifica…",
|
|
265
|
+
installingCpolar: "Installazione del componente ufficiale cpolar. Mantieni DSH in esecuzione fino al termine.",
|
|
266
|
+
installFailed: "Installazione componente non riuscita: {error}",
|
|
267
|
+
invalidToken: "Incolla l’Authtoken completo dalla dashboard cpolar.",
|
|
268
|
+
configuredConnecting: "Configurazione account salvata; connessione tramite cpolar…",
|
|
269
|
+
configureFailed: "Configurazione non riuscita: {error}",
|
|
270
|
+
purgeConfirm: "Rimuovere completamente componente cpolar, configurazione token e log runtime dalla directory privata DSH Mobile? Rete locale, dati DSH e altri programmi non sono interessati.",
|
|
271
|
+
purging: "Arresto del canale e rimozione dei file cpolar gestiti da DSH Mobile…",
|
|
272
|
+
purgeFailed: "Pulizia non riuscita: {error}",
|
|
273
|
+
reconnectingCpolar: "Riconnessione a un nodo cpolar…",
|
|
274
|
+
reconnectingTailscale: "Controllo impostazioni Tailscale e riconnessione…",
|
|
275
|
+
remoteQrReady: "QR di abbinamento remoto generato. Scansionalo da “Accesso remoto” nell’app.",
|
|
276
|
+
resetCpolarConfirm: "Disattivare il canale remoto cpolar e rimuovere tutti i dispositivi remoti? Account cpolar e altri tunnel restano invariati.",
|
|
277
|
+
resetTailscaleConfirm: "Uscire da Tailscale su questo computer e rimuovere tutti i dispositivi remoti? La configurazione della rete locale resta invariata.",
|
|
278
|
+
requestFailed: "Richiesta non riuscita: {error}"
|
|
279
|
+
},
|
|
280
|
+
zh: {}
|
|
281
|
+
};
|
|
282
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
|
|
283
|
+
mobileAccess: "移动访问",
|
|
284
|
+
collapseMobileAccess: "收起移动访问",
|
|
285
|
+
downloadAndroid: "下载 Android App",
|
|
286
|
+
downloadAndroidAria: "前往 GitHub Releases 下载最新版 Android App",
|
|
287
|
+
lan: "局域网",
|
|
288
|
+
remote: "远程",
|
|
289
|
+
lanAccess: "局域网访问",
|
|
290
|
+
remoteAccess: "远程访问",
|
|
291
|
+
browserAccess: "浏览器访问",
|
|
292
|
+
remoteAddress: "远程地址",
|
|
293
|
+
loadingStatus: "正在读取状态…",
|
|
294
|
+
loadingRemoteStatus: "正在读取远程状态…",
|
|
295
|
+
generateCopyKey: "生成并复制密钥",
|
|
296
|
+
copyPairLink: "复制配对链接",
|
|
297
|
+
managePairedDevices: "管理配对设备",
|
|
298
|
+
clearAllDevices: "清除所有设备",
|
|
299
|
+
pairingQr: "配对二维码",
|
|
300
|
+
remoteIntro: "选择更适合你的远程通道。切换或关闭远程访问不会影响局域网。",
|
|
301
|
+
chooseProvider: "选择连接方式",
|
|
302
|
+
providerInfoAria: "查看远程连接安全与网络说明",
|
|
303
|
+
providerGroupAria: "远程连接方式",
|
|
304
|
+
providerSafeTitle: "你始终可以放心",
|
|
305
|
+
providerSafeText: "只有已配对设备能进入 DSH。cpolar 按需安装并可彻底清理;Tailscale 在中国大陆网络下可能连接缓慢、中断或无法使用,国内网络建议优先尝试 cpolar。",
|
|
306
|
+
builtIn: "内置",
|
|
307
|
+
mainlandPreferred: "国内网络优先",
|
|
308
|
+
tailscaleDescription: "覆盖更广;中国大陆网络可能不稳定,首次需登录并允许 Funnel。",
|
|
309
|
+
cpolarDescription: "按需安装官方组件,适合国内网络环境。",
|
|
310
|
+
prepareCpolar: "准备 cpolar",
|
|
311
|
+
checkingComponent: "正在检查组件…",
|
|
312
|
+
installOfficial: "安装官方组件",
|
|
313
|
+
cpolarAccountNote: "登录 cpolar 官网后复制 Authtoken。令牌只保存在本机插件私有目录,不会显示在页面或日志中。",
|
|
314
|
+
registerCpolar: "注册 cpolar",
|
|
315
|
+
openDashboard: "打开控制台获取令牌",
|
|
316
|
+
tokenPlaceholder: "粘贴 cpolar Authtoken",
|
|
317
|
+
saveConnect: "保存并连接",
|
|
318
|
+
saving: "正在保存…",
|
|
319
|
+
componentDetails: "组件来源与清理说明",
|
|
320
|
+
componentDetailsText: "仅在你点击安装后从 cpolar 官网下载并校验固定版本。不会写入系统服务、开机启动、注册表或 PATH。",
|
|
321
|
+
pluginPrivateDirectory: "插件私有目录",
|
|
322
|
+
officialDownload: "官方下载安装页",
|
|
323
|
+
terms: "服务条款",
|
|
324
|
+
purgeCpolar: "彻底移除 cpolar 组件与配置",
|
|
325
|
+
tailscaleHelp: "Tailscale 使用说明",
|
|
326
|
+
tailscaleHelpText: "运行组件已随插件提供。首次连接会打开 Tailscale 官方登录和 Funnel 授权页;插件不会接触你的账号密码。",
|
|
327
|
+
funnelGuideAria: "Tailscale Funnel 启用步骤",
|
|
328
|
+
funnelGuideTitle: "远程访问设置 · 第 2 步",
|
|
329
|
+
funnelGuideSummary: "Tailscale 登录已完成。还需为这台电脑允许 Funnel,官方页面会同时启用 HTTPS。",
|
|
330
|
+
funnelStep1: "打开当前节点的 Tailscale 官方授权页。",
|
|
331
|
+
funnelStep2: "确认启用 Funnel;无需再次登录 DSH。",
|
|
332
|
+
funnelStep3: "返回 DSH,插件会自动检查并建立连接。",
|
|
333
|
+
funnelGuideNote: "需要使用 Owner、Admin 或 Network admin 账号。",
|
|
334
|
+
continueFunnel: "继续完成 Funnel 授权",
|
|
335
|
+
retryNow: "已完成,立即重试",
|
|
336
|
+
enableRemote: "启用远程访问",
|
|
337
|
+
disableRemote: "关闭远程访问",
|
|
338
|
+
continueLogin: "继续登录",
|
|
339
|
+
reconnect: "重新连接",
|
|
340
|
+
generateRemoteQr: "生成远程配对二维码",
|
|
341
|
+
manageRemoteDevices: "管理远程设备",
|
|
342
|
+
resetRemoteLogin: "退出并清除远程登录",
|
|
343
|
+
resetRemoteDevices: "关闭并清除远程设备",
|
|
344
|
+
lanOn: "局域网访问已开启。",
|
|
345
|
+
lanOff: "局域网访问已关闭。",
|
|
346
|
+
enableLan: "开启局域网访问",
|
|
347
|
+
disableLan: "关闭局域网访问",
|
|
348
|
+
extensionsLoaded: "扩展:{loaded} 个已加载",
|
|
349
|
+
extensionsFailed: "扩展:{loaded} 个已加载,{failed} 个加载失败",
|
|
350
|
+
keyGenerationFailed: "无法生成配对密钥。",
|
|
351
|
+
keyCopied: "配对密钥已复制,请粘贴到 Android App。",
|
|
352
|
+
linkCopied: "配对链接已复制,发给手机后 App 粘贴或浏览器打开即可配对。",
|
|
353
|
+
copySecret: "请复制{kind}:{value}",
|
|
354
|
+
pairingKey: "配对密钥",
|
|
355
|
+
pairingLink: "配对链接",
|
|
356
|
+
noDevices: "暂无配对设备。",
|
|
357
|
+
noRemoteDevices: "暂无远程配对设备。",
|
|
358
|
+
device: "设备",
|
|
359
|
+
expires: "到期 {time}",
|
|
360
|
+
revoke: "撤销",
|
|
361
|
+
confirmResetDevices: "确定要移除所有配对设备吗?此操作会立即终止已连接设备。",
|
|
362
|
+
ready: "已就绪",
|
|
363
|
+
installed: "已安装",
|
|
364
|
+
installWithSize: "安装官方组件 · {size} MB",
|
|
365
|
+
cpolarUnsupported: "当前仅支持 Windows x64。你仍可选择内置的 Tailscale Funnel。",
|
|
366
|
+
cpolarNotInstalled: "尚未安装。只有点击下方按钮后,才会从 cpolar 官网下载固定版本。",
|
|
367
|
+
cpolarNeedsToken: "官方组件 {version} 已校验,下一步只需保存账号令牌。",
|
|
368
|
+
cpolarReady: "官方组件 {version} 与本机账号配置已就绪。",
|
|
369
|
+
remoteOff: "远程访问未启用。局域网访问不受影响。",
|
|
370
|
+
remoteUnavailableCpolar: "cpolar 尚未安装或未完成本机账号配置。",
|
|
371
|
+
remoteUnavailableTailscale: "当前电脑缺少 Funnel 运行组件,请重新安装完整插件包。",
|
|
372
|
+
remoteStartingCpolar: "正在连接 cpolar 国内节点…",
|
|
373
|
+
remoteStartingTailscale: "正在启动 Tailscale 安全通道…",
|
|
374
|
+
remoteNeedsLogin: "需要在浏览器完成一次 Tailscale 登录。插件不会读取你的密码。",
|
|
375
|
+
remoteConnectingCpolar: "公网地址已分配,正在启动 DSH 认证网关…",
|
|
376
|
+
remoteConnectingTailscale: "登录完成,正在建立公开 HTTPS 地址…",
|
|
377
|
+
remoteReady: "远程访问已就绪。只有已配对设备可以进入 DSH。",
|
|
378
|
+
remoteError: "远程连接未建立。可重新连接,局域网访问仍可正常使用。",
|
|
379
|
+
funnelPermission: "登录已完成。请继续授权 Funnel,完成后会自动建立远程连接。",
|
|
380
|
+
funnelHttps: "登录已完成。请继续授权 Funnel,官方页面会同时启用 HTTPS。",
|
|
381
|
+
funnelStart: "登录已完成。请继续完成 Tailscale Funnel 的首次授权。",
|
|
382
|
+
tailscaleDnsMissing: "Tailscale 暂未提供远程地址。请重新连接并确认已完成登录。",
|
|
383
|
+
gatewayStartFailed: "远程网关启动失败。请重新连接,局域网访问不受影响。",
|
|
384
|
+
controlChannelFailed: "远程组件连接中断。请重新连接。",
|
|
385
|
+
cpolarMissing: "cpolar 官方组件尚未安装。请先完成上方准备步骤。",
|
|
386
|
+
cpolarInvalid: "cpolar 组件校验失败。请彻底移除后重新安装。",
|
|
387
|
+
cpolarConfigMissing: "cpolar 尚未保存账号令牌。请先完成上方准备步骤。",
|
|
388
|
+
cpolarConfigInvalid: "cpolar 本机配置无效。请重新保存账号令牌。",
|
|
389
|
+
cpolarPortUnavailable: "无法分配本机远程网关端口,请重试。",
|
|
390
|
+
cpolarLaunchFailed: "cpolar 客户端未能启动。",
|
|
391
|
+
cpolarTimeout: "连接 cpolar 国内节点超时,请重新连接。",
|
|
392
|
+
cpolarStopped: "cpolar 连接已停止。",
|
|
393
|
+
cpolarExited: "cpolar 连接意外退出,请重新连接。",
|
|
394
|
+
cpolarOutputInvalid: "cpolar 返回了无法识别的状态。",
|
|
395
|
+
cpolarOriginInvalid: "cpolar 返回的公网地址未通过校验。",
|
|
396
|
+
setupOpened: "Tailscale 官方页面已打开。完成启用后返回 DSH,这里会自动重新连接。",
|
|
397
|
+
switchProviderConfirm: "切换连接方式会先关闭当前远程通道。局域网和配对设备不会受影响,是否继续?",
|
|
398
|
+
switchingCpolar: "正在切换到 cpolar…",
|
|
399
|
+
switchingTailscale: "正在切换到 Tailscale Funnel…",
|
|
400
|
+
installConfirm: "将从 cpolar 官方网站下载并校验固定版本(约 7.3 MB),仅解压到 DSH Mobile 私有目录。不会安装系统服务、写入 PATH/注册表或设置开机启动。是否继续?",
|
|
401
|
+
downloading: "正在下载并校验…",
|
|
402
|
+
installingCpolar: "正在安装 cpolar 官方组件。完成前请保持 DSH 运行。",
|
|
403
|
+
installFailed: "组件安装失败:{error}",
|
|
404
|
+
invalidToken: "请粘贴 cpolar 控制台提供的完整 Authtoken。",
|
|
405
|
+
configuredConnecting: "账号配置已保存,正在建立 cpolar 远程通道…",
|
|
406
|
+
configureFailed: "配置失败:{error}",
|
|
407
|
+
purgeConfirm: "彻底移除 DSH Mobile 私有目录中的 cpolar 组件、令牌配置和运行日志?不会影响局域网、DSH 数据或系统中的其他程序。",
|
|
408
|
+
purging: "正在关闭通道并清理 DSH Mobile 管理的 cpolar 文件…",
|
|
409
|
+
purgeFailed: "清理失败:{error}",
|
|
410
|
+
reconnectingCpolar: "正在重新连接 cpolar 国内节点…",
|
|
411
|
+
reconnectingTailscale: "正在确认 Tailscale 设置并重新连接…",
|
|
412
|
+
remoteQrReady: "远程配对二维码已生成。请在 App 的“远程访问”中扫描。",
|
|
413
|
+
resetCpolarConfirm: "关闭 cpolar 远程通道并移除所有远程配对设备?不会修改你的 cpolar 账号或其他隧道。",
|
|
414
|
+
resetTailscaleConfirm: "退出电脑上的 Tailscale 登录并移除所有远程配对设备?局域网配置不会改变。",
|
|
415
|
+
requestFailed: "请求失败:{error}"
|
|
416
|
+
});
|
|
417
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.en, {
|
|
418
|
+
requestTimeout: "The operation timed out. Confirm that DSH is still running, then try again.",
|
|
419
|
+
diagnostics: "Diagnostics",
|
|
420
|
+
openDiagnostics: "Open connection diagnostics",
|
|
421
|
+
back: "Back",
|
|
422
|
+
backToMobile: "Back to Mobile access",
|
|
423
|
+
connectionDiagnostics: "Connection diagnostics",
|
|
424
|
+
diagnosticsIntro: "Check versions, gateway, network adapter, firewall, and remote channel. The report is automatically redacted and never reads conversations or credentials.",
|
|
425
|
+
diagnosticsNotRun: "Not checked yet",
|
|
426
|
+
diagnosticsStartHint: "Select the button below to begin.",
|
|
427
|
+
diagnosticsIdleMeta: "Waiting to run · connection status only",
|
|
428
|
+
diagnosticsStart: "Start check",
|
|
429
|
+
diagnosticsCopy: "Copy redacted report",
|
|
430
|
+
diagnosticsAdvanced: "Advanced diagnostic details",
|
|
431
|
+
diagnosticsComplete: "Check complete",
|
|
432
|
+
diagnosticsAttention: "Some items need attention",
|
|
433
|
+
diagnosticsProblem: "Connection problems found",
|
|
434
|
+
diagnosticsCompleteFallback: "The check is complete.",
|
|
435
|
+
diagnosticStatusOk: "OK",
|
|
436
|
+
diagnosticStatusWarning: "Attention",
|
|
437
|
+
diagnosticStatusError: "Problem",
|
|
438
|
+
diagnosticStatusInfo: "Info",
|
|
439
|
+
diagnosticItems: "{count} items",
|
|
440
|
+
diagnosticCheck: "Check",
|
|
441
|
+
diagnosticAction: "Suggested action",
|
|
442
|
+
diagnosticNeedsAction: "Needs action",
|
|
443
|
+
diagnosticDetails: "Check details",
|
|
444
|
+
diagnosticOther: "Other checks",
|
|
445
|
+
diagnosticNoBlockers: "{count} checks · no blocking issues found",
|
|
446
|
+
diagnosticNeedsCount: "{count} checks · {issues} need attention",
|
|
447
|
+
diagnosticsChecking: "Checking…",
|
|
448
|
+
diagnosticsCheckingTitle: "Checking connection",
|
|
449
|
+
diagnosticsCheckingText: "This usually takes a few seconds. The remote channel performs a real reachability test.",
|
|
450
|
+
diagnosticsRunningMeta: "Running · keep DSH online",
|
|
451
|
+
diagnosticsIncomplete: "Check not completed",
|
|
452
|
+
diagnosticsReadFailed: "Could not read diagnostics: {error}",
|
|
453
|
+
diagnosticsUnavailable: "Diagnostics service unavailable · try again later",
|
|
454
|
+
diagnosticsRetry: "Check again",
|
|
455
|
+
diagnosticsCopied: "Redacted report copied. You can paste it directly into an issue.",
|
|
456
|
+
diagnosticsCopyManual: "Clipboard access was denied. Details are expanded for manual copying.",
|
|
457
|
+
diagnosticLabelVersions: "Version compatibility",
|
|
458
|
+
diagnosticLabelNetwork: "Local network adapter",
|
|
459
|
+
diagnosticLabelLan: "Local network gateway",
|
|
460
|
+
diagnosticLabelFirewall: "Windows Firewall",
|
|
461
|
+
diagnosticLabelRemote: "Remote channel",
|
|
462
|
+
diagnosticLabelPhone: "Phone network",
|
|
463
|
+
funnelTimeout: "The Tailscale component timed out while starting. Check the network, then reconnect."
|
|
464
|
+
});
|
|
465
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.it, {
|
|
466
|
+
requestTimeout: "Operazione scaduta. Verifica che DSH sia ancora in esecuzione e riprova.",
|
|
467
|
+
diagnostics: "Diagnostica",
|
|
468
|
+
openDiagnostics: "Apri diagnostica connessione",
|
|
469
|
+
back: "Indietro",
|
|
470
|
+
backToMobile: "Torna ad Accesso mobile",
|
|
471
|
+
connectionDiagnostics: "Diagnostica connessione",
|
|
472
|
+
diagnosticsIntro: "Controlla versioni, gateway, scheda di rete, firewall e canale remoto. Il report viene anonimizzato automaticamente e non legge conversazioni o credenziali.",
|
|
473
|
+
diagnosticsNotRun: "Controllo non eseguito",
|
|
474
|
+
diagnosticsStartHint: "Premi il pulsante sotto per iniziare.",
|
|
475
|
+
diagnosticsIdleMeta: "In attesa · solo stato connessione",
|
|
476
|
+
diagnosticsStart: "Avvia controllo",
|
|
477
|
+
diagnosticsCopy: "Copia report anonimizzato",
|
|
478
|
+
diagnosticsAdvanced: "Dettagli diagnostici avanzati",
|
|
479
|
+
diagnosticsComplete: "Controllo completato",
|
|
480
|
+
diagnosticsAttention: "Alcuni elementi richiedono attenzione",
|
|
481
|
+
diagnosticsProblem: "Rilevati problemi di connessione",
|
|
482
|
+
diagnosticsCompleteFallback: "Controllo completato.",
|
|
483
|
+
diagnosticStatusOk: "OK",
|
|
484
|
+
diagnosticStatusWarning: "Attenzione",
|
|
485
|
+
diagnosticStatusError: "Problema",
|
|
486
|
+
diagnosticStatusInfo: "Informazione",
|
|
487
|
+
diagnosticItems: "{count} elementi",
|
|
488
|
+
diagnosticCheck: "Controllo",
|
|
489
|
+
diagnosticAction: "Suggerimento",
|
|
490
|
+
diagnosticNeedsAction: "Da risolvere",
|
|
491
|
+
diagnosticDetails: "Dettagli controllo",
|
|
492
|
+
diagnosticOther: "Altri controlli",
|
|
493
|
+
diagnosticNoBlockers: "{count} controlli · nessun problema bloccante",
|
|
494
|
+
diagnosticNeedsCount: "{count} controlli · {issues} richiedono attenzione",
|
|
495
|
+
diagnosticsChecking: "Controllo…",
|
|
496
|
+
diagnosticsCheckingTitle: "Controllo connessione",
|
|
497
|
+
diagnosticsCheckingText: "Di solito richiede pochi secondi. Il canale remoto esegue un test reale di raggiungibilità.",
|
|
498
|
+
diagnosticsRunningMeta: "In esecuzione · mantieni DSH online",
|
|
499
|
+
diagnosticsIncomplete: "Controllo non completato",
|
|
500
|
+
diagnosticsReadFailed: "Impossibile leggere la diagnostica: {error}",
|
|
501
|
+
diagnosticsUnavailable: "Servizio diagnostico non disponibile · riprova più tardi",
|
|
502
|
+
diagnosticsRetry: "Ripeti controllo",
|
|
503
|
+
diagnosticsCopied: "Report anonimizzato copiato. Puoi incollarlo direttamente in una issue.",
|
|
504
|
+
diagnosticsCopyManual: "Il browser ha negato la copia. I dettagli sono stati aperti per la copia manuale.",
|
|
505
|
+
diagnosticLabelVersions: "Compatibilità versioni",
|
|
506
|
+
diagnosticLabelNetwork: "Scheda rete locale",
|
|
507
|
+
diagnosticLabelLan: "Gateway rete locale",
|
|
508
|
+
diagnosticLabelFirewall: "Windows Firewall",
|
|
509
|
+
diagnosticLabelRemote: "Canale remoto",
|
|
510
|
+
diagnosticLabelPhone: "Rete telefono",
|
|
511
|
+
funnelTimeout: "Avvio del componente Tailscale scaduto. Controlla la rete e riconnettiti."
|
|
512
|
+
});
|
|
513
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
|
|
514
|
+
requestTimeout: "操作超时,请确认 DSH 仍在运行后重试。",
|
|
515
|
+
diagnostics: "诊断",
|
|
516
|
+
openDiagnostics: "打开连接诊断",
|
|
517
|
+
back: "返回",
|
|
518
|
+
backToMobile: "返回移动访问",
|
|
519
|
+
connectionDiagnostics: "连接诊断",
|
|
520
|
+
diagnosticsIntro: "检查版本、网关、网卡、防火墙和远程通道。报告自动脱敏,不读取对话或凭据。",
|
|
521
|
+
diagnosticsNotRun: "尚未检查",
|
|
522
|
+
diagnosticsStartHint: "点击下方按钮开始。",
|
|
523
|
+
diagnosticsIdleMeta: "等待运行 · 仅收集连接状态",
|
|
524
|
+
diagnosticsStart: "开始检查",
|
|
525
|
+
diagnosticsCopy: "复制脱敏报告",
|
|
526
|
+
diagnosticsAdvanced: "高级诊断详情",
|
|
527
|
+
diagnosticsComplete: "检查完成",
|
|
528
|
+
diagnosticsAttention: "有项目需要留意",
|
|
529
|
+
diagnosticsProblem: "发现连接问题",
|
|
530
|
+
diagnosticsCompleteFallback: "检查已完成。",
|
|
531
|
+
diagnosticStatusOk: "正常",
|
|
532
|
+
diagnosticStatusWarning: "注意",
|
|
533
|
+
diagnosticStatusError: "问题",
|
|
534
|
+
diagnosticStatusInfo: "说明",
|
|
535
|
+
diagnosticItems: "{count} 项",
|
|
536
|
+
diagnosticCheck: "检查项",
|
|
537
|
+
diagnosticAction: "建议",
|
|
538
|
+
diagnosticNeedsAction: "需要处理",
|
|
539
|
+
diagnosticDetails: "检查详情",
|
|
540
|
+
diagnosticOther: "其他检查",
|
|
541
|
+
diagnosticNoBlockers: "{count} 项检查 · 未发现阻断问题",
|
|
542
|
+
diagnosticNeedsCount: "{count} 项检查 · {issues} 项需要处理",
|
|
543
|
+
diagnosticsChecking: "正在检查…",
|
|
544
|
+
diagnosticsCheckingTitle: "正在检查连接",
|
|
545
|
+
diagnosticsCheckingText: "通常几秒内完成。远程通道会执行一次真实可达性测试。",
|
|
546
|
+
diagnosticsRunningMeta: "正在运行 · 请保持 DSH 在线",
|
|
547
|
+
diagnosticsIncomplete: "检查未完成",
|
|
548
|
+
diagnosticsReadFailed: "无法读取诊断结果:{error}",
|
|
549
|
+
diagnosticsUnavailable: "诊断服务暂不可用 · 请稍后重试",
|
|
550
|
+
diagnosticsRetry: "重新检查",
|
|
551
|
+
diagnosticsCopied: "脱敏报告已复制,可直接粘贴到 Issue。",
|
|
552
|
+
diagnosticsCopyManual: "浏览器未允许复制,已展开详情,请手动复制。",
|
|
553
|
+
diagnosticLabelVersions: "版本兼容",
|
|
554
|
+
diagnosticLabelNetwork: "局域网网卡",
|
|
555
|
+
diagnosticLabelLan: "局域网网关",
|
|
556
|
+
diagnosticLabelFirewall: "Windows 防火墙",
|
|
557
|
+
diagnosticLabelRemote: "远程通道",
|
|
558
|
+
diagnosticLabelPhone: "手机网络",
|
|
559
|
+
funnelTimeout: "Tailscale 组件启动超时,请检查网络后重新连接。"
|
|
560
|
+
});
|
|
561
|
+
const LOCALIZED_DIAGNOSTIC_COPY = {
|
|
562
|
+
en: {
|
|
563
|
+
versions: "Installed plugin, DSH, and minimum Android app versions are shown.",
|
|
564
|
+
networkOk: "The configured local network adapter is available.",
|
|
565
|
+
networkError: "The saved local network adapter is unavailable.",
|
|
566
|
+
networkInfo: "A fixed local network configuration is in use.",
|
|
567
|
+
networkAction: "Run dsh-mobile setup again.",
|
|
568
|
+
lanOk: "The local gateway is listening and pairing is available.",
|
|
569
|
+
lanInfo: "Local network access is currently off.",
|
|
570
|
+
lanAction: "Enable local network access when the phone must connect directly.",
|
|
571
|
+
firewallOk: "Local TCP and discovery firewall rules are enabled.",
|
|
572
|
+
firewallWarning: "The complete local network firewall rules were not found.",
|
|
573
|
+
firewallInfo: "The system did not allow the plugin to read firewall status.",
|
|
574
|
+
firewallAction: "Run dsh-mobile setup as administrator and check the firewall rules.",
|
|
575
|
+
remoteOk: "The remote public address passed the reachability check.",
|
|
576
|
+
remoteWarning: "The remote channel needs attention or is still connecting.",
|
|
577
|
+
remoteError: "The remote connection is not currently reachable.",
|
|
578
|
+
remoteInfo: "Remote access is currently off.",
|
|
579
|
+
remoteAction: "Return to Remote access, follow the provider guidance, and reconnect.",
|
|
580
|
+
phone: "The computer cannot determine whether the router isolates the phone.",
|
|
581
|
+
phoneAction: "Confirm the phone and computer use the same network, then disable guest-network or AP isolation.",
|
|
582
|
+
reportTitle: "DSH Mobile diagnostic report",
|
|
583
|
+
generated: "Generated"
|
|
584
|
+
},
|
|
585
|
+
it: {
|
|
586
|
+
versions: "Sono indicate le versioni installate di plugin e DSH e la versione minima dell’app Android.",
|
|
587
|
+
networkOk: "La scheda di rete locale configurata è disponibile.",
|
|
588
|
+
networkError: "La scheda di rete locale salvata non è disponibile.",
|
|
589
|
+
networkInfo: "È in uso una configurazione di rete locale fissa.",
|
|
590
|
+
networkAction: "Esegui di nuovo dsh-mobile setup.",
|
|
591
|
+
lanOk: "Il gateway locale è in ascolto e l’abbinamento è disponibile.",
|
|
592
|
+
lanInfo: "L’accesso dalla rete locale è disattivato.",
|
|
593
|
+
lanAction: "Attiva l’accesso locale quando il telefono deve collegarsi direttamente.",
|
|
594
|
+
firewallOk: "Le regole firewall TCP locale e di rilevamento sono attive.",
|
|
595
|
+
firewallWarning: "Le regole firewall complete per la rete locale non sono state trovate.",
|
|
596
|
+
firewallInfo: "Il sistema non ha consentito al plugin di leggere lo stato del firewall.",
|
|
597
|
+
firewallAction: "Esegui dsh-mobile setup come amministratore e controlla le regole firewall.",
|
|
598
|
+
remoteOk: "L’indirizzo pubblico remoto ha superato il test di raggiungibilità.",
|
|
599
|
+
remoteWarning: "Il canale remoto richiede attenzione o è ancora in connessione.",
|
|
600
|
+
remoteError: "La connessione remota non è al momento raggiungibile.",
|
|
601
|
+
remoteInfo: "L’accesso remoto è disattivato.",
|
|
602
|
+
remoteAction: "Torna ad Accesso remoto, segui le indicazioni del provider e riconnettiti.",
|
|
603
|
+
phone: "Il computer non può stabilire se il router isola il telefono.",
|
|
604
|
+
phoneAction: "Verifica che telefono e computer usino la stessa rete, poi disattiva rete ospiti o isolamento AP.",
|
|
605
|
+
reportTitle: "Report diagnostico DSH Mobile",
|
|
606
|
+
generated: "Generato"
|
|
607
|
+
},
|
|
608
|
+
zh: {
|
|
609
|
+
reportTitle: "DSH Mobile 诊断报告",
|
|
610
|
+
generated: "生成时间"
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
const DIAGNOSTIC_REASON_MESSAGES = {
|
|
614
|
+
en: {
|
|
615
|
+
"versions-current": ["Installed plugin, DSH, and minimum Android app versions are shown.", ""],
|
|
616
|
+
"network-unavailable": ["The saved local network adapter is unavailable.", "Run dsh-mobile setup again."],
|
|
617
|
+
"network-interface": ["Using network interface {interfaceName}.", ""],
|
|
618
|
+
"network-fixed": ["A fixed local network configuration is in use.", ""],
|
|
619
|
+
"lan-ready": ["The local gateway is listening at {endpointSuffix}; pairing is available.", ""],
|
|
620
|
+
"lan-off": ["Local network access is currently off.", "Enable local network access when the phone must connect directly."],
|
|
621
|
+
"firewall-ready": ["Local TCP and discovery firewall rules are enabled.", ""],
|
|
622
|
+
"firewall-missing": ["The complete local network firewall rules were not found.", "Run dsh-mobile setup as administrator and check the firewall rules."],
|
|
623
|
+
"firewall-unknown": ["The system did not allow the plugin to read firewall status.", "If the phone cannot find this computer, run setup as administrator."],
|
|
624
|
+
"remote-off": ["Remote access through {provider} is currently off.", ""],
|
|
625
|
+
"remote-ready": ["{provider} endpoint {endpointSuffix} is reachable in about {latencyMs} ms.", ""],
|
|
626
|
+
"remote-rate-limited": ["{provider} endpoint {endpointSuffix} is reachable, but this check observed rate limiting.", "Try again later; older sessions load on demand to reduce traffic."],
|
|
627
|
+
"remote-fake-ip": ["The Tailscale address is intercepted by the current VPN or DNS proxy, so TLS was not established.", "Switch VPN node or proxy mode; if it still fails, use cpolar."],
|
|
628
|
+
"remote-unreachable": ["{provider} reports ready, but endpoint {endpointSuffix} is not reachable.", "Reconnect, then check the provider status if it still fails."],
|
|
629
|
+
"remote-needs-login": ["Tailscale is waiting for login to finish.", "Return to Remote access and continue login."],
|
|
630
|
+
"remote-connecting": ["The {provider} remote channel is still connecting.", "Wait briefly, then check again."],
|
|
631
|
+
"remote-controller-error": ["The {provider} controller reported {controllerCode}.", "Return to Remote access and reconnect."],
|
|
632
|
+
"phone-network-unknown": ["The computer cannot determine whether the router isolates the phone.", "Confirm the phone and computer use the same network, then disable guest-network or AP isolation."]
|
|
633
|
+
},
|
|
634
|
+
it: {
|
|
635
|
+
"versions-current": ["Sono indicate le versioni installate di plugin e DSH e la versione minima dell’app Android.", ""],
|
|
636
|
+
"network-unavailable": ["La scheda di rete locale salvata non è disponibile.", "Esegui di nuovo dsh-mobile setup."],
|
|
637
|
+
"network-interface": ["È in uso l’interfaccia di rete {interfaceName}.", ""],
|
|
638
|
+
"network-fixed": ["È in uso una configurazione di rete locale fissa.", ""],
|
|
639
|
+
"lan-ready": ["Il gateway locale è in ascolto su {endpointSuffix}; l’abbinamento è disponibile.", ""],
|
|
640
|
+
"lan-off": ["L’accesso dalla rete locale è disattivato.", "Attiva l’accesso locale quando il telefono deve collegarsi direttamente."],
|
|
641
|
+
"firewall-ready": ["Le regole firewall TCP locale e di rilevamento sono attive.", ""],
|
|
642
|
+
"firewall-missing": ["Le regole firewall complete per la rete locale non sono state trovate.", "Esegui dsh-mobile setup come amministratore e controlla le regole firewall."],
|
|
643
|
+
"firewall-unknown": ["Il sistema non ha consentito al plugin di leggere lo stato del firewall.", "Se il telefono non trova il computer, esegui setup come amministratore."],
|
|
644
|
+
"remote-off": ["L’accesso remoto tramite {provider} è disattivato.", ""],
|
|
645
|
+
"remote-ready": ["L’endpoint {provider} {endpointSuffix} è raggiungibile in circa {latencyMs} ms.", ""],
|
|
646
|
+
"remote-rate-limited": ["L’endpoint {provider} {endpointSuffix} è raggiungibile, ma il controllo ha rilevato una limitazione temporanea.", "Riprova più tardi; le sessioni precedenti vengono caricate su richiesta per ridurre il traffico."],
|
|
647
|
+
"remote-fake-ip": ["L’indirizzo Tailscale è intercettato dalla VPN o dal proxy DNS corrente e TLS non è stato stabilito.", "Cambia nodo VPN o modalità proxy; se il problema continua, usa cpolar."],
|
|
648
|
+
"remote-unreachable": ["{provider} risulta pronto, ma l’endpoint {endpointSuffix} non è raggiungibile.", "Riconnettiti; se il problema continua, controlla lo stato del provider."],
|
|
649
|
+
"remote-needs-login": ["Tailscale attende il completamento dell’accesso.", "Torna ad Accesso remoto e continua l’accesso."],
|
|
650
|
+
"remote-connecting": ["Il canale remoto {provider} è ancora in connessione.", "Attendi qualche istante e ripeti il controllo."],
|
|
651
|
+
"remote-controller-error": ["Il controller {provider} ha segnalato {controllerCode}.", "Torna ad Accesso remoto e riconnettiti."],
|
|
652
|
+
"phone-network-unknown": ["Il computer non può stabilire se il router isola il telefono.", "Verifica che telefono e computer usino la stessa rete, poi disattiva rete ospiti o isolamento AP."]
|
|
653
|
+
},
|
|
654
|
+
zh: {
|
|
655
|
+
"versions-current": ["已显示插件、DSH 和 Android App 最低版本。", ""],
|
|
656
|
+
"network-unavailable": ["已保存的局域网网卡当前不可用。", "重新运行 dsh-mobile setup。"],
|
|
657
|
+
"network-interface": ["正在使用网卡 {interfaceName}。", ""],
|
|
658
|
+
"network-fixed": ["当前使用固定局域网配置。", ""],
|
|
659
|
+
"lan-ready": ["局域网网关正在监听 {endpointSuffix},配对入口可用。", ""],
|
|
660
|
+
"lan-off": ["局域网访问当前未开启。", "手机需要直连时开启局域网访问。"],
|
|
661
|
+
"firewall-ready": ["局域网 TCP 与发现防火墙规则已启用。", ""],
|
|
662
|
+
"firewall-missing": ["未找到完整的局域网防火墙规则。", "以管理员身份运行 dsh-mobile setup 并检查防火墙规则。"],
|
|
663
|
+
"firewall-unknown": ["系统未允许插件读取防火墙状态。", "若手机找不到电脑,请以管理员身份运行 setup。"],
|
|
664
|
+
"remote-off": ["{provider} 远程访问当前未启用。", ""],
|
|
665
|
+
"remote-ready": ["{provider} 端点 {endpointSuffix} 可达,往返约 {latencyMs} ms。", ""],
|
|
666
|
+
"remote-rate-limited": ["{provider} 端点 {endpointSuffix} 可达,但本次检查观察到服务限流。", "稍后重试;旧会话会按需加载以减少流量。"],
|
|
667
|
+
"remote-fake-ip": ["Tailscale 地址被当前 VPN 或 DNS 代理接管,TLS 链路未建立。", "切换 VPN 节点或代理模式;仍失败时改用 cpolar。"],
|
|
668
|
+
"remote-unreachable": ["{provider} 显示已就绪,但端点 {endpointSuffix} 暂不可达。", "点击重新连接;仍失败时检查提供方状态。"],
|
|
669
|
+
"remote-needs-login": ["Tailscale 正在等待完成登录。", "返回远程访问并继续登录。"],
|
|
670
|
+
"remote-connecting": ["{provider} 远程通道仍在连接。", "等待片刻后重新检查。"],
|
|
671
|
+
"remote-controller-error": ["{provider} 控制器报告 {controllerCode}。", "返回远程访问并重新连接。"],
|
|
672
|
+
"phone-network-unknown": ["电脑无法判断路由器是否隔离了手机。", "确认手机与电脑使用同一网络,并关闭访客网络或 AP 隔离。"]
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
//#endregion
|
|
8
676
|
//#region src/native-mobile.ts
|
|
9
677
|
/** Mobile feature and compatibility rules applied to DSH React surfaces. */
|
|
10
678
|
const NATIVE_MOBILE_STYLES = `
|
|
@@ -38,7 +706,8 @@ window.__ModuleLoader__.load({
|
|
|
38
706
|
[data-dsh-mobile-header] [class*="_headerUtilities"] { gap:2px !important; }
|
|
39
707
|
[data-dsh-mobile-header] [class*="_sessionLogButton"] { width:40px; min-width:40px; padding:0 !important; overflow:hidden; color:transparent; font-size:0 !important; }
|
|
40
708
|
[data-dsh-mobile-header] [class*="_sessionLogButton"] > * { display:none !important; }
|
|
41
|
-
[data-dsh-mobile-header] [class*="_sessionLogButton"]::after { color:var(--dsw-text, #171a21); content:"
|
|
709
|
+
[data-dsh-mobile-header] [class*="_sessionLogButton"]::after { color:var(--dsw-text, #171a21); content:"Log"; font-size:11px; font-weight:600; }
|
|
710
|
+
html[data-dsh-mobile-language="zh"] [data-dsh-mobile-header] [class*="_sessionLogButton"]::after { content:"日志"; }
|
|
42
711
|
[data-dsh-mobile-header] [class*="_tabs"] { box-sizing:border-box !important; width:max-content !important; max-width:calc(100% - 58px) !important; min-height:28px !important; height:28px !important; margin-top:0 !important; padding-left:6px !important; padding-right:6px !important; overflow-x:auto; scrollbar-width:none; }
|
|
43
712
|
[data-dsh-mobile-header] [class*="_tab"] { padding-bottom:5px !important; }
|
|
44
713
|
[data-dsh-mobile-header] [class*="_tabs"]::-webkit-scrollbar { display:none; }
|
|
@@ -133,8 +802,15 @@ window.__ModuleLoader__.load({
|
|
|
133
802
|
[data-dsh-mobile-center] [role="dialog"][aria-label*="Context"] [class*="_rows"] > [class*="_row"] { display:flex !important; align-items:center !important; justify-content:space-between !important; width:100% !important; min-width:0 !important; white-space:nowrap !important; }
|
|
134
803
|
[data-dsh-mobile-center] [role="dialog"][aria-label*="上下文"] :is(dt,dd),
|
|
135
804
|
[data-dsh-mobile-center] [role="dialog"][aria-label*="Context"] :is(dt,dd) { white-space:nowrap !important; word-break:keep-all !important; }
|
|
136
|
-
.dsh-mobile-branch-toast { position:fixed; z-index:
|
|
137
|
-
.dsh-mobile-branch-toast[data-visible="true"] { opacity:1; transform:translate(-50%,0); }
|
|
805
|
+
.dsh-mobile-branch-toast,.dsh-mobile-media-toast { 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; }
|
|
806
|
+
.dsh-mobile-branch-toast[data-visible="true"],.dsh-mobile-media-toast[data-visible="true"] { opacity:1; transform:translate(-50%,0); }
|
|
807
|
+
.dsh-mobile-media-shortcuts { box-sizing:border-box; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; width:100%; padding:4px 4px 8px; margin-bottom:4px; border-bottom:1px solid var(--dsw-alias-border-inverted,var(--dsw-alias-border-subtle,rgb(148 163 184 / 28%))); }
|
|
808
|
+
.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:44px; padding:8px 10px; border:0; border-radius:10px; background:var(--dsw-alias-interactive-bg-hover,rgb(148 163 184 / 12%)); color:var(--dsw-alias-label-primary,inherit); cursor:pointer; font:inherit; font-size:14px; line-height:22px; text-align:left; touch-action:manipulation; }
|
|
809
|
+
.dsh-mobile-media-action:active { opacity:.72; }
|
|
810
|
+
.dsh-mobile-media-action:focus-visible { outline:2px solid var(--dsw-alias-interactive-border-focus,#4c82f7); outline-offset:1px; }
|
|
811
|
+
.dsh-mobile-media-action:disabled { cursor:default; opacity:.38; }
|
|
812
|
+
.dsh-mobile-media-action svg { flex:none; width:16px; height:16px; color:var(--dsw-alias-label-tertiary,currentColor); }
|
|
813
|
+
.dsh-mobile-media-action span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
138
814
|
[data-dsh-mobile-center] [class*="_composer"] { padding-left:8px !important; padding-right:8px !important; padding-bottom:max(8px,env(safe-area-inset-bottom)) !important; }
|
|
139
815
|
/* The desktop composer intentionally wraps whole toolbar groups. On a phone,
|
|
140
816
|
dynamic model and status labels made that row alternate between one and
|
|
@@ -170,13 +846,99 @@ window.__ModuleLoader__.load({
|
|
|
170
846
|
return Array.from(root.querySelectorAll("[class]")).find((element) => classToken(element, suffix));
|
|
171
847
|
}
|
|
172
848
|
const AUTO_HISTORY_THRESHOLD_PX = 64;
|
|
849
|
+
function controlledFileDragEvent(type, files, initialDropEffect) {
|
|
850
|
+
const event = new Event(type, {
|
|
851
|
+
bubbles: true,
|
|
852
|
+
cancelable: true
|
|
853
|
+
});
|
|
854
|
+
const dataTransfer = {
|
|
855
|
+
dropEffect: initialDropEffect,
|
|
856
|
+
effectAllowed: "copy",
|
|
857
|
+
files: Object.freeze([...files]),
|
|
858
|
+
types: Object.freeze(["Files"])
|
|
859
|
+
};
|
|
860
|
+
Object.defineProperty(event, "dataTransfer", { value: dataTransfer });
|
|
861
|
+
return event;
|
|
862
|
+
}
|
|
863
|
+
/** Ask the official document dragover listener whether the current composer accepts files. */
|
|
864
|
+
function preflightComposerImageDrop(target, files) {
|
|
865
|
+
if (files.length === 0) return false;
|
|
866
|
+
const event = controlledFileDragEvent("dragover", files, "none");
|
|
867
|
+
target.dispatchEvent(event);
|
|
868
|
+
return event.dataTransfer?.dropEffect === "copy";
|
|
869
|
+
}
|
|
870
|
+
/** Dispatch a real drop only after a fresh official-listener preflight; the result is not an attachment ACK. */
|
|
871
|
+
function dispatchComposerImageDrop(target, files) {
|
|
872
|
+
if (!preflightComposerImageDrop(target, files)) return false;
|
|
873
|
+
target.dispatchEvent(controlledFileDragEvent("drop", files, "copy"));
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
/** Apply the resolved locale independently from the document's possibly different lang attribute. */
|
|
877
|
+
function applyNativeMobileLanguageMarker(root, language) {
|
|
878
|
+
const previous = root.dataset.dshMobileLanguage;
|
|
879
|
+
root.dataset.dshMobileLanguage = language;
|
|
880
|
+
return () => {
|
|
881
|
+
if (root.dataset.dshMobileLanguage !== language) return;
|
|
882
|
+
if (previous === void 0) delete root.dataset.dshMobileLanguage;
|
|
883
|
+
else root.dataset.dshMobileLanguage = previous;
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
/** Resolve the supported language used by native-mobile controls. */
|
|
887
|
+
function resolveNativeMobileLanguage(documentLanguage, browserLanguages) {
|
|
888
|
+
return [documentLanguage, ...browserLanguages].map((value) => value.trim().toLowerCase().split(/[-_]/u)[0]).find((value) => value === "it" || value === "en" || value === "zh") ?? "en";
|
|
889
|
+
}
|
|
173
890
|
/** Whether a user-driven scroll moved upward into the automatic history-loading zone. */
|
|
174
891
|
function shouldAutoLoadEarlier(previousTop, currentTop) {
|
|
175
892
|
return currentTop <= AUTO_HISTORY_THRESHOLD_PX && currentTop < previousTop - .5;
|
|
176
893
|
}
|
|
894
|
+
/** Check that an asynchronous picker result still belongs to its originating session and composer. */
|
|
895
|
+
function isComposerMediaOriginCurrent(origin, current) {
|
|
896
|
+
return !current.disposed && origin.generation === current.generation && origin.href === current.href && origin.composer !== null && current.composerConnected && origin.composer === current.composer && origin.sessionRoot !== null && origin.sessionRoot === current.sessionRoot && origin.sessionId !== null && origin.sessionId === current.sessionId;
|
|
897
|
+
}
|
|
177
898
|
/** Add mobile semantics without replacing feature trees. */
|
|
178
899
|
function installNativeMobileSurface() {
|
|
179
900
|
document.documentElement.classList.add("dsh-native-mobile-active");
|
|
901
|
+
const browserLanguages = navigator.languages.length > 0 ? navigator.languages : [navigator.language];
|
|
902
|
+
const language = resolveNativeMobileLanguage(document.documentElement.lang, browserLanguages);
|
|
903
|
+
const restoreLanguageMarker = applyNativeMobileLanguageMarker(document.documentElement, language);
|
|
904
|
+
const label = (italian, english, chinese) => language === "it" ? italian : language === "zh" ? chinese : english;
|
|
905
|
+
const mediaIcon = (kind) => {
|
|
906
|
+
const namespace = "http://www.w3.org/2000/svg";
|
|
907
|
+
const icon = document.createElementNS(namespace, "svg");
|
|
908
|
+
icon.setAttribute("viewBox", "0 0 16 16");
|
|
909
|
+
icon.setAttribute("fill", "none");
|
|
910
|
+
icon.setAttribute("aria-hidden", "true");
|
|
911
|
+
if (kind === "attachment") {
|
|
912
|
+
const path = document.createElementNS(namespace, "path");
|
|
913
|
+
path.setAttribute("d", "M5.5498 9.75V5H6.9502V9.75C6.9502 10.3299 7.4201 10.7998 8 10.7998C8.5799 10.7998 9.0498 10.3299 9.0498 9.75V4.5C9.0498 2.9536 7.7964 1.7002 6.25 1.7002C4.7036 1.7002 3.4502 2.9536 3.4502 4.5V9.75C3.4502 12.2629 5.4871 14.2998 8 14.2998C10.5129 14.2998 12.5498 12.2629 12.5498 9.75V4H13.9502V9.75C13.9502 13.0361 11.2861 15.7002 8 15.7002C4.71391 15.7002 2.0498 13.0361 2.0498 9.75V4.5C2.04981 2.1804 3.9304 0.299806 6.25 0.299805C8.5696 0.299805 10.4502 2.1804 10.4502 4.5V9.75C10.4502 11.1031 9.3531 12.2002 8 12.2002C6.6469 12.2002 5.5498 11.1031 5.5498 9.75Z");
|
|
914
|
+
path.setAttribute("fill", "currentColor");
|
|
915
|
+
icon.append(path);
|
|
916
|
+
return icon;
|
|
917
|
+
}
|
|
918
|
+
const body = document.createElementNS(namespace, "path");
|
|
919
|
+
body.setAttribute("d", "M5.15 3.2 6.05 2h3.9l.9 1.2h1.45c1.05 0 1.9.85 1.9 1.9v6c0 1.05-.85 1.9-1.9 1.9H3.7a1.9 1.9 0 0 1-1.9-1.9v-6c0-1.05.85-1.9 1.9-1.9h1.45Zm-1.45 1.3a.6.6 0 0 0-.6.6v6c0 .33.27.6.6.6h8.6a.6.6 0 0 0 .6-.6v-6a.6.6 0 0 0-.6-.6h-2.1l-.9-1.2H6.7l-.9 1.2H3.7Z");
|
|
920
|
+
body.setAttribute("fill", "currentColor");
|
|
921
|
+
const lens = document.createElementNS(namespace, "circle");
|
|
922
|
+
lens.setAttribute("cx", "8");
|
|
923
|
+
lens.setAttribute("cy", "8.1");
|
|
924
|
+
lens.setAttribute("r", "2.15");
|
|
925
|
+
lens.setAttribute("stroke", "currentColor");
|
|
926
|
+
lens.setAttribute("stroke-width", "1.3");
|
|
927
|
+
icon.append(body, lens);
|
|
928
|
+
return icon;
|
|
929
|
+
};
|
|
930
|
+
const createMediaAction = (kind, text) => {
|
|
931
|
+
const button = document.createElement("button");
|
|
932
|
+
button.type = "button";
|
|
933
|
+
button.className = "dsh-mobile-media-action";
|
|
934
|
+
button.lang = language;
|
|
935
|
+
button.dataset.dshMobileMediaAction = kind;
|
|
936
|
+
button.append(mediaIcon(kind));
|
|
937
|
+
const caption = document.createElement("span");
|
|
938
|
+
caption.textContent = text;
|
|
939
|
+
button.append(caption);
|
|
940
|
+
return button;
|
|
941
|
+
};
|
|
180
942
|
const setInputMode = (mode) => {
|
|
181
943
|
document.documentElement.dataset.dshMobileInput = mode;
|
|
182
944
|
};
|
|
@@ -191,19 +953,221 @@ window.__ModuleLoader__.load({
|
|
|
191
953
|
const backdrop = document.createElement("button");
|
|
192
954
|
backdrop.type = "button";
|
|
193
955
|
backdrop.className = "dsh-native-mobile-backdrop";
|
|
956
|
+
backdrop.lang = language;
|
|
194
957
|
backdrop.hidden = true;
|
|
195
|
-
backdrop.setAttribute("aria-label", "关闭工作区导航");
|
|
958
|
+
backdrop.setAttribute("aria-label", label("Chiudi navigazione area di lavoro", "Close workspace navigation", "关闭工作区导航"));
|
|
196
959
|
document.body.append(backdrop);
|
|
197
960
|
const branchToast = document.createElement("div");
|
|
198
961
|
branchToast.className = "dsh-mobile-branch-toast";
|
|
962
|
+
branchToast.lang = language;
|
|
199
963
|
branchToast.setAttribute("role", "status");
|
|
200
964
|
branchToast.setAttribute("aria-live", "polite");
|
|
201
965
|
document.body.append(branchToast);
|
|
966
|
+
const mediaToast = document.createElement("div");
|
|
967
|
+
mediaToast.className = "dsh-mobile-media-toast";
|
|
968
|
+
mediaToast.lang = language;
|
|
969
|
+
mediaToast.setAttribute("role", "status");
|
|
970
|
+
mediaToast.setAttribute("aria-live", "polite");
|
|
971
|
+
document.body.append(mediaToast);
|
|
972
|
+
const mediaActions = document.createElement("div");
|
|
973
|
+
mediaActions.className = "dsh-mobile-media-shortcuts";
|
|
974
|
+
mediaActions.lang = language;
|
|
975
|
+
mediaActions.dataset.dshMobileMediaShortcuts = "true";
|
|
976
|
+
mediaActions.setAttribute("role", "group");
|
|
977
|
+
mediaActions.setAttribute("aria-label", label("Aggiungi immagine", "Add image", "添加图片"));
|
|
978
|
+
const fileButton = createMediaAction("attachment", label("Scegli immagine", "Choose image", "选择图片"));
|
|
979
|
+
const cameraButton = createMediaAction("camera", label("Scatta foto", "Take photo", "拍照"));
|
|
980
|
+
fileButton.disabled = true;
|
|
981
|
+
cameraButton.disabled = true;
|
|
982
|
+
mediaActions.append(fileButton, cameraButton);
|
|
202
983
|
let branchToastTimer = 0;
|
|
984
|
+
let mediaToastTimer = 0;
|
|
985
|
+
const showMediaToast = (message) => {
|
|
986
|
+
mediaToast.textContent = message;
|
|
987
|
+
mediaToast.dataset.visible = "true";
|
|
988
|
+
if (mediaToastTimer !== 0) window.clearTimeout(mediaToastTimer);
|
|
989
|
+
mediaToastTimer = window.setTimeout(() => {
|
|
990
|
+
mediaToast.removeAttribute("data-visible");
|
|
991
|
+
mediaToastTimer = 0;
|
|
992
|
+
}, 2200);
|
|
993
|
+
};
|
|
994
|
+
let mediaRequestGeneration = 0;
|
|
995
|
+
let disposed = false;
|
|
996
|
+
let boundComposer = null;
|
|
997
|
+
let boundSessionRoot = null;
|
|
998
|
+
let boundSessionId = null;
|
|
999
|
+
const preflightFile = new File([], "dsh-mobile-preflight.png", { type: "image/png" });
|
|
1000
|
+
const canAcceptComposerDrop = () => preflightComposerImageDrop(document, [preflightFile]);
|
|
1001
|
+
const mediaPickerAbortController = new AbortController();
|
|
1002
|
+
const browserPickerCleanups = /* @__PURE__ */ new Set();
|
|
1003
|
+
const sessionTokens = /* @__PURE__ */ new WeakMap();
|
|
1004
|
+
let nextSessionToken = 0;
|
|
1005
|
+
const currentSessionOrigin = () => {
|
|
1006
|
+
const dedicatedRoot = boundComposer?.closest("[data-dsh-mobile-session]") ?? null;
|
|
1007
|
+
const dedicatedId = dedicatedRoot?.getAttribute("data-dsh-mobile-session");
|
|
1008
|
+
if (dedicatedRoot !== null && typeof dedicatedId === "string" && dedicatedId !== "") return {
|
|
1009
|
+
sessionRoot: dedicatedRoot,
|
|
1010
|
+
sessionId: dedicatedId
|
|
1011
|
+
};
|
|
1012
|
+
const selectedRow = document.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]");
|
|
1013
|
+
if (selectedRow === null) return {
|
|
1014
|
+
sessionRoot: null,
|
|
1015
|
+
sessionId: null
|
|
1016
|
+
};
|
|
1017
|
+
let token = sessionTokens.get(selectedRow);
|
|
1018
|
+
if (token === void 0) {
|
|
1019
|
+
token = `stock-${String(++nextSessionToken)}`;
|
|
1020
|
+
sessionTokens.set(selectedRow, token);
|
|
1021
|
+
}
|
|
1022
|
+
const identity = selectedRow.getAttribute("data-session-id") ?? selectedRow.getAttribute("aria-label") ?? selectedRow.textContent?.trim() ?? "";
|
|
1023
|
+
return {
|
|
1024
|
+
sessionRoot: selectedRow,
|
|
1025
|
+
sessionId: `${token}:${identity}`
|
|
1026
|
+
};
|
|
1027
|
+
};
|
|
1028
|
+
const mediaRequestContext = () => {
|
|
1029
|
+
const session = currentSessionOrigin();
|
|
1030
|
+
return {
|
|
1031
|
+
generation: ++mediaRequestGeneration,
|
|
1032
|
+
href: window.location.href,
|
|
1033
|
+
composer: boundComposer,
|
|
1034
|
+
...session
|
|
1035
|
+
};
|
|
1036
|
+
};
|
|
1037
|
+
const mediaRequestIsCurrent = (context) => {
|
|
1038
|
+
const session = currentSessionOrigin();
|
|
1039
|
+
const composer = boundComposer;
|
|
1040
|
+
return isComposerMediaOriginCurrent(context, {
|
|
1041
|
+
generation: mediaRequestGeneration,
|
|
1042
|
+
href: window.location.href,
|
|
1043
|
+
composer,
|
|
1044
|
+
sessionRoot: session.sessionRoot,
|
|
1045
|
+
sessionId: session.sessionId,
|
|
1046
|
+
disposed,
|
|
1047
|
+
composerConnected: context.composer?.isConnected === true
|
|
1048
|
+
});
|
|
1049
|
+
};
|
|
1050
|
+
const deliverImages = (files, context) => {
|
|
1051
|
+
if (files.length === 0 || !mediaRequestIsCurrent(context)) return;
|
|
1052
|
+
dispatchComposerImageDrop(document, files);
|
|
1053
|
+
};
|
|
1054
|
+
const launchBrowserPicker = (camera, context) => {
|
|
1055
|
+
const input = document.createElement("input");
|
|
1056
|
+
input.type = "file";
|
|
1057
|
+
input.accept = "image/png,image/jpeg,image/webp,image/gif";
|
|
1058
|
+
if (camera) input.capture = "environment";
|
|
1059
|
+
input.hidden = true;
|
|
1060
|
+
const signal = mediaPickerAbortController.signal;
|
|
1061
|
+
let cleanupTimer = 0;
|
|
1062
|
+
let watchdogTimer = 0;
|
|
1063
|
+
let cleaned = false;
|
|
1064
|
+
const scheduleCleanup = () => {
|
|
1065
|
+
if (!cleaned && cleanupTimer === 0) cleanupTimer = window.setTimeout(cleanup, 1e3);
|
|
1066
|
+
};
|
|
1067
|
+
const onVisibilityChange = () => {
|
|
1068
|
+
if (document.visibilityState === "visible") scheduleCleanup();
|
|
1069
|
+
};
|
|
1070
|
+
const onChange = () => {
|
|
1071
|
+
if (cleaned) return;
|
|
1072
|
+
const files = input.files === null ? [] : [...input.files];
|
|
1073
|
+
cleanup();
|
|
1074
|
+
deliverImages(files, context);
|
|
1075
|
+
};
|
|
1076
|
+
const cleanup = () => {
|
|
1077
|
+
if (cleaned) return;
|
|
1078
|
+
cleaned = true;
|
|
1079
|
+
if (cleanupTimer !== 0) window.clearTimeout(cleanupTimer);
|
|
1080
|
+
if (watchdogTimer !== 0) window.clearTimeout(watchdogTimer);
|
|
1081
|
+
cleanupTimer = 0;
|
|
1082
|
+
watchdogTimer = 0;
|
|
1083
|
+
input.removeEventListener("change", onChange);
|
|
1084
|
+
input.removeEventListener("cancel", cleanup);
|
|
1085
|
+
window.removeEventListener("focus", scheduleCleanup);
|
|
1086
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1087
|
+
signal.removeEventListener("abort", cleanup);
|
|
1088
|
+
browserPickerCleanups.delete(cleanup);
|
|
1089
|
+
input.remove();
|
|
1090
|
+
};
|
|
1091
|
+
input.addEventListener("change", onChange);
|
|
1092
|
+
input.addEventListener("cancel", cleanup);
|
|
1093
|
+
window.addEventListener("focus", scheduleCleanup);
|
|
1094
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1095
|
+
signal.addEventListener("abort", cleanup, { once: true });
|
|
1096
|
+
watchdogTimer = window.setTimeout(cleanup, 3e5);
|
|
1097
|
+
browserPickerCleanups.add(cleanup);
|
|
1098
|
+
if (signal.aborted) {
|
|
1099
|
+
cleanup();
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
document.body.append(input);
|
|
1104
|
+
input.click();
|
|
1105
|
+
} catch {
|
|
1106
|
+
cleanup();
|
|
1107
|
+
if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aprire il selettore immagini", "Could not open the image picker", "无法打开图片选择器"));
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
const dismissCommandMenu = () => {
|
|
1111
|
+
(boundComposer?.querySelector("button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]"))?.click();
|
|
1112
|
+
};
|
|
1113
|
+
const setMediaActionsDisabled = (disabled, title) => {
|
|
1114
|
+
for (const button of [fileButton, cameraButton]) {
|
|
1115
|
+
if (button.disabled !== disabled) button.disabled = disabled;
|
|
1116
|
+
if (button.title !== title) button.title = title;
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
const quietMediaPointer = (event) => {
|
|
1120
|
+
event.preventDefault();
|
|
1121
|
+
event.stopPropagation();
|
|
1122
|
+
const active = document.activeElement;
|
|
1123
|
+
if (active instanceof HTMLElement && (active.matches("input,textarea") || active.isContentEditable)) active.blur();
|
|
1124
|
+
};
|
|
1125
|
+
const pickImage = (camera) => {
|
|
1126
|
+
if (!canAcceptComposerDrop()) {
|
|
1127
|
+
setMediaActionsDisabled(true, label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
|
|
1128
|
+
dismissCommandMenu();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
const context = mediaRequestContext();
|
|
1132
|
+
dismissCommandMenu();
|
|
1133
|
+
const bridge = window.__DSH_MOBILE_NATIVE__;
|
|
1134
|
+
if (bridge === void 0) {
|
|
1135
|
+
launchBrowserPicker(camera, context);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const action = camera ? "camera.capture" : "files.pick";
|
|
1139
|
+
const input = camera ? {} : { accept: [
|
|
1140
|
+
"image/png",
|
|
1141
|
+
"image/jpeg",
|
|
1142
|
+
"image/webp",
|
|
1143
|
+
"image/gif"
|
|
1144
|
+
] };
|
|
1145
|
+
Promise.resolve().then(() => bridge.invoke(action, input)).then((value) => {
|
|
1146
|
+
if (!mediaRequestIsCurrent(context)) return;
|
|
1147
|
+
if (value instanceof File) deliverImages([value], context);
|
|
1148
|
+
else showMediaToast(label("Il file selezionato non è utilizzabile", "The selected file is unavailable", "所选文件不可用"));
|
|
1149
|
+
}).catch((error) => {
|
|
1150
|
+
if (!mediaRequestIsCurrent(context)) return;
|
|
1151
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
1152
|
+
if (code === "cancelled") return;
|
|
1153
|
+
showMediaToast(code === "payload_too_large" ? label("L’immagine supera il limite di 8 MiB", "The image exceeds the 8 MiB limit", "图片超过 8 MiB 限制") : label("Impossibile aggiungere l’immagine", "Could not attach the image", "无法附加图片"));
|
|
1154
|
+
});
|
|
1155
|
+
};
|
|
1156
|
+
const chooseImage = () => {
|
|
1157
|
+
pickImage(false);
|
|
1158
|
+
};
|
|
1159
|
+
const takePhoto = () => {
|
|
1160
|
+
pickImage(true);
|
|
1161
|
+
};
|
|
1162
|
+
fileButton.addEventListener("pointerdown", quietMediaPointer);
|
|
1163
|
+
cameraButton.addEventListener("pointerdown", quietMediaPointer);
|
|
1164
|
+
fileButton.addEventListener("click", chooseImage);
|
|
1165
|
+
cameraButton.addEventListener("click", takePhoto);
|
|
203
1166
|
const showBranchToast = () => {
|
|
204
1167
|
const header = document.querySelector("[data-dsh-mobile-header]");
|
|
205
1168
|
const title = header === null ? void 0 : header.querySelector("[class*=\"_crumbCurrent\"]")?.textContent?.trim();
|
|
206
|
-
|
|
1169
|
+
const prefix = label("Ramo corrente", "Current branch", "当前分支");
|
|
1170
|
+
branchToast.textContent = title === void 0 ? prefix : `${prefix}: ${title}`;
|
|
207
1171
|
branchToast.dataset.visible = "true";
|
|
208
1172
|
if (branchToastTimer !== 0) window.clearTimeout(branchToastTimer);
|
|
209
1173
|
branchToastTimer = window.setTimeout(() => {
|
|
@@ -213,7 +1177,7 @@ window.__ModuleLoader__.load({
|
|
|
213
1177
|
};
|
|
214
1178
|
const onBranchClick = (event) => {
|
|
215
1179
|
if (!(event.target instanceof Element)) return;
|
|
216
|
-
const branch = event.target.closest("button[aria-label*=\"分支\"],button[aria-label*=\"Branch\"],button[aria-label*=\"branch\"]");
|
|
1180
|
+
const branch = event.target.closest("button[aria-label*=\"分支\"],button[aria-label*=\"Branch\"],button[aria-label*=\"branch\"],button[aria-label*=\"Ramo\"],button[aria-label*=\"ramo\"]");
|
|
217
1181
|
if (branch === null || branch.hasAttribute("disabled") || branch.getAttribute("aria-disabled") === "true") return;
|
|
218
1182
|
window.setTimeout(showBranchToast, 80);
|
|
219
1183
|
};
|
|
@@ -282,6 +1246,15 @@ window.__ModuleLoader__.load({
|
|
|
282
1246
|
});
|
|
283
1247
|
};
|
|
284
1248
|
document.addEventListener("click", animateNavigation);
|
|
1249
|
+
const syncMediaBinding = (composer) => {
|
|
1250
|
+
const previousComposer = boundComposer;
|
|
1251
|
+
boundComposer = composer;
|
|
1252
|
+
const session = currentSessionOrigin();
|
|
1253
|
+
if (composer === previousComposer && session.sessionRoot === boundSessionRoot && session.sessionId === boundSessionId) return;
|
|
1254
|
+
boundSessionRoot = session.sessionRoot;
|
|
1255
|
+
boundSessionId = session.sessionId;
|
|
1256
|
+
mediaRequestGeneration += 1;
|
|
1257
|
+
};
|
|
285
1258
|
const sync = () => {
|
|
286
1259
|
scheduled = 0;
|
|
287
1260
|
frame = firstByClassSuffix(document, "_frame");
|
|
@@ -293,6 +1266,9 @@ window.__ModuleLoader__.load({
|
|
|
293
1266
|
const handle = frame === void 0 ? void 0 : firstByClassSuffix(frame, "_handle");
|
|
294
1267
|
if (center === void 0) {
|
|
295
1268
|
bindHistoryScroller(void 0);
|
|
1269
|
+
syncMediaBinding(null);
|
|
1270
|
+
setMediaActionsDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
|
|
1271
|
+
mediaActions.remove();
|
|
296
1272
|
return;
|
|
297
1273
|
}
|
|
298
1274
|
if (center !== void 0) {
|
|
@@ -326,16 +1302,34 @@ window.__ModuleLoader__.load({
|
|
|
326
1302
|
table.before(wrapper);
|
|
327
1303
|
wrapper.append(table);
|
|
328
1304
|
}
|
|
329
|
-
const
|
|
1305
|
+
const composerCard = center.querySelector("[data-composer-card]");
|
|
1306
|
+
const composerRow = composerCard?.querySelector(":scope > [data-input-scroll]")?.nextElementSibling;
|
|
1307
|
+
if (!(composerRow instanceof HTMLElement)) {
|
|
1308
|
+
syncMediaBinding(null);
|
|
1309
|
+
setMediaActionsDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
|
|
1310
|
+
mediaActions.remove();
|
|
1311
|
+
}
|
|
330
1312
|
if (composerRow instanceof HTMLElement) {
|
|
331
1313
|
composerRow.dataset.dshMobileComposerRow = "true";
|
|
332
1314
|
const groups = Array.from(composerRow.children).filter((child) => child instanceof HTMLElement);
|
|
333
1315
|
const composerTools = groups[0];
|
|
334
1316
|
const composerTrailing = groups.at(-1);
|
|
335
|
-
if (composerTools !== void 0)
|
|
1317
|
+
if (composerTools !== void 0) {
|
|
1318
|
+
composerTools.dataset.dshMobileComposerTools = "true";
|
|
1319
|
+
syncMediaBinding(composerCard ?? null);
|
|
1320
|
+
const composerInput = composerCard?.querySelector("textarea");
|
|
1321
|
+
const composerEditor = composerCard?.querySelector("[contenteditable=\"true\"],[contenteditable=\"plaintext-only\"]");
|
|
1322
|
+
const composerBusy = composerCard?.getAttribute("aria-busy") === "true" || composerInput?.disabled === true || composerInput?.readOnly === true || composerEditor?.getAttribute("aria-disabled") === "true";
|
|
1323
|
+
const attachmentBlocked = !canAcceptComposerDrop();
|
|
1324
|
+
const mediaDisabled = conversation === null || currentSessionOrigin().sessionId === null || composerBusy || attachmentBlocked;
|
|
1325
|
+
const mediaTitle = composerBusy ? label("Attendi il completamento della risposta", "Wait for the response to finish", "请等待回复完成") : attachmentBlocked ? label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用") : mediaDisabled ? label("Apri prima una sessione", "Open a session first", "请先打开会话") : label("Allega screenshot, immagine o foto", "Attach screenshot, image, or photo", "附加截图、图片或照片");
|
|
1326
|
+
setMediaActionsDisabled(mediaDisabled, mediaTitle);
|
|
1327
|
+
const commandMenu = composerCard?.querySelector("[data-trigger-menu]") ?? null;
|
|
1328
|
+
if (commandMenu !== null && (mediaActions.parentElement !== commandMenu || commandMenu.firstElementChild !== mediaActions)) commandMenu.prepend(mediaActions);
|
|
1329
|
+
}
|
|
336
1330
|
if (composerTrailing !== void 0 && composerTrailing !== composerTools) {
|
|
337
1331
|
composerTrailing.dataset.dshMobileComposerTrailing = "true";
|
|
338
|
-
const modelTrigger = composerTrailing.querySelector("button[aria-label^=\"选择模型\"],button[aria-label^=\"Select model\"]");
|
|
1332
|
+
const modelTrigger = composerTrailing.querySelector("button[aria-label^=\"选择模型\"],button[aria-label^=\"Select model\"],button[aria-label^=\"Seleziona modello\"]");
|
|
339
1333
|
if (modelTrigger !== null) {
|
|
340
1334
|
modelTrigger.dataset.dshMobileComposerModelTrigger = "true";
|
|
341
1335
|
modelTrigger.parentElement?.setAttribute("data-dsh-mobile-composer-model", "true");
|
|
@@ -392,7 +1386,11 @@ window.__ModuleLoader__.load({
|
|
|
392
1386
|
attributeFilter: [
|
|
393
1387
|
"class",
|
|
394
1388
|
"style",
|
|
395
|
-
"disabled"
|
|
1389
|
+
"disabled",
|
|
1390
|
+
"readonly",
|
|
1391
|
+
"aria-busy",
|
|
1392
|
+
"aria-selected",
|
|
1393
|
+
"data-dsh-mobile-session"
|
|
396
1394
|
]
|
|
397
1395
|
});
|
|
398
1396
|
backdrop.addEventListener("click", () => {
|
|
@@ -400,10 +1398,22 @@ window.__ModuleLoader__.load({
|
|
|
400
1398
|
});
|
|
401
1399
|
sync();
|
|
402
1400
|
return () => {
|
|
1401
|
+
disposed = true;
|
|
1402
|
+
mediaRequestGeneration += 1;
|
|
1403
|
+
mediaPickerAbortController.abort();
|
|
1404
|
+
restoreLanguageMarker();
|
|
1405
|
+
for (const cleanup of [...browserPickerCleanups]) cleanup();
|
|
403
1406
|
observer.disconnect();
|
|
404
1407
|
document.removeEventListener("click", onBranchClick, true);
|
|
1408
|
+
fileButton.removeEventListener("pointerdown", quietMediaPointer);
|
|
1409
|
+
cameraButton.removeEventListener("pointerdown", quietMediaPointer);
|
|
1410
|
+
fileButton.removeEventListener("click", chooseImage);
|
|
1411
|
+
cameraButton.removeEventListener("click", takePhoto);
|
|
405
1412
|
if (branchToastTimer !== 0) window.clearTimeout(branchToastTimer);
|
|
1413
|
+
if (mediaToastTimer !== 0) window.clearTimeout(mediaToastTimer);
|
|
406
1414
|
branchToast.remove();
|
|
1415
|
+
mediaToast.remove();
|
|
1416
|
+
mediaActions.remove();
|
|
407
1417
|
if (scheduled !== 0) cancelAnimationFrame(scheduled);
|
|
408
1418
|
if (transitionFrame !== 0) cancelAnimationFrame(transitionFrame);
|
|
409
1419
|
if (transitionRestartFrame !== 0) cancelAnimationFrame(transitionRestartFrame);
|
|
@@ -433,6 +1443,95 @@ window.__ModuleLoader__.load({
|
|
|
433
1443
|
function isLoopbackHost(hostname) {
|
|
434
1444
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
435
1445
|
}
|
|
1446
|
+
function selectMobileControlLocale(documentLanguage = "", navigatorLanguages = []) {
|
|
1447
|
+
for (const value of [documentLanguage, ...navigatorLanguages]) {
|
|
1448
|
+
const language = value.trim().toLowerCase().split(/[-_]/u)[0];
|
|
1449
|
+
if (language === "it" || language === "en" || language === "zh") return language;
|
|
1450
|
+
}
|
|
1451
|
+
return "en";
|
|
1452
|
+
}
|
|
1453
|
+
function selectedMobileControlLocale() {
|
|
1454
|
+
return selectMobileControlLocale(document.documentElement.lang, navigator.languages?.length ? navigator.languages : [navigator.language]);
|
|
1455
|
+
}
|
|
1456
|
+
/** Remount one plugin-owned surface when DSH changes the document language. */
|
|
1457
|
+
function installDshLanguageBoundSurface(install) {
|
|
1458
|
+
let locale = selectedMobileControlLocale();
|
|
1459
|
+
let dispose = install();
|
|
1460
|
+
const observer = new MutationObserver(() => {
|
|
1461
|
+
const next = selectedMobileControlLocale();
|
|
1462
|
+
if (next === locale) return;
|
|
1463
|
+
dispose();
|
|
1464
|
+
locale = next;
|
|
1465
|
+
dispose = install();
|
|
1466
|
+
});
|
|
1467
|
+
observer.observe(document.documentElement, {
|
|
1468
|
+
attributes: true,
|
|
1469
|
+
attributeFilter: ["lang"]
|
|
1470
|
+
});
|
|
1471
|
+
return () => {
|
|
1472
|
+
observer.disconnect();
|
|
1473
|
+
dispose();
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function controlTranslator(locale = selectedMobileControlLocale()) {
|
|
1477
|
+
const messages = MOBILE_CONTROL_MESSAGES;
|
|
1478
|
+
return (key, values = {}) => {
|
|
1479
|
+
return (messages[locale][key] ?? messages.en[key] ?? key).replace(/\{(\w+)\}/gu, (_match, name) => String(values[name] ?? `{${name}}`));
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
function normalizeDiagnosticOverall(value) {
|
|
1483
|
+
return value === "ok" ? "ok" : value === "attention" ? "attention" : "error";
|
|
1484
|
+
}
|
|
1485
|
+
function normalizeDiagnosticStatus(value) {
|
|
1486
|
+
return value === "ok" || value === "warning" || value === "error" || value === "info" ? value : "error";
|
|
1487
|
+
}
|
|
1488
|
+
function diagnosticOverallForChecks(value, statuses) {
|
|
1489
|
+
const normalized = statuses.map(normalizeDiagnosticStatus);
|
|
1490
|
+
const payloadOverall = normalizeDiagnosticOverall(value);
|
|
1491
|
+
if (payloadOverall === "error" || normalized.includes("error")) return "error";
|
|
1492
|
+
if (payloadOverall === "attention" || normalized.includes("warning")) return "attention";
|
|
1493
|
+
return "ok";
|
|
1494
|
+
}
|
|
1495
|
+
function validateDiagnosticChecks(value) {
|
|
1496
|
+
if (!Array.isArray(value)) return {
|
|
1497
|
+
entries: [],
|
|
1498
|
+
malformed: true
|
|
1499
|
+
};
|
|
1500
|
+
const entries = [];
|
|
1501
|
+
let malformed = false;
|
|
1502
|
+
for (const candidate of value) {
|
|
1503
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
|
|
1504
|
+
malformed = true;
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
entries.push(candidate);
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
entries,
|
|
1511
|
+
malformed
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
function diagnosticEntriesForRender(data) {
|
|
1515
|
+
const overallKnown = data.overall === "ok" || data.overall === "attention" || data.overall === "error";
|
|
1516
|
+
const validated = validateDiagnosticChecks(data.checks);
|
|
1517
|
+
const statusesKnown = validated.entries.every((entry) => entry.status === "ok" || entry.status === "warning" || entry.status === "error" || entry.status === "info");
|
|
1518
|
+
if (!overallKnown || validated.malformed || validated.entries.length === 0 || !statusesKnown) throw new TypeError("diagnostics envelope is unavailable");
|
|
1519
|
+
return validated.entries;
|
|
1520
|
+
}
|
|
1521
|
+
function renderDiagnosticPayloadSafely(data, render, onFailure) {
|
|
1522
|
+
try {
|
|
1523
|
+
render(data);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
onFailure(error);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function diagnosticServerCopy(entry) {
|
|
1529
|
+
return {
|
|
1530
|
+
label: typeof entry.label === "string" ? entry.label : "",
|
|
1531
|
+
detail: typeof entry.detail === "string" ? entry.detail : "",
|
|
1532
|
+
action: typeof entry.action === "string" ? entry.action : ""
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
436
1535
|
/**
|
|
437
1536
|
* Match DSH's client-side privilege hint to the authenticated mobile gateway.
|
|
438
1537
|
* The gateway authenticates the paired device and forwards allowed requests to
|
|
@@ -477,7 +1576,7 @@ window.__ModuleLoader__.load({
|
|
|
477
1576
|
if (!response.ok) throw new Error(typeof body.error === "string" ? body.error : `HTTP ${String(response.status)}`);
|
|
478
1577
|
return body;
|
|
479
1578
|
} catch (error) {
|
|
480
|
-
if (controller.signal.aborted && upstreamSignal?.aborted !== true) throw new Error("
|
|
1579
|
+
if (controller.signal.aborted && upstreamSignal?.aborted !== true) throw new Error(controlTranslator()("requestTimeout"));
|
|
481
1580
|
throw error;
|
|
482
1581
|
} finally {
|
|
483
1582
|
clearTimeout(timer);
|
|
@@ -498,37 +1597,47 @@ window.__ModuleLoader__.load({
|
|
|
498
1597
|
return url.toString();
|
|
499
1598
|
}
|
|
500
1599
|
function installControl() {
|
|
1600
|
+
const locale = selectedMobileControlLocale();
|
|
1601
|
+
const localeTag = locale === "it" ? "it-IT" : locale === "zh" ? "zh-CN" : "en-US";
|
|
1602
|
+
const t = controlTranslator(locale);
|
|
1603
|
+
const lifecycle = new AbortController();
|
|
1604
|
+
const controlRequestJson = (path, init, timeoutMs) => requestJson(path, {
|
|
1605
|
+
...init,
|
|
1606
|
+
signal: lifecycle.signal
|
|
1607
|
+
}, timeoutMs);
|
|
501
1608
|
const root = element("div", "dsh-mobile-control");
|
|
1609
|
+
root.lang = locale;
|
|
502
1610
|
const panel = element("section", "dsh-mobile-control__panel");
|
|
503
1611
|
panel.hidden = true;
|
|
504
|
-
panel.
|
|
1612
|
+
panel.lang = locale;
|
|
1613
|
+
panel.setAttribute("aria-label", t("mobileAccess"));
|
|
505
1614
|
const header = element("header", "dsh-mobile-control__header");
|
|
506
1615
|
const title = element("h2");
|
|
507
|
-
title.textContent = "
|
|
1616
|
+
title.textContent = t("mobileAccess");
|
|
508
1617
|
const headerActions = element("div", "dsh-mobile-control__header-actions");
|
|
509
1618
|
const diagnosticsEntry = element("button", "dsh-mobile-control__diagnostic-entry");
|
|
510
1619
|
diagnosticsEntry.type = "button";
|
|
511
|
-
diagnosticsEntry.textContent = "
|
|
512
|
-
diagnosticsEntry.setAttribute("aria-label", "
|
|
1620
|
+
diagnosticsEntry.textContent = t("diagnostics");
|
|
1621
|
+
diagnosticsEntry.setAttribute("aria-label", t("openDiagnostics"));
|
|
513
1622
|
diagnosticsEntry.setAttribute("aria-pressed", "false");
|
|
514
1623
|
const close = element("button", "dsh-mobile-control__close");
|
|
515
1624
|
close.type = "button";
|
|
516
1625
|
close.textContent = "×";
|
|
517
|
-
close.setAttribute("aria-label", "
|
|
1626
|
+
close.setAttribute("aria-label", t("collapseMobileAccess"));
|
|
518
1627
|
headerActions.append(diagnosticsEntry, close);
|
|
519
1628
|
const appDownload = element("a", "dsh-mobile-control__app-download");
|
|
520
1629
|
appDownload.href = "https://github.com/saya-ch/dsh-mobile/releases/latest";
|
|
521
1630
|
appDownload.target = "_blank";
|
|
522
1631
|
appDownload.rel = "noopener noreferrer";
|
|
523
|
-
appDownload.textContent = "
|
|
524
|
-
appDownload.setAttribute("aria-label", "
|
|
1632
|
+
appDownload.textContent = t("downloadAndroid");
|
|
1633
|
+
appDownload.setAttribute("aria-label", t("downloadAndroidAria"));
|
|
525
1634
|
const switcher = element("div", "dsh-mobile-control__switcher");
|
|
526
1635
|
const lanTab = element("button", "dsh-mobile-control__tab is-active");
|
|
527
1636
|
lanTab.type = "button";
|
|
528
|
-
lanTab.textContent = "
|
|
1637
|
+
lanTab.textContent = t("lan");
|
|
529
1638
|
const remoteTab = element("button", "dsh-mobile-control__tab");
|
|
530
1639
|
remoteTab.type = "button";
|
|
531
|
-
remoteTab.textContent = "
|
|
1640
|
+
remoteTab.textContent = t("remote");
|
|
532
1641
|
lanTab.setAttribute("aria-pressed", "true");
|
|
533
1642
|
remoteTab.setAttribute("aria-pressed", "false");
|
|
534
1643
|
switcher.append(lanTab, remoteTab);
|
|
@@ -536,7 +1645,7 @@ window.__ModuleLoader__.load({
|
|
|
536
1645
|
const access = element("div", "dsh-mobile-control__access");
|
|
537
1646
|
access.hidden = true;
|
|
538
1647
|
const accessLabel = element("span", "dsh-mobile-control__access-label");
|
|
539
|
-
accessLabel.textContent = "
|
|
1648
|
+
accessLabel.textContent = t("browserAccess");
|
|
540
1649
|
const accessLink = element("a", "dsh-mobile-control__access-link");
|
|
541
1650
|
accessLink.target = "_blank";
|
|
542
1651
|
accessLink.rel = "noreferrer";
|
|
@@ -544,7 +1653,7 @@ window.__ModuleLoader__.load({
|
|
|
544
1653
|
const qrBox = element("div", "dsh-mobile-control__qr");
|
|
545
1654
|
qrBox.hidden = true;
|
|
546
1655
|
const status = element("p", "dsh-mobile-control__status");
|
|
547
|
-
status.textContent = "
|
|
1656
|
+
status.textContent = t("loadingStatus");
|
|
548
1657
|
const extensionStatus = element("p", "dsh-mobile-control__extensions");
|
|
549
1658
|
extensionStatus.hidden = true;
|
|
550
1659
|
const actions = element("div", "dsh-mobile-control__actions");
|
|
@@ -552,31 +1661,31 @@ window.__ModuleLoader__.load({
|
|
|
552
1661
|
toggle.type = "button";
|
|
553
1662
|
const pair = element("button", "dsh-mobile-control__primary");
|
|
554
1663
|
pair.type = "button";
|
|
555
|
-
pair.textContent = "
|
|
1664
|
+
pair.textContent = t("generateCopyKey");
|
|
556
1665
|
const linkPair = element("button", "dsh-mobile-control__secondary");
|
|
557
1666
|
linkPair.type = "button";
|
|
558
|
-
linkPair.textContent = "
|
|
1667
|
+
linkPair.textContent = t("copyPairLink");
|
|
559
1668
|
const manageRow = element("div", "dsh-mobile-control__manage-row");
|
|
560
1669
|
const manageDevices = element("button", "dsh-mobile-control__manage");
|
|
561
1670
|
manageDevices.type = "button";
|
|
562
|
-
manageDevices.textContent = "
|
|
1671
|
+
manageDevices.textContent = t("managePairedDevices");
|
|
563
1672
|
const resetAll = element("button", "dsh-mobile-control__manage");
|
|
564
1673
|
resetAll.type = "button";
|
|
565
|
-
resetAll.textContent = "
|
|
1674
|
+
resetAll.textContent = t("clearAllDevices");
|
|
566
1675
|
manageRow.append(manageDevices, resetAll);
|
|
567
1676
|
const devicePanel = element("div", "dsh-mobile-control__devices");
|
|
568
1677
|
devicePanel.hidden = true;
|
|
569
1678
|
const remoteView = element("div", "dsh-mobile-control__view is-remote");
|
|
570
1679
|
remoteView.hidden = true;
|
|
571
1680
|
const remoteIntro = element("p", "dsh-mobile-control__intro");
|
|
572
|
-
remoteIntro.textContent = "
|
|
1681
|
+
remoteIntro.textContent = t("remoteIntro");
|
|
573
1682
|
const providerSection = element("section", "dsh-mobile-control__provider-section");
|
|
574
1683
|
const providerHeading = element("h3", "dsh-mobile-control__section-title");
|
|
575
|
-
providerHeading.textContent = "
|
|
1684
|
+
providerHeading.textContent = t("chooseProvider");
|
|
576
1685
|
const providerInfo = element("div", "dsh-mobile-control__provider-info");
|
|
577
1686
|
const providerInfoButton = element("button", "dsh-mobile-control__provider-info-button");
|
|
578
1687
|
providerInfoButton.type = "button";
|
|
579
|
-
providerInfoButton.setAttribute("aria-label", "
|
|
1688
|
+
providerInfoButton.setAttribute("aria-label", t("providerInfoAria"));
|
|
580
1689
|
providerInfoButton.setAttribute("aria-expanded", "false");
|
|
581
1690
|
providerInfoButton.setAttribute("aria-controls", "dsh-mobile-provider-info");
|
|
582
1691
|
providerInfoButton.setAttribute("aria-describedby", "dsh-mobile-provider-info");
|
|
@@ -588,15 +1697,15 @@ window.__ModuleLoader__.load({
|
|
|
588
1697
|
providerInfoPopover.setAttribute("role", "tooltip");
|
|
589
1698
|
providerInfoPopover.hidden = true;
|
|
590
1699
|
const providerInfoTitle = element("strong");
|
|
591
|
-
providerInfoTitle.textContent = "
|
|
1700
|
+
providerInfoTitle.textContent = t("providerSafeTitle");
|
|
592
1701
|
const providerInfoText = element("span");
|
|
593
|
-
providerInfoText.textContent = "
|
|
1702
|
+
providerInfoText.textContent = t("providerSafeText");
|
|
594
1703
|
providerInfoButton.append(providerInfoGlyph);
|
|
595
1704
|
providerInfoPopover.append(providerInfoTitle, providerInfoText);
|
|
596
1705
|
providerInfo.append(providerInfoButton, providerInfoPopover);
|
|
597
1706
|
const providerChoices = element("div", "dsh-mobile-control__provider-choices");
|
|
598
1707
|
providerChoices.setAttribute("role", "radiogroup");
|
|
599
|
-
providerChoices.setAttribute("aria-label", "
|
|
1708
|
+
providerChoices.setAttribute("aria-label", t("providerGroupAria"));
|
|
600
1709
|
const tailscaleChoice = element("button", "dsh-mobile-control__provider");
|
|
601
1710
|
tailscaleChoice.type = "button";
|
|
602
1711
|
tailscaleChoice.setAttribute("role", "radio");
|
|
@@ -605,9 +1714,9 @@ window.__ModuleLoader__.load({
|
|
|
605
1714
|
const tailscaleChoiceName = element("strong");
|
|
606
1715
|
tailscaleChoiceName.textContent = "Tailscale Funnel";
|
|
607
1716
|
const tailscaleChoiceBadge = element("span", "dsh-mobile-control__provider-badge");
|
|
608
|
-
tailscaleChoiceBadge.textContent = "
|
|
1717
|
+
tailscaleChoiceBadge.textContent = t("builtIn");
|
|
609
1718
|
const tailscaleChoiceDescription = element("span", "dsh-mobile-control__provider-description");
|
|
610
|
-
tailscaleChoiceDescription.textContent = "
|
|
1719
|
+
tailscaleChoiceDescription.textContent = t("tailscaleDescription");
|
|
611
1720
|
tailscaleChoiceTop.append(tailscaleChoiceName, tailscaleChoiceBadge);
|
|
612
1721
|
tailscaleChoice.append(tailscaleChoiceTop, tailscaleChoiceDescription);
|
|
613
1722
|
const cpolarChoice = element("button", "dsh-mobile-control__provider");
|
|
@@ -618,9 +1727,9 @@ window.__ModuleLoader__.load({
|
|
|
618
1727
|
const cpolarChoiceName = element("strong");
|
|
619
1728
|
cpolarChoiceName.textContent = "cpolar";
|
|
620
1729
|
const cpolarChoiceBadge = element("span", "dsh-mobile-control__provider-badge is-cpolar");
|
|
621
|
-
cpolarChoiceBadge.textContent = "
|
|
1730
|
+
cpolarChoiceBadge.textContent = t("mainlandPreferred");
|
|
622
1731
|
const cpolarChoiceDescription = element("span", "dsh-mobile-control__provider-description");
|
|
623
|
-
cpolarChoiceDescription.textContent = "
|
|
1732
|
+
cpolarChoiceDescription.textContent = t("cpolarDescription");
|
|
624
1733
|
cpolarChoiceTop.append(cpolarChoiceName, cpolarChoiceBadge);
|
|
625
1734
|
cpolarChoice.append(cpolarChoiceTop, cpolarChoiceDescription);
|
|
626
1735
|
providerChoices.append(cpolarChoice, tailscaleChoice);
|
|
@@ -628,27 +1737,27 @@ window.__ModuleLoader__.load({
|
|
|
628
1737
|
const cpolarSetup = element("section", "dsh-mobile-control__cpolar-setup");
|
|
629
1738
|
cpolarSetup.hidden = true;
|
|
630
1739
|
const cpolarSetupTitle = element("h3", "dsh-mobile-control__section-title");
|
|
631
|
-
cpolarSetupTitle.textContent = "
|
|
1740
|
+
cpolarSetupTitle.textContent = t("prepareCpolar");
|
|
632
1741
|
const cpolarComponentStatus = element("p", "dsh-mobile-control__component-status");
|
|
633
|
-
cpolarComponentStatus.textContent = "
|
|
1742
|
+
cpolarComponentStatus.textContent = t("checkingComponent");
|
|
634
1743
|
const cpolarInstall = element("button", "dsh-mobile-control__primary");
|
|
635
1744
|
cpolarInstall.type = "button";
|
|
636
|
-
cpolarInstall.textContent = "
|
|
1745
|
+
cpolarInstall.textContent = t("installOfficial");
|
|
637
1746
|
const cpolarAccount = element("div", "dsh-mobile-control__cpolar-account");
|
|
638
1747
|
cpolarAccount.hidden = true;
|
|
639
1748
|
const cpolarAccountText = element("p", "dsh-mobile-control__component-note");
|
|
640
|
-
cpolarAccountText.textContent = "
|
|
1749
|
+
cpolarAccountText.textContent = t("cpolarAccountNote");
|
|
641
1750
|
const cpolarAccountLinks = element("div", "dsh-mobile-control__link-row");
|
|
642
1751
|
const cpolarSignup = element("a", "dsh-mobile-control__text-link");
|
|
643
1752
|
cpolarSignup.href = "https://dashboard.cpolar.com/signup";
|
|
644
1753
|
cpolarSignup.target = "_blank";
|
|
645
1754
|
cpolarSignup.rel = "noopener noreferrer";
|
|
646
|
-
cpolarSignup.textContent = "
|
|
1755
|
+
cpolarSignup.textContent = t("registerCpolar");
|
|
647
1756
|
const cpolarDashboard = element("a", "dsh-mobile-control__text-link");
|
|
648
1757
|
cpolarDashboard.href = "https://dashboard.cpolar.com/auth";
|
|
649
1758
|
cpolarDashboard.target = "_blank";
|
|
650
1759
|
cpolarDashboard.rel = "noopener noreferrer";
|
|
651
|
-
cpolarDashboard.textContent = "
|
|
1760
|
+
cpolarDashboard.textContent = t("openDashboard");
|
|
652
1761
|
cpolarAccountLinks.append(cpolarSignup, cpolarDashboard);
|
|
653
1762
|
const cpolarTokenLabel = element("label", "dsh-mobile-control__token-label");
|
|
654
1763
|
cpolarTokenLabel.textContent = "Authtoken";
|
|
@@ -656,48 +1765,48 @@ window.__ModuleLoader__.load({
|
|
|
656
1765
|
cpolarToken.type = "password";
|
|
657
1766
|
cpolarToken.autocomplete = "off";
|
|
658
1767
|
cpolarToken.spellcheck = false;
|
|
659
|
-
cpolarToken.placeholder = "
|
|
1768
|
+
cpolarToken.placeholder = t("tokenPlaceholder");
|
|
660
1769
|
cpolarTokenLabel.append(cpolarToken);
|
|
661
1770
|
const cpolarConfigure = element("button", "dsh-mobile-control__primary dsh-mobile-control__cpolar-connect");
|
|
662
1771
|
cpolarConfigure.type = "button";
|
|
663
|
-
cpolarConfigure.textContent = "
|
|
1772
|
+
cpolarConfigure.textContent = t("saveConnect");
|
|
664
1773
|
cpolarAccount.append(cpolarAccountText, cpolarAccountLinks, cpolarTokenLabel, cpolarConfigure);
|
|
665
1774
|
const cpolarDetails = element("details", "dsh-mobile-control__details");
|
|
666
1775
|
const cpolarDetailsSummary = element("summary");
|
|
667
|
-
cpolarDetailsSummary.textContent = "
|
|
1776
|
+
cpolarDetailsSummary.textContent = t("componentDetails");
|
|
668
1777
|
const cpolarDetailsBody = element("div", "dsh-mobile-control__details-body");
|
|
669
1778
|
const cpolarDetailsText = element("p");
|
|
670
|
-
cpolarDetailsText.textContent = "
|
|
1779
|
+
cpolarDetailsText.textContent = t("componentDetailsText");
|
|
671
1780
|
const cpolarStorage = element("code", "dsh-mobile-control__storage");
|
|
672
|
-
cpolarStorage.textContent = "
|
|
1781
|
+
cpolarStorage.textContent = t("pluginPrivateDirectory");
|
|
673
1782
|
const cpolarOfficial = element("a", "dsh-mobile-control__text-link");
|
|
674
1783
|
cpolarOfficial.href = "https://www.cpolar.com/download";
|
|
675
1784
|
cpolarOfficial.target = "_blank";
|
|
676
1785
|
cpolarOfficial.rel = "noopener noreferrer";
|
|
677
|
-
cpolarOfficial.textContent = "
|
|
1786
|
+
cpolarOfficial.textContent = t("officialDownload");
|
|
678
1787
|
const cpolarTerms = element("a", "dsh-mobile-control__text-link");
|
|
679
1788
|
cpolarTerms.href = "https://www.cpolar.com/tos";
|
|
680
1789
|
cpolarTerms.target = "_blank";
|
|
681
1790
|
cpolarTerms.rel = "noopener noreferrer";
|
|
682
|
-
cpolarTerms.textContent = "
|
|
1791
|
+
cpolarTerms.textContent = t("terms");
|
|
683
1792
|
const cpolarPurge = element("button", "dsh-mobile-control__danger");
|
|
684
1793
|
cpolarPurge.type = "button";
|
|
685
|
-
cpolarPurge.textContent = "
|
|
1794
|
+
cpolarPurge.textContent = t("purgeCpolar");
|
|
686
1795
|
cpolarDetailsBody.append(cpolarDetailsText, cpolarStorage, cpolarOfficial, cpolarTerms, cpolarPurge);
|
|
687
1796
|
cpolarDetails.append(cpolarDetailsSummary, cpolarDetailsBody);
|
|
688
1797
|
cpolarSetup.append(cpolarSetupTitle, cpolarComponentStatus, cpolarInstall, cpolarAccount, cpolarDetails);
|
|
689
1798
|
const tailscaleInfo = element("details", "dsh-mobile-control__details");
|
|
690
1799
|
const tailscaleInfoSummary = element("summary");
|
|
691
|
-
tailscaleInfoSummary.textContent = "
|
|
1800
|
+
tailscaleInfoSummary.textContent = t("tailscaleHelp");
|
|
692
1801
|
const tailscaleInfoBody = element("div", "dsh-mobile-control__details-body");
|
|
693
1802
|
const tailscaleInfoText = element("p");
|
|
694
|
-
tailscaleInfoText.textContent = "
|
|
1803
|
+
tailscaleInfoText.textContent = t("tailscaleHelpText");
|
|
695
1804
|
tailscaleInfoBody.append(tailscaleInfoText);
|
|
696
1805
|
tailscaleInfo.append(tailscaleInfoSummary, tailscaleInfoBody);
|
|
697
1806
|
const remoteAccess = element("div", "dsh-mobile-control__access");
|
|
698
1807
|
remoteAccess.hidden = true;
|
|
699
1808
|
const remoteAccessLabel = element("span", "dsh-mobile-control__access-label");
|
|
700
|
-
remoteAccessLabel.textContent = "
|
|
1809
|
+
remoteAccessLabel.textContent = t("remoteAddress");
|
|
701
1810
|
const remoteAccessLink = element("a", "dsh-mobile-control__access-link");
|
|
702
1811
|
remoteAccessLink.target = "_blank";
|
|
703
1812
|
remoteAccessLink.rel = "noreferrer";
|
|
@@ -705,68 +1814,68 @@ window.__ModuleLoader__.load({
|
|
|
705
1814
|
const remoteQr = element("div", "dsh-mobile-control__qr");
|
|
706
1815
|
remoteQr.hidden = true;
|
|
707
1816
|
const remoteStatus = element("p", "dsh-mobile-control__status");
|
|
708
|
-
remoteStatus.textContent = "
|
|
1817
|
+
remoteStatus.textContent = t("loadingRemoteStatus");
|
|
709
1818
|
remoteStatus.setAttribute("aria-live", "polite");
|
|
710
1819
|
const remoteGuide = element("section", "dsh-mobile-control__guide");
|
|
711
1820
|
remoteGuide.hidden = true;
|
|
712
|
-
remoteGuide.setAttribute("aria-label", "
|
|
1821
|
+
remoteGuide.setAttribute("aria-label", t("funnelGuideAria"));
|
|
713
1822
|
const remoteGuideTitle = element("h3", "dsh-mobile-control__guide-title");
|
|
714
|
-
remoteGuideTitle.textContent = "
|
|
1823
|
+
remoteGuideTitle.textContent = t("funnelGuideTitle");
|
|
715
1824
|
const remoteGuideSummary = element("p", "dsh-mobile-control__guide-summary");
|
|
716
|
-
remoteGuideSummary.textContent = "
|
|
1825
|
+
remoteGuideSummary.textContent = t("funnelGuideSummary");
|
|
717
1826
|
const remoteGuideSteps = element("ol", "dsh-mobile-control__guide-steps");
|
|
718
1827
|
for (const text of [
|
|
719
|
-
"
|
|
720
|
-
"
|
|
721
|
-
"
|
|
1828
|
+
t("funnelStep1"),
|
|
1829
|
+
t("funnelStep2"),
|
|
1830
|
+
t("funnelStep3")
|
|
722
1831
|
]) {
|
|
723
1832
|
const item = element("li");
|
|
724
1833
|
item.textContent = text;
|
|
725
1834
|
remoteGuideSteps.append(item);
|
|
726
1835
|
}
|
|
727
1836
|
const remoteGuideNote = element("p", "dsh-mobile-control__guide-note");
|
|
728
|
-
remoteGuideNote.textContent = "
|
|
1837
|
+
remoteGuideNote.textContent = t("funnelGuideNote");
|
|
729
1838
|
const remoteGuideActions = element("div", "dsh-mobile-control__guide-actions");
|
|
730
1839
|
const remoteSetup = element("button", "dsh-mobile-control__primary");
|
|
731
1840
|
remoteSetup.type = "button";
|
|
732
|
-
remoteSetup.textContent = "
|
|
1841
|
+
remoteSetup.textContent = t("continueFunnel");
|
|
733
1842
|
const remoteSetupRetry = element("button", "dsh-mobile-control__secondary");
|
|
734
1843
|
remoteSetupRetry.type = "button";
|
|
735
|
-
remoteSetupRetry.textContent = "
|
|
1844
|
+
remoteSetupRetry.textContent = t("retryNow");
|
|
736
1845
|
remoteGuideActions.append(remoteSetup, remoteSetupRetry);
|
|
737
1846
|
remoteGuide.append(remoteGuideTitle, remoteGuideSummary, remoteGuideSteps, remoteGuideNote, remoteGuideActions);
|
|
738
1847
|
const remoteActions = element("div", "dsh-mobile-control__actions");
|
|
739
1848
|
const remoteToggle = element("button", "dsh-mobile-control__primary");
|
|
740
1849
|
remoteToggle.type = "button";
|
|
741
|
-
remoteToggle.textContent = "
|
|
1850
|
+
remoteToggle.textContent = t("enableRemote");
|
|
742
1851
|
const remoteLogin = element("button", "dsh-mobile-control__primary");
|
|
743
1852
|
remoteLogin.type = "button";
|
|
744
|
-
remoteLogin.textContent = "
|
|
1853
|
+
remoteLogin.textContent = t("continueLogin");
|
|
745
1854
|
remoteLogin.hidden = true;
|
|
746
1855
|
const remoteReconnect = element("button", "dsh-mobile-control__secondary");
|
|
747
1856
|
remoteReconnect.type = "button";
|
|
748
|
-
remoteReconnect.textContent = "
|
|
1857
|
+
remoteReconnect.textContent = t("reconnect");
|
|
749
1858
|
remoteReconnect.hidden = true;
|
|
750
1859
|
const remotePair = element("button", "dsh-mobile-control__secondary");
|
|
751
1860
|
remotePair.type = "button";
|
|
752
|
-
remotePair.textContent = "
|
|
1861
|
+
remotePair.textContent = t("generateRemoteQr");
|
|
753
1862
|
remotePair.disabled = true;
|
|
754
1863
|
remoteActions.append(remoteToggle, remoteLogin, remoteReconnect, remotePair);
|
|
755
1864
|
const remoteManageRow = element("div", "dsh-mobile-control__manage-row");
|
|
756
1865
|
const remoteDevices = element("button", "dsh-mobile-control__manage");
|
|
757
1866
|
remoteDevices.type = "button";
|
|
758
|
-
remoteDevices.textContent = "
|
|
1867
|
+
remoteDevices.textContent = t("manageRemoteDevices");
|
|
759
1868
|
remoteDevices.disabled = true;
|
|
760
1869
|
const remoteReset = element("button", "dsh-mobile-control__manage");
|
|
761
1870
|
remoteReset.type = "button";
|
|
762
|
-
remoteReset.textContent = "
|
|
1871
|
+
remoteReset.textContent = t("resetRemoteLogin");
|
|
763
1872
|
remoteManageRow.append(remoteDevices, remoteReset);
|
|
764
1873
|
const remoteDevicePanel = element("div", "dsh-mobile-control__devices");
|
|
765
1874
|
remoteDevicePanel.hidden = true;
|
|
766
1875
|
const diagnosticsView = element("div", "dsh-mobile-control__view is-diagnostics");
|
|
767
1876
|
diagnosticsView.hidden = true;
|
|
768
1877
|
const diagnosticsIntro = element("p", "dsh-mobile-control__intro");
|
|
769
|
-
diagnosticsIntro.textContent = "
|
|
1878
|
+
diagnosticsIntro.textContent = t("diagnosticsIntro");
|
|
770
1879
|
const diagnosticsSummary = element("section", "dsh-mobile-control__diagnostic-summary is-idle");
|
|
771
1880
|
diagnosticsSummary.setAttribute("aria-live", "polite");
|
|
772
1881
|
const diagnosticsSummaryMain = element("div", "dsh-mobile-control__diagnostic-summary-main");
|
|
@@ -774,21 +1883,21 @@ window.__ModuleLoader__.load({
|
|
|
774
1883
|
diagnosticsSummaryIcon.setAttribute("aria-hidden", "true");
|
|
775
1884
|
const diagnosticsSummaryBody = element("div", "dsh-mobile-control__diagnostic-summary-body");
|
|
776
1885
|
const diagnosticsSummaryTitle = element("strong");
|
|
777
|
-
diagnosticsSummaryTitle.textContent = "
|
|
1886
|
+
diagnosticsSummaryTitle.textContent = t("diagnosticsNotRun");
|
|
778
1887
|
const diagnosticsSummaryText = element("span");
|
|
779
|
-
diagnosticsSummaryText.textContent = "
|
|
1888
|
+
diagnosticsSummaryText.textContent = t("diagnosticsStartHint");
|
|
780
1889
|
const diagnosticsSummaryMeta = element("span", "dsh-mobile-control__diagnostic-summary-meta");
|
|
781
|
-
diagnosticsSummaryMeta.textContent = "
|
|
1890
|
+
diagnosticsSummaryMeta.textContent = t("diagnosticsIdleMeta");
|
|
782
1891
|
diagnosticsSummaryBody.append(diagnosticsSummaryTitle, diagnosticsSummaryText);
|
|
783
1892
|
diagnosticsSummaryMain.append(diagnosticsSummaryIcon, diagnosticsSummaryBody);
|
|
784
1893
|
diagnosticsSummary.append(diagnosticsSummaryMain, diagnosticsSummaryMeta);
|
|
785
1894
|
const diagnosticsToolbar = element("div", "dsh-mobile-control__diagnostic-toolbar");
|
|
786
1895
|
const diagnosticsRun = element("button", "dsh-mobile-control__primary dsh-mobile-control__diagnostic-run");
|
|
787
1896
|
diagnosticsRun.type = "button";
|
|
788
|
-
diagnosticsRun.textContent = "
|
|
1897
|
+
diagnosticsRun.textContent = t("diagnosticsStart");
|
|
789
1898
|
const diagnosticsCopy = element("button", "dsh-mobile-control__secondary dsh-mobile-control__diagnostic-copy");
|
|
790
1899
|
diagnosticsCopy.type = "button";
|
|
791
|
-
diagnosticsCopy.textContent = "
|
|
1900
|
+
diagnosticsCopy.textContent = t("diagnosticsCopy");
|
|
792
1901
|
diagnosticsCopy.disabled = true;
|
|
793
1902
|
diagnosticsCopy.hidden = true;
|
|
794
1903
|
diagnosticsToolbar.append(diagnosticsRun, diagnosticsCopy);
|
|
@@ -801,7 +1910,7 @@ window.__ModuleLoader__.load({
|
|
|
801
1910
|
const diagnosticsDetails = element("details", "dsh-mobile-control__details dsh-mobile-control__diagnostic-details");
|
|
802
1911
|
diagnosticsDetails.hidden = true;
|
|
803
1912
|
const diagnosticsDetailsSummary = element("summary");
|
|
804
|
-
diagnosticsDetailsSummary.textContent = "
|
|
1913
|
+
diagnosticsDetailsSummary.textContent = t("diagnosticsAdvanced");
|
|
805
1914
|
const diagnosticsReport = element("pre", "dsh-mobile-control__diagnostic-report");
|
|
806
1915
|
diagnosticsDetails.append(diagnosticsDetailsSummary, diagnosticsReport);
|
|
807
1916
|
header.append(title, headerActions);
|
|
@@ -868,11 +1977,11 @@ window.__ModuleLoader__.load({
|
|
|
868
1977
|
lanTab.setAttribute("aria-pressed", String(view === "lan"));
|
|
869
1978
|
remoteTab.setAttribute("aria-pressed", String(view === "remote"));
|
|
870
1979
|
diagnosticsEntry.setAttribute("aria-pressed", String(view === "diagnostics"));
|
|
871
|
-
diagnosticsEntry.textContent = view === "diagnostics" ? "
|
|
872
|
-
diagnosticsEntry.setAttribute("aria-label", view === "diagnostics" ? "
|
|
1980
|
+
diagnosticsEntry.textContent = view === "diagnostics" ? t("back") : t("diagnostics");
|
|
1981
|
+
diagnosticsEntry.setAttribute("aria-label", view === "diagnostics" ? t("backToMobile") : t("openDiagnostics"));
|
|
873
1982
|
appDownload.hidden = view === "diagnostics";
|
|
874
1983
|
switcher.hidden = view === "diagnostics";
|
|
875
|
-
title.textContent = view === "lan" ? "
|
|
1984
|
+
title.textContent = view === "lan" ? t("lanAccess") : view === "remote" ? t("remoteAccess") : t("connectionDiagnostics");
|
|
876
1985
|
};
|
|
877
1986
|
lanTab.addEventListener("click", () => {
|
|
878
1987
|
selectView("lan");
|
|
@@ -893,16 +2002,19 @@ window.__ModuleLoader__.load({
|
|
|
893
2002
|
accessLink.textContent = origin;
|
|
894
2003
|
accessLink.title = origin;
|
|
895
2004
|
status.classList.toggle("is-running", running);
|
|
896
|
-
status.textContent = running ? "
|
|
2005
|
+
status.textContent = running ? t("lanOn") : t("lanOff");
|
|
897
2006
|
const extensionData = data.extensions;
|
|
898
2007
|
if (extensionData !== null && typeof extensionData === "object") {
|
|
899
2008
|
const loaded = typeof extensionData.loaded === "number" ? extensionData.loaded : 0;
|
|
900
2009
|
const failed = typeof extensionData.failed === "number" ? extensionData.failed : 0;
|
|
901
2010
|
extensionStatus.hidden = false;
|
|
902
|
-
extensionStatus.textContent = failed === 0 ?
|
|
2011
|
+
extensionStatus.textContent = failed === 0 ? t("extensionsLoaded", { loaded }) : t("extensionsFailed", {
|
|
2012
|
+
loaded,
|
|
2013
|
+
failed
|
|
2014
|
+
});
|
|
903
2015
|
} else extensionStatus.hidden = true;
|
|
904
2016
|
if (!running) qrBox.hidden = true;
|
|
905
|
-
toggle.textContent = running ? "
|
|
2017
|
+
toggle.textContent = running ? t("disableLan") : t("enableLan");
|
|
906
2018
|
pair.disabled = !running;
|
|
907
2019
|
linkPair.disabled = !running;
|
|
908
2020
|
manageDevices.disabled = !running;
|
|
@@ -915,7 +2027,7 @@ window.__ModuleLoader__.load({
|
|
|
915
2027
|
return;
|
|
916
2028
|
}
|
|
917
2029
|
const image = element("img");
|
|
918
|
-
image.alt = "
|
|
2030
|
+
image.alt = t("pairingQr");
|
|
919
2031
|
image.width = 176;
|
|
920
2032
|
image.height = 176;
|
|
921
2033
|
image.src = `data:image/svg+xml;base64,${btoa(svg)}`;
|
|
@@ -923,25 +2035,28 @@ window.__ModuleLoader__.load({
|
|
|
923
2035
|
target.append(image);
|
|
924
2036
|
};
|
|
925
2037
|
const openPairing = (target) => {
|
|
926
|
-
|
|
2038
|
+
controlRequestJson("/api/mobile-access/lan/pairing/open", {
|
|
927
2039
|
method: "POST",
|
|
928
2040
|
body: "{}"
|
|
929
2041
|
}).then(async (data) => {
|
|
930
2042
|
const value = target === "key" ? typeof data.appKey === "string" ? data.appKey : "" : typeof data.pairUrl === "string" ? data.pairUrl : "";
|
|
931
2043
|
showQr(typeof data.qrSvg === "string" ? data.qrSvg : "");
|
|
932
2044
|
if (value === "") {
|
|
933
|
-
status.textContent = "
|
|
2045
|
+
status.textContent = t("keyGenerationFailed");
|
|
934
2046
|
return;
|
|
935
2047
|
}
|
|
936
2048
|
try {
|
|
937
2049
|
await navigator.clipboard.writeText(value);
|
|
938
|
-
status.textContent = target === "key" ? "
|
|
2050
|
+
status.textContent = target === "key" ? t("keyCopied") : t("linkCopied");
|
|
939
2051
|
} catch {
|
|
940
|
-
status.textContent =
|
|
2052
|
+
status.textContent = t("copySecret", {
|
|
2053
|
+
kind: target === "key" ? t("pairingKey") : t("pairingLink"),
|
|
2054
|
+
value
|
|
2055
|
+
});
|
|
941
2056
|
status.classList.add("is-key");
|
|
942
2057
|
}
|
|
943
2058
|
}, (error) => {
|
|
944
|
-
status.textContent = String(error);
|
|
2059
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
945
2060
|
}).finally(() => {
|
|
946
2061
|
pair.disabled = !running;
|
|
947
2062
|
linkPair.disabled = !running;
|
|
@@ -949,41 +2064,45 @@ window.__ModuleLoader__.load({
|
|
|
949
2064
|
};
|
|
950
2065
|
toggle.addEventListener("click", () => {
|
|
951
2066
|
toggle.disabled = true;
|
|
952
|
-
|
|
2067
|
+
controlRequestJson("/api/mobile-access/lan/control", {
|
|
953
2068
|
method: "POST",
|
|
954
2069
|
body: JSON.stringify({ running: !running })
|
|
955
2070
|
}).then(render, (error) => {
|
|
956
|
-
status.textContent = String(error);
|
|
2071
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
957
2072
|
}).finally(() => {
|
|
958
2073
|
toggle.disabled = false;
|
|
959
2074
|
});
|
|
960
2075
|
});
|
|
961
|
-
const formatTime = (ms) => typeof ms === "number" ? new Date(ms).toLocaleString() : "";
|
|
2076
|
+
const formatTime = (ms) => typeof ms === "number" ? new Date(ms).toLocaleString(localeTag) : "";
|
|
2077
|
+
const formatMegabytes = (bytes) => new Intl.NumberFormat(localeTag, {
|
|
2078
|
+
minimumFractionDigits: 1,
|
|
2079
|
+
maximumFractionDigits: 1
|
|
2080
|
+
}).format(bytes / 1024 / 1024);
|
|
962
2081
|
const renderDevices = (data) => {
|
|
963
2082
|
const devices = Array.isArray(data.devices) ? data.devices : [];
|
|
964
2083
|
devicePanel.replaceChildren();
|
|
965
2084
|
if (devices.length === 0) {
|
|
966
2085
|
const empty = element("p", "dsh-mobile-control__device-empty");
|
|
967
|
-
empty.textContent = "
|
|
2086
|
+
empty.textContent = t("noDevices");
|
|
968
2087
|
devicePanel.append(empty);
|
|
969
2088
|
return;
|
|
970
2089
|
}
|
|
971
2090
|
for (const device of devices) {
|
|
972
2091
|
const row = element("div", "dsh-mobile-control__device");
|
|
973
2092
|
const label = element("span", "dsh-mobile-control__device-label");
|
|
974
|
-
label.textContent = typeof device.label === "string" ? device.label : "
|
|
2093
|
+
label.textContent = typeof device.label === "string" ? device.label : t("device");
|
|
975
2094
|
const meta = element("span", "dsh-mobile-control__device-meta");
|
|
976
|
-
meta.textContent =
|
|
2095
|
+
meta.textContent = t("expires", { time: formatTime(device.expiresAt) });
|
|
977
2096
|
const revoke = element("button", "dsh-mobile-control__device-revoke");
|
|
978
2097
|
revoke.type = "button";
|
|
979
|
-
revoke.textContent = "
|
|
2098
|
+
revoke.textContent = t("revoke");
|
|
980
2099
|
const id = typeof device.id === "string" ? device.id : "";
|
|
981
2100
|
revoke.addEventListener("click", () => {
|
|
982
|
-
|
|
2101
|
+
controlRequestJson("/api/mobile-access/lan/devices/revoke", {
|
|
983
2102
|
method: "POST",
|
|
984
2103
|
body: JSON.stringify({ deviceId: id })
|
|
985
2104
|
}).then(loadDevices, (error) => {
|
|
986
|
-
status.textContent = String(error);
|
|
2105
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
987
2106
|
});
|
|
988
2107
|
});
|
|
989
2108
|
row.append(label, meta, revoke);
|
|
@@ -991,8 +2110,8 @@ window.__ModuleLoader__.load({
|
|
|
991
2110
|
}
|
|
992
2111
|
};
|
|
993
2112
|
const loadDevices = () => {
|
|
994
|
-
|
|
995
|
-
status.textContent = String(error);
|
|
2113
|
+
controlRequestJson("/api/mobile-access/lan/devices").then(renderDevices, (error) => {
|
|
2114
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
996
2115
|
});
|
|
997
2116
|
};
|
|
998
2117
|
manageDevices.addEventListener("click", () => {
|
|
@@ -1001,12 +2120,12 @@ window.__ModuleLoader__.load({
|
|
|
1001
2120
|
if (show) loadDevices();
|
|
1002
2121
|
});
|
|
1003
2122
|
resetAll.addEventListener("click", () => {
|
|
1004
|
-
if (!window.confirm("
|
|
1005
|
-
|
|
2123
|
+
if (!window.confirm(t("confirmResetDevices"))) return;
|
|
2124
|
+
controlRequestJson("/api/mobile-access/lan/devices/reset", {
|
|
1006
2125
|
method: "POST",
|
|
1007
2126
|
body: JSON.stringify({ confirm: true })
|
|
1008
2127
|
}).then(loadDevices, (error) => {
|
|
1009
|
-
status.textContent = String(error);
|
|
2128
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
1010
2129
|
});
|
|
1011
2130
|
});
|
|
1012
2131
|
const renderRemote = (data) => {
|
|
@@ -1021,26 +2140,26 @@ window.__ModuleLoader__.load({
|
|
|
1021
2140
|
cpolarChoice.disabled = remoteProviderBusy;
|
|
1022
2141
|
cpolarSetup.hidden = !cpolar;
|
|
1023
2142
|
tailscaleInfo.hidden = cpolar;
|
|
1024
|
-
remoteReset.textContent = cpolar ? "
|
|
2143
|
+
remoteReset.textContent = cpolar ? t("resetRemoteDevices") : t("resetRemoteLogin");
|
|
1025
2144
|
const providers = data.providers !== null && typeof data.providers === "object" ? data.providers : {};
|
|
1026
2145
|
const cpolarProvider = providers.cpolar !== null && typeof providers.cpolar === "object" ? providers.cpolar : {};
|
|
1027
2146
|
const component = cpolarProvider.component !== null && typeof cpolarProvider.component === "object" ? cpolarProvider.component : {};
|
|
1028
2147
|
cpolarInstalled = component.installed === true;
|
|
1029
2148
|
cpolarConfigured = component.configured === true;
|
|
1030
|
-
cpolarChoiceBadge.textContent = cpolarConfigured ? "
|
|
2149
|
+
cpolarChoiceBadge.textContent = cpolarConfigured ? t("ready") : cpolarInstalled ? t("installed") : t("mainlandPreferred");
|
|
1031
2150
|
const cpolarSupported = component.supported !== false;
|
|
1032
2151
|
const componentVersion = typeof component.version === "string" ? component.version : "";
|
|
1033
2152
|
const componentDownloadBytes = typeof component.downloadBytes === "number" ? component.downloadBytes : 0;
|
|
1034
|
-
const componentStorage = typeof component.storagePath === "string" ? component.storagePath :
|
|
2153
|
+
const componentStorage = typeof component.storagePath === "string" ? component.storagePath : `DSH Mobile ${t("pluginPrivateDirectory")}`;
|
|
1035
2154
|
cpolarStorage.textContent = componentStorage;
|
|
1036
2155
|
cpolarStorage.title = componentStorage;
|
|
1037
2156
|
cpolarInstall.hidden = cpolarInstalled || !cpolarSupported;
|
|
1038
|
-
cpolarInstall.textContent = componentDownloadBytes > 0 ?
|
|
2157
|
+
cpolarInstall.textContent = componentDownloadBytes > 0 ? t("installWithSize", { size: formatMegabytes(componentDownloadBytes) }) : t("installOfficial");
|
|
1039
2158
|
cpolarInstall.disabled = remoteProviderBusy;
|
|
1040
2159
|
cpolarAccount.hidden = !cpolarInstalled || cpolarConfigured;
|
|
1041
2160
|
cpolarConfigure.disabled = remoteProviderBusy;
|
|
1042
2161
|
cpolarPurge.hidden = !cpolarInstalled && !cpolarConfigured;
|
|
1043
|
-
cpolarComponentStatus.textContent = !cpolarSupported ? "
|
|
2162
|
+
cpolarComponentStatus.textContent = !cpolarSupported ? t("cpolarUnsupported") : !cpolarInstalled ? t("cpolarNotInstalled") : !cpolarConfigured ? t("cpolarNeedsToken", { version: componentVersion }) : t("cpolarReady", { version: componentVersion });
|
|
1044
2163
|
const state = typeof data.state === "string" ? data.state : "error";
|
|
1045
2164
|
const errorCode = typeof data.errorCode === "string" ? data.errorCode : "";
|
|
1046
2165
|
const remoteOrigin = typeof data.origin === "string" ? data.origin : "";
|
|
@@ -1059,38 +2178,39 @@ window.__ModuleLoader__.load({
|
|
|
1059
2178
|
remoteAccessLink.title = remoteOrigin;
|
|
1060
2179
|
remoteStatus.classList.toggle("is-running", remoteReady);
|
|
1061
2180
|
const labels = {
|
|
1062
|
-
off: "
|
|
1063
|
-
unavailable: cpolar ? "
|
|
1064
|
-
starting: cpolar ? "
|
|
1065
|
-
"needs-login": "
|
|
1066
|
-
connecting: cpolar ? "
|
|
1067
|
-
ready: "
|
|
1068
|
-
error: "
|
|
2181
|
+
off: t("remoteOff"),
|
|
2182
|
+
unavailable: cpolar ? t("remoteUnavailableCpolar") : t("remoteUnavailableTailscale"),
|
|
2183
|
+
starting: cpolar ? t("remoteStartingCpolar") : t("remoteStartingTailscale"),
|
|
2184
|
+
"needs-login": t("remoteNeedsLogin"),
|
|
2185
|
+
connecting: cpolar ? t("remoteConnectingCpolar") : t("remoteConnectingTailscale"),
|
|
2186
|
+
ready: t("remoteReady"),
|
|
2187
|
+
error: t("remoteError")
|
|
1069
2188
|
};
|
|
1070
|
-
|
|
1071
|
-
funnel_permission_required: "
|
|
1072
|
-
funnel_https_required: "
|
|
1073
|
-
funnel_start_failed: "
|
|
1074
|
-
funnel_start_timeout: "
|
|
1075
|
-
tailscale_dns_missing: "
|
|
1076
|
-
gateway_start_failed: "
|
|
1077
|
-
control_channel_failed: "
|
|
1078
|
-
cpolar_component_missing: "
|
|
1079
|
-
cpolar_component_invalid: "
|
|
1080
|
-
cpolar_config_missing: "
|
|
1081
|
-
cpolar_config_invalid: "
|
|
1082
|
-
cpolar_port_unavailable: "
|
|
1083
|
-
cpolar_launch_failed: "
|
|
1084
|
-
cpolar_start_timeout: "
|
|
1085
|
-
cpolar_stopped: "
|
|
1086
|
-
cpolar_exited: "
|
|
1087
|
-
cpolar_invalid_output: "
|
|
1088
|
-
cpolar_invalid_origin: "
|
|
1089
|
-
}
|
|
2189
|
+
const errorLabels = {
|
|
2190
|
+
funnel_permission_required: t("funnelPermission"),
|
|
2191
|
+
funnel_https_required: t("funnelHttps"),
|
|
2192
|
+
funnel_start_failed: t("funnelStart"),
|
|
2193
|
+
funnel_start_timeout: t("funnelTimeout"),
|
|
2194
|
+
tailscale_dns_missing: t("tailscaleDnsMissing"),
|
|
2195
|
+
gateway_start_failed: t("gatewayStartFailed"),
|
|
2196
|
+
control_channel_failed: t("controlChannelFailed"),
|
|
2197
|
+
cpolar_component_missing: t("cpolarMissing"),
|
|
2198
|
+
cpolar_component_invalid: t("cpolarInvalid"),
|
|
2199
|
+
cpolar_config_missing: t("cpolarConfigMissing"),
|
|
2200
|
+
cpolar_config_invalid: t("cpolarConfigInvalid"),
|
|
2201
|
+
cpolar_port_unavailable: t("cpolarPortUnavailable"),
|
|
2202
|
+
cpolar_launch_failed: t("cpolarLaunchFailed"),
|
|
2203
|
+
cpolar_start_timeout: t("cpolarTimeout"),
|
|
2204
|
+
cpolar_stopped: t("cpolarStopped"),
|
|
2205
|
+
cpolar_exited: t("cpolarExited"),
|
|
2206
|
+
cpolar_invalid_output: t("cpolarOutputInvalid"),
|
|
2207
|
+
cpolar_invalid_origin: t("cpolarOriginInvalid")
|
|
2208
|
+
};
|
|
2209
|
+
remoteStatus.textContent = remoteSetupPending && needsFunnelSetup ? t("setupOpened") : state === "error" ? errorLabels[errorCode] ?? labels.error : labels[state] ?? labels.error;
|
|
1090
2210
|
remoteGuide.hidden = !needsFunnelSetup;
|
|
1091
2211
|
remoteSetup.disabled = remoteSetupUrl === "" || remoteReconnectBusy;
|
|
1092
2212
|
remoteSetupRetry.disabled = remoteReconnectBusy;
|
|
1093
|
-
remoteToggle.textContent = remoteRunning ? "
|
|
2213
|
+
remoteToggle.textContent = remoteRunning ? t("disableRemote") : t("enableRemote");
|
|
1094
2214
|
remoteToggle.disabled = remoteProviderBusy || cpolar && (!cpolarInstalled || !cpolarConfigured);
|
|
1095
2215
|
remoteLogin.hidden = cpolar || state !== "needs-login" || remoteLoginUrl === "";
|
|
1096
2216
|
remoteReconnect.hidden = needsFunnelSetup || state !== "error" && state !== "unavailable" || cpolar && (!cpolarInstalled || !cpolarConfigured);
|
|
@@ -1104,24 +2224,24 @@ window.__ModuleLoader__.load({
|
|
|
1104
2224
|
const loadRemote = () => {
|
|
1105
2225
|
if (remoteLoadInFlight) return;
|
|
1106
2226
|
remoteLoadInFlight = true;
|
|
1107
|
-
|
|
1108
|
-
remoteStatus.textContent = String(error);
|
|
2227
|
+
controlRequestJson("/api/mobile-access/remote/control").then(renderRemote, (error) => {
|
|
2228
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1109
2229
|
}).finally(() => {
|
|
1110
2230
|
remoteLoadInFlight = false;
|
|
1111
2231
|
});
|
|
1112
2232
|
};
|
|
1113
2233
|
const chooseRemoteProvider = (provider) => {
|
|
1114
2234
|
if (remoteProviderBusy || provider === remoteProvider) return;
|
|
1115
|
-
if (remoteRunning && !window.confirm("
|
|
2235
|
+
if (remoteRunning && !window.confirm(t("switchProviderConfirm"))) return;
|
|
1116
2236
|
remoteProviderBusy = true;
|
|
1117
2237
|
tailscaleChoice.disabled = true;
|
|
1118
2238
|
cpolarChoice.disabled = true;
|
|
1119
|
-
remoteStatus.textContent = provider === "cpolar" ? "
|
|
1120
|
-
|
|
2239
|
+
remoteStatus.textContent = provider === "cpolar" ? t("switchingCpolar") : t("switchingTailscale");
|
|
2240
|
+
controlRequestJson("/api/mobile-access/remote/provider", {
|
|
1121
2241
|
method: "POST",
|
|
1122
2242
|
body: JSON.stringify({ provider })
|
|
1123
2243
|
}).then(renderRemote, (error) => {
|
|
1124
|
-
remoteStatus.textContent = String(error);
|
|
2244
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1125
2245
|
}).finally(() => {
|
|
1126
2246
|
remoteProviderBusy = false;
|
|
1127
2247
|
loadRemote();
|
|
@@ -1135,16 +2255,16 @@ window.__ModuleLoader__.load({
|
|
|
1135
2255
|
});
|
|
1136
2256
|
cpolarInstall.addEventListener("click", () => {
|
|
1137
2257
|
if (remoteProviderBusy) return;
|
|
1138
|
-
if (!window.confirm("
|
|
2258
|
+
if (!window.confirm(t("installConfirm"))) return;
|
|
1139
2259
|
remoteProviderBusy = true;
|
|
1140
2260
|
cpolarInstall.disabled = true;
|
|
1141
|
-
cpolarInstall.textContent = "
|
|
1142
|
-
remoteStatus.textContent = "
|
|
1143
|
-
|
|
2261
|
+
cpolarInstall.textContent = t("downloading");
|
|
2262
|
+
remoteStatus.textContent = t("installingCpolar");
|
|
2263
|
+
controlRequestJson("/api/mobile-access/remote/cpolar/component/install", {
|
|
1144
2264
|
method: "POST",
|
|
1145
2265
|
body: JSON.stringify({ confirm: true })
|
|
1146
2266
|
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderRemote, (error) => {
|
|
1147
|
-
remoteStatus.textContent =
|
|
2267
|
+
remoteStatus.textContent = t("installFailed", { error: String(error) });
|
|
1148
2268
|
}).finally(() => {
|
|
1149
2269
|
remoteProviderBusy = false;
|
|
1150
2270
|
loadRemote();
|
|
@@ -1154,44 +2274,44 @@ window.__ModuleLoader__.load({
|
|
|
1154
2274
|
if (remoteProviderBusy) return;
|
|
1155
2275
|
const authtoken = cpolarToken.value.trim();
|
|
1156
2276
|
if (authtoken.length < 20 || /\s/u.test(authtoken)) {
|
|
1157
|
-
remoteStatus.textContent = "
|
|
2277
|
+
remoteStatus.textContent = t("invalidToken");
|
|
1158
2278
|
cpolarToken.focus();
|
|
1159
2279
|
return;
|
|
1160
2280
|
}
|
|
1161
2281
|
remoteProviderBusy = true;
|
|
1162
2282
|
cpolarConfigure.disabled = true;
|
|
1163
2283
|
cpolarConfigure.setAttribute("aria-busy", "true");
|
|
1164
|
-
cpolarConfigure.textContent = "
|
|
1165
|
-
|
|
2284
|
+
cpolarConfigure.textContent = t("saving");
|
|
2285
|
+
controlRequestJson("/api/mobile-access/remote/cpolar/configure", {
|
|
1166
2286
|
method: "POST",
|
|
1167
2287
|
body: JSON.stringify({ authtoken })
|
|
1168
2288
|
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(() => {
|
|
1169
2289
|
cpolarToken.value = "";
|
|
1170
|
-
remoteStatus.textContent = "
|
|
1171
|
-
return
|
|
2290
|
+
remoteStatus.textContent = t("configuredConnecting");
|
|
2291
|
+
return controlRequestJson("/api/mobile-access/remote/control", {
|
|
1172
2292
|
method: "POST",
|
|
1173
2293
|
body: JSON.stringify({ running: true })
|
|
1174
2294
|
});
|
|
1175
2295
|
}).then(renderRemote, (error) => {
|
|
1176
|
-
remoteStatus.textContent =
|
|
2296
|
+
remoteStatus.textContent = t("configureFailed", { error: String(error) });
|
|
1177
2297
|
}).finally(() => {
|
|
1178
2298
|
remoteProviderBusy = false;
|
|
1179
2299
|
cpolarConfigure.setAttribute("aria-busy", "false");
|
|
1180
|
-
cpolarConfigure.textContent = "
|
|
2300
|
+
cpolarConfigure.textContent = t("saveConnect");
|
|
1181
2301
|
loadRemote();
|
|
1182
2302
|
});
|
|
1183
2303
|
});
|
|
1184
2304
|
cpolarPurge.addEventListener("click", () => {
|
|
1185
2305
|
if (remoteProviderBusy) return;
|
|
1186
|
-
if (!window.confirm("
|
|
2306
|
+
if (!window.confirm(t("purgeConfirm"))) return;
|
|
1187
2307
|
remoteProviderBusy = true;
|
|
1188
2308
|
cpolarPurge.disabled = true;
|
|
1189
|
-
remoteStatus.textContent = "
|
|
1190
|
-
|
|
2309
|
+
remoteStatus.textContent = t("purging");
|
|
2310
|
+
controlRequestJson("/api/mobile-access/remote/cpolar/component/purge", {
|
|
1191
2311
|
method: "POST",
|
|
1192
2312
|
body: JSON.stringify({ confirm: true })
|
|
1193
2313
|
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderRemote, (error) => {
|
|
1194
|
-
remoteStatus.textContent =
|
|
2314
|
+
remoteStatus.textContent = t("purgeFailed", { error: String(error) });
|
|
1195
2315
|
}).finally(() => {
|
|
1196
2316
|
remoteProviderBusy = false;
|
|
1197
2317
|
cpolarPurge.disabled = false;
|
|
@@ -1200,11 +2320,11 @@ window.__ModuleLoader__.load({
|
|
|
1200
2320
|
});
|
|
1201
2321
|
remoteToggle.addEventListener("click", () => {
|
|
1202
2322
|
remoteToggle.disabled = true;
|
|
1203
|
-
|
|
2323
|
+
controlRequestJson("/api/mobile-access/remote/control", {
|
|
1204
2324
|
method: "POST",
|
|
1205
2325
|
body: JSON.stringify({ running: !remoteRunning })
|
|
1206
2326
|
}).then(renderRemote, (error) => {
|
|
1207
|
-
remoteStatus.textContent = String(error);
|
|
2327
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1208
2328
|
}).finally(loadRemote);
|
|
1209
2329
|
});
|
|
1210
2330
|
remoteLogin.addEventListener("click", () => {
|
|
@@ -1216,12 +2336,12 @@ window.__ModuleLoader__.load({
|
|
|
1216
2336
|
remoteReconnect.disabled = true;
|
|
1217
2337
|
remoteSetup.disabled = true;
|
|
1218
2338
|
remoteSetupRetry.disabled = true;
|
|
1219
|
-
remoteStatus.textContent = remoteProvider === "cpolar" ? "
|
|
1220
|
-
|
|
2339
|
+
remoteStatus.textContent = remoteProvider === "cpolar" ? t("reconnectingCpolar") : t("reconnectingTailscale");
|
|
2340
|
+
controlRequestJson("/api/mobile-access/remote/reconnect", {
|
|
1221
2341
|
method: "POST",
|
|
1222
2342
|
body: "{}"
|
|
1223
2343
|
}).then(renderRemote, (error) => {
|
|
1224
|
-
remoteStatus.textContent = String(error);
|
|
2344
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1225
2345
|
}).finally(() => {
|
|
1226
2346
|
remoteReconnectBusy = false;
|
|
1227
2347
|
remoteReconnect.disabled = false;
|
|
@@ -1238,7 +2358,7 @@ window.__ModuleLoader__.load({
|
|
|
1238
2358
|
if (remoteSetupUrl === "") return;
|
|
1239
2359
|
remoteSetupPending = true;
|
|
1240
2360
|
remoteSetupOpenedAt = Date.now();
|
|
1241
|
-
remoteStatus.textContent = "
|
|
2361
|
+
remoteStatus.textContent = t("setupOpened");
|
|
1242
2362
|
window.open(remoteSetupUrl, "_blank", "noopener,noreferrer");
|
|
1243
2363
|
});
|
|
1244
2364
|
const retryAfterSetup = () => {
|
|
@@ -1250,7 +2370,7 @@ window.__ModuleLoader__.load({
|
|
|
1250
2370
|
document.addEventListener("visibilitychange", retryAfterSetup);
|
|
1251
2371
|
remotePair.addEventListener("click", () => {
|
|
1252
2372
|
remotePair.disabled = true;
|
|
1253
|
-
|
|
2373
|
+
controlRequestJson("/api/mobile-access/remote/pairing/open", {
|
|
1254
2374
|
method: "POST",
|
|
1255
2375
|
body: "{}"
|
|
1256
2376
|
}).then(async (data) => {
|
|
@@ -1259,9 +2379,9 @@ window.__ModuleLoader__.load({
|
|
|
1259
2379
|
if (pairUrl !== "") try {
|
|
1260
2380
|
await navigator.clipboard.writeText(pairUrl);
|
|
1261
2381
|
} catch {}
|
|
1262
|
-
remoteStatus.textContent = "
|
|
2382
|
+
remoteStatus.textContent = t("remoteQrReady");
|
|
1263
2383
|
}, (error) => {
|
|
1264
|
-
remoteStatus.textContent = String(error);
|
|
2384
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1265
2385
|
}).finally(() => {
|
|
1266
2386
|
remotePair.disabled = !remoteReady;
|
|
1267
2387
|
});
|
|
@@ -1271,26 +2391,26 @@ window.__ModuleLoader__.load({
|
|
|
1271
2391
|
remoteDevicePanel.replaceChildren();
|
|
1272
2392
|
if (devices.length === 0) {
|
|
1273
2393
|
const empty = element("p", "dsh-mobile-control__device-empty");
|
|
1274
|
-
empty.textContent = "
|
|
2394
|
+
empty.textContent = t("noRemoteDevices");
|
|
1275
2395
|
remoteDevicePanel.append(empty);
|
|
1276
2396
|
return;
|
|
1277
2397
|
}
|
|
1278
2398
|
for (const device of devices) {
|
|
1279
2399
|
const row = element("div", "dsh-mobile-control__device");
|
|
1280
2400
|
const label = element("span", "dsh-mobile-control__device-label");
|
|
1281
|
-
label.textContent = typeof device.label === "string" ? device.label : "
|
|
2401
|
+
label.textContent = typeof device.label === "string" ? device.label : t("device");
|
|
1282
2402
|
const meta = element("span", "dsh-mobile-control__device-meta");
|
|
1283
|
-
meta.textContent =
|
|
2403
|
+
meta.textContent = t("expires", { time: formatTime(device.expiresAt) });
|
|
1284
2404
|
const revoke = element("button", "dsh-mobile-control__device-revoke");
|
|
1285
2405
|
revoke.type = "button";
|
|
1286
|
-
revoke.textContent = "
|
|
2406
|
+
revoke.textContent = t("revoke");
|
|
1287
2407
|
const id = typeof device.id === "string" ? device.id : "";
|
|
1288
2408
|
revoke.addEventListener("click", () => {
|
|
1289
|
-
|
|
2409
|
+
controlRequestJson("/api/mobile-access/remote/devices/revoke", {
|
|
1290
2410
|
method: "POST",
|
|
1291
2411
|
body: JSON.stringify({ deviceId: id })
|
|
1292
2412
|
}).then(loadRemoteDevices, (error) => {
|
|
1293
|
-
remoteStatus.textContent = String(error);
|
|
2413
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1294
2414
|
});
|
|
1295
2415
|
});
|
|
1296
2416
|
row.append(label, meta, revoke);
|
|
@@ -1298,8 +2418,8 @@ window.__ModuleLoader__.load({
|
|
|
1298
2418
|
}
|
|
1299
2419
|
};
|
|
1300
2420
|
const loadRemoteDevices = () => {
|
|
1301
|
-
|
|
1302
|
-
remoteStatus.textContent = String(error);
|
|
2421
|
+
controlRequestJson("/api/mobile-access/remote/devices").then(renderRemoteDevices, (error) => {
|
|
2422
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1303
2423
|
});
|
|
1304
2424
|
};
|
|
1305
2425
|
remoteDevices.addEventListener("click", () => {
|
|
@@ -1308,29 +2428,89 @@ window.__ModuleLoader__.load({
|
|
|
1308
2428
|
if (show) loadRemoteDevices();
|
|
1309
2429
|
});
|
|
1310
2430
|
remoteReset.addEventListener("click", () => {
|
|
1311
|
-
const prompt = remoteProvider === "cpolar" ? "
|
|
2431
|
+
const prompt = remoteProvider === "cpolar" ? t("resetCpolarConfirm") : t("resetTailscaleConfirm");
|
|
1312
2432
|
if (!window.confirm(prompt)) return;
|
|
1313
|
-
|
|
2433
|
+
controlRequestJson("/api/mobile-access/remote/reset", {
|
|
1314
2434
|
method: "POST",
|
|
1315
2435
|
body: JSON.stringify({ confirm: true })
|
|
1316
2436
|
}).then(renderRemote, (error) => {
|
|
1317
|
-
remoteStatus.textContent = String(error);
|
|
2437
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1318
2438
|
});
|
|
1319
2439
|
});
|
|
1320
2440
|
const renderDiagnostics = (data) => {
|
|
1321
|
-
const
|
|
2441
|
+
const entries = [...diagnosticEntriesForRender(data)];
|
|
2442
|
+
const overall = diagnosticOverallForChecks(data.overall, entries.map((entry) => entry.status));
|
|
1322
2443
|
diagnosticsSummary.className = `dsh-mobile-control__diagnostic-summary is-${overall}`;
|
|
1323
|
-
diagnosticsSummaryTitle.textContent = overall === "ok" ? "
|
|
1324
|
-
diagnosticsSummaryText.textContent = typeof data.summary === "string" ? data.summary : "
|
|
1325
|
-
const entries = Array.isArray(data.checks) ? data.checks : [];
|
|
2444
|
+
diagnosticsSummaryTitle.textContent = overall === "ok" ? t("diagnosticsComplete") : overall === "attention" ? t("diagnosticsAttention") : t("diagnosticsProblem");
|
|
2445
|
+
diagnosticsSummaryText.textContent = locale === "zh" && data.overall === overall && typeof data.summary === "string" ? data.summary : t("diagnosticsCompleteFallback");
|
|
1326
2446
|
diagnosticsChecks.replaceChildren();
|
|
1327
2447
|
const statusLabels = {
|
|
1328
|
-
ok: "
|
|
1329
|
-
warning: "
|
|
1330
|
-
error: "
|
|
1331
|
-
info: "
|
|
2448
|
+
ok: t("diagnosticStatusOk"),
|
|
2449
|
+
warning: t("diagnosticStatusWarning"),
|
|
2450
|
+
error: t("diagnosticStatusError"),
|
|
2451
|
+
info: t("diagnosticStatusInfo")
|
|
2452
|
+
};
|
|
2453
|
+
const diagnosticLabelKeys = {
|
|
2454
|
+
versions: "diagnosticLabelVersions",
|
|
2455
|
+
network: "diagnosticLabelNetwork",
|
|
2456
|
+
lan: "diagnosticLabelLan",
|
|
2457
|
+
firewall: "diagnosticLabelFirewall",
|
|
2458
|
+
remote: "diagnosticLabelRemote",
|
|
2459
|
+
"phone-network": "diagnosticLabelPhone"
|
|
2460
|
+
};
|
|
2461
|
+
const statusOf = (entry) => normalizeDiagnosticStatus(entry.status);
|
|
2462
|
+
const localizedEntryCopy = (entry) => {
|
|
2463
|
+
const serverCopy = diagnosticServerCopy(entry);
|
|
2464
|
+
const reason = typeof entry.reason === "string" ? entry.reason : "";
|
|
2465
|
+
const templates = DIAGNOSTIC_REASON_MESSAGES[locale][reason];
|
|
2466
|
+
if (templates === void 0) return serverCopy;
|
|
2467
|
+
const facts = entry.facts !== null && typeof entry.facts === "object" ? entry.facts : {};
|
|
2468
|
+
const values = {
|
|
2469
|
+
provider: facts.provider === "tailscale" || facts.provider === "cpolar" ? facts.provider : "",
|
|
2470
|
+
latencyMs: typeof facts.latencyMs === "number" && Number.isFinite(facts.latencyMs) ? new Intl.NumberFormat(localeTag).format(facts.latencyMs) : "",
|
|
2471
|
+
interfaceName: typeof facts.interfaceName === "string" ? facts.interfaceName : "",
|
|
2472
|
+
endpointSuffix: typeof facts.endpointSuffix === "string" ? facts.endpointSuffix : "",
|
|
2473
|
+
controllerCode: typeof facts.controllerCode === "string" ? facts.controllerCode : ""
|
|
2474
|
+
};
|
|
2475
|
+
const interpolate = (template) => template.replace(/\{(\w+)\}/gu, (_match, key) => values[key] ?? "");
|
|
2476
|
+
let detail = interpolate(templates[0]);
|
|
2477
|
+
let action = interpolate(templates[1]);
|
|
2478
|
+
if (reason === "versions-current") {
|
|
2479
|
+
const versions = data.versions !== null && typeof data.versions === "object" ? data.versions : {};
|
|
2480
|
+
if ([
|
|
2481
|
+
versions.plugin,
|
|
2482
|
+
versions.dsh,
|
|
2483
|
+
versions.minimumAndroidApp
|
|
2484
|
+
].every((value) => typeof value === "string")) detail += ` plugin ${String(versions.plugin)}, DSH ${String(versions.dsh)}, Android ${String(versions.minimumAndroidApp)}.`;
|
|
2485
|
+
}
|
|
2486
|
+
if (reason === "remote-controller-error") {
|
|
2487
|
+
const actionKey = {
|
|
2488
|
+
component_missing: "remoteUnavailableTailscale",
|
|
2489
|
+
funnel_permission_required: "funnelPermission",
|
|
2490
|
+
funnel_https_required: "funnelHttps",
|
|
2491
|
+
funnel_start_failed: "funnelStart",
|
|
2492
|
+
funnel_start_timeout: "funnelTimeout",
|
|
2493
|
+
tailscale_dns_missing: "tailscaleDnsMissing",
|
|
2494
|
+
sidecar_launch_failed: "remoteUnavailableTailscale",
|
|
2495
|
+
sidecar_stopped: "controlChannelFailed",
|
|
2496
|
+
sidecar_exited: "controlChannelFailed",
|
|
2497
|
+
control_channel_failed: "controlChannelFailed",
|
|
2498
|
+
cpolar_component_missing: "cpolarMissing",
|
|
2499
|
+
cpolar_component_invalid: "cpolarInvalid",
|
|
2500
|
+
cpolar_config_missing: "cpolarConfigMissing",
|
|
2501
|
+
cpolar_config_invalid: "cpolarConfigInvalid",
|
|
2502
|
+
cpolar_start_timeout: "cpolarTimeout",
|
|
2503
|
+
cpolar_stopped: "cpolarStopped",
|
|
2504
|
+
cpolar_exited: "cpolarExited",
|
|
2505
|
+
gateway_start_failed: "gatewayStartFailed"
|
|
2506
|
+
}[values.controllerCode ?? ""];
|
|
2507
|
+
if (actionKey !== void 0) action = t(actionKey);
|
|
2508
|
+
}
|
|
2509
|
+
return {
|
|
2510
|
+
detail,
|
|
2511
|
+
action
|
|
2512
|
+
};
|
|
1332
2513
|
};
|
|
1333
|
-
const statusOf = (entry) => entry.status === "ok" || entry.status === "warning" || entry.status === "error" ? entry.status : "info";
|
|
1334
2514
|
const appendGroup = (label, groupEntries) => {
|
|
1335
2515
|
if (groupEntries.length === 0) return;
|
|
1336
2516
|
const group = element("section", "dsh-mobile-control__diagnostic-group");
|
|
@@ -1338,7 +2518,7 @@ window.__ModuleLoader__.load({
|
|
|
1338
2518
|
const groupTitle = element("h3");
|
|
1339
2519
|
groupTitle.textContent = label;
|
|
1340
2520
|
const groupCount = element("span");
|
|
1341
|
-
groupCount.textContent =
|
|
2521
|
+
groupCount.textContent = t("diagnosticItems", { count: groupEntries.length });
|
|
1342
2522
|
const list = element("div", "dsh-mobile-control__diagnostic-list");
|
|
1343
2523
|
list.setAttribute("role", "list");
|
|
1344
2524
|
groupHeader.append(groupTitle, groupCount);
|
|
@@ -1350,19 +2530,21 @@ window.__ModuleLoader__.load({
|
|
|
1350
2530
|
marker.setAttribute("aria-hidden", "true");
|
|
1351
2531
|
const rowBody = element("div", "dsh-mobile-control__diagnostic-check-body");
|
|
1352
2532
|
const rowHeader = element("div", "dsh-mobile-control__diagnostic-check-header");
|
|
2533
|
+
const labelKey = typeof entry.id === "string" ? diagnosticLabelKeys[entry.id] : void 0;
|
|
1353
2534
|
const rowTitle = element("strong");
|
|
1354
|
-
rowTitle.textContent = typeof entry.label === "string" ? entry.label : "
|
|
2535
|
+
rowTitle.textContent = locale !== "zh" && labelKey !== void 0 ? t(labelKey) : typeof entry.label === "string" ? entry.label : t("diagnosticCheck");
|
|
1355
2536
|
const badge = element("span", "dsh-mobile-control__diagnostic-badge");
|
|
1356
2537
|
badge.textContent = statusLabels[state];
|
|
2538
|
+
const localizedCopy = localizedEntryCopy(entry);
|
|
1357
2539
|
const detail = element("p");
|
|
1358
|
-
detail.textContent =
|
|
2540
|
+
detail.textContent = localizedCopy.detail;
|
|
1359
2541
|
rowHeader.append(rowTitle, badge);
|
|
1360
2542
|
rowBody.append(rowHeader, detail);
|
|
1361
|
-
if (
|
|
2543
|
+
if (localizedCopy.action !== "") {
|
|
1362
2544
|
const action = element("p", "dsh-mobile-control__diagnostic-action");
|
|
1363
2545
|
const actionLabel = element("span");
|
|
1364
|
-
actionLabel.textContent = "
|
|
1365
|
-
action.append(actionLabel, document.createTextNode(
|
|
2546
|
+
actionLabel.textContent = t("diagnosticAction");
|
|
2547
|
+
action.append(actionLabel, document.createTextNode(localizedCopy.action));
|
|
1366
2548
|
rowBody.append(action);
|
|
1367
2549
|
}
|
|
1368
2550
|
row.append(marker, rowBody);
|
|
@@ -1373,40 +2555,67 @@ window.__ModuleLoader__.load({
|
|
|
1373
2555
|
};
|
|
1374
2556
|
const issues = entries.filter((entry) => statusOf(entry) === "error" || statusOf(entry) === "warning");
|
|
1375
2557
|
const otherChecks = entries.filter((entry) => statusOf(entry) !== "error" && statusOf(entry) !== "warning");
|
|
1376
|
-
appendGroup("
|
|
1377
|
-
appendGroup(issues.length === 0 ? "
|
|
1378
|
-
diagnosticsSummaryMeta.textContent = issues.length === 0 ?
|
|
2558
|
+
appendGroup(t("diagnosticNeedsAction"), issues);
|
|
2559
|
+
appendGroup(issues.length === 0 ? t("diagnosticDetails") : t("diagnosticOther"), otherChecks);
|
|
2560
|
+
diagnosticsSummaryMeta.textContent = issues.length === 0 ? t("diagnosticNoBlockers", { count: entries.length }) : t("diagnosticNeedsCount", {
|
|
2561
|
+
count: entries.length,
|
|
2562
|
+
issues: issues.length
|
|
2563
|
+
});
|
|
1379
2564
|
diagnosticsChecks.hidden = entries.length === 0;
|
|
1380
|
-
|
|
2565
|
+
const reportCopy = LOCALIZED_DIAGNOSTIC_COPY[locale];
|
|
2566
|
+
const generatedAt = typeof data.generatedAt === "number" ? new Date(data.generatedAt).toLocaleString(localeTag) : (/* @__PURE__ */ new Date()).toLocaleString(localeTag);
|
|
2567
|
+
const lines = entries.map((entry) => {
|
|
2568
|
+
const state = statusOf(entry);
|
|
2569
|
+
const labelKey = typeof entry.id === "string" ? diagnosticLabelKeys[entry.id] : void 0;
|
|
2570
|
+
const label = labelKey === void 0 && typeof entry.label === "string" ? entry.label : labelKey === void 0 ? t("diagnosticCheck") : t(labelKey);
|
|
2571
|
+
const localizedCopy = localizedEntryCopy(entry);
|
|
2572
|
+
return `[${statusLabels[state]}] ${label}: ${localizedCopy.detail}${localizedCopy.action === "" ? "" : ` ${localizedCopy.action}`}`;
|
|
2573
|
+
});
|
|
2574
|
+
copiedDiagnosticReport = [
|
|
2575
|
+
reportCopy.reportTitle,
|
|
2576
|
+
`${reportCopy.generated}: ${generatedAt}`,
|
|
2577
|
+
...lines
|
|
2578
|
+
].join("\n");
|
|
1381
2579
|
diagnosticsReport.textContent = copiedDiagnosticReport;
|
|
1382
2580
|
diagnosticsDetails.hidden = copiedDiagnosticReport === "";
|
|
1383
2581
|
diagnosticsCopy.disabled = copiedDiagnosticReport === "";
|
|
1384
2582
|
diagnosticsCopy.hidden = copiedDiagnosticReport === "";
|
|
1385
2583
|
diagnosticsToolbar.classList.toggle("has-report", copiedDiagnosticReport !== "");
|
|
1386
2584
|
};
|
|
2585
|
+
const showDiagnosticsFailure = (error) => {
|
|
2586
|
+
diagnosticsSummary.className = "dsh-mobile-control__diagnostic-summary is-error";
|
|
2587
|
+
diagnosticsSummaryTitle.textContent = t("diagnosticsIncomplete");
|
|
2588
|
+
diagnosticsSummaryText.textContent = t("diagnosticsReadFailed", { error: String(error) });
|
|
2589
|
+
diagnosticsSummaryMeta.textContent = t("diagnosticsUnavailable");
|
|
2590
|
+
diagnosticsChecks.replaceChildren();
|
|
2591
|
+
diagnosticsChecks.hidden = true;
|
|
2592
|
+
copiedDiagnosticReport = "";
|
|
2593
|
+
diagnosticsReport.textContent = "";
|
|
2594
|
+
diagnosticsDetails.hidden = true;
|
|
2595
|
+
diagnosticsCopy.disabled = true;
|
|
2596
|
+
diagnosticsCopy.hidden = true;
|
|
2597
|
+
diagnosticsToolbar.classList.remove("has-report");
|
|
2598
|
+
};
|
|
1387
2599
|
const loadDiagnostics = () => {
|
|
1388
2600
|
if (diagnosticsBusy) return;
|
|
1389
2601
|
diagnosticsBusy = true;
|
|
1390
2602
|
diagnosticsRun.disabled = true;
|
|
1391
2603
|
diagnosticsRun.setAttribute("aria-busy", "true");
|
|
1392
|
-
diagnosticsRun.textContent = "
|
|
2604
|
+
diagnosticsRun.textContent = t("diagnosticsChecking");
|
|
1393
2605
|
diagnosticsSummary.className = "dsh-mobile-control__diagnostic-summary is-running";
|
|
1394
|
-
diagnosticsSummaryTitle.textContent = "
|
|
1395
|
-
diagnosticsSummaryText.textContent = "
|
|
1396
|
-
diagnosticsSummaryMeta.textContent = "
|
|
2606
|
+
diagnosticsSummaryTitle.textContent = t("diagnosticsCheckingTitle");
|
|
2607
|
+
diagnosticsSummaryText.textContent = t("diagnosticsCheckingText");
|
|
2608
|
+
diagnosticsSummaryMeta.textContent = t("diagnosticsRunningMeta");
|
|
1397
2609
|
diagnosticsFeedback.hidden = true;
|
|
1398
2610
|
diagnosticsChecks.classList.add("is-refreshing");
|
|
1399
2611
|
diagnosticsChecks.setAttribute("aria-busy", "true");
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
diagnosticsSummaryText.textContent = `无法读取诊断结果:${String(error)}`;
|
|
1404
|
-
diagnosticsSummaryMeta.textContent = "诊断服务暂不可用 · 请稍后重试";
|
|
1405
|
-
}).finally(() => {
|
|
2612
|
+
controlRequestJson("/api/mobile-access/diagnostics").then((data) => {
|
|
2613
|
+
renderDiagnosticPayloadSafely(data, renderDiagnostics, showDiagnosticsFailure);
|
|
2614
|
+
}, showDiagnosticsFailure).finally(() => {
|
|
1406
2615
|
diagnosticsBusy = false;
|
|
1407
2616
|
diagnosticsRun.disabled = false;
|
|
1408
2617
|
diagnosticsRun.setAttribute("aria-busy", "false");
|
|
1409
|
-
diagnosticsRun.textContent = "
|
|
2618
|
+
diagnosticsRun.textContent = t("diagnosticsRetry");
|
|
1410
2619
|
diagnosticsChecks.classList.remove("is-refreshing");
|
|
1411
2620
|
diagnosticsChecks.setAttribute("aria-busy", "false");
|
|
1412
2621
|
});
|
|
@@ -1423,11 +2632,11 @@ window.__ModuleLoader__.load({
|
|
|
1423
2632
|
diagnosticsCopy.addEventListener("click", () => {
|
|
1424
2633
|
if (copiedDiagnosticReport === "") return;
|
|
1425
2634
|
navigator.clipboard.writeText(copiedDiagnosticReport).then(() => {
|
|
1426
|
-
diagnosticsFeedback.textContent = "
|
|
2635
|
+
diagnosticsFeedback.textContent = t("diagnosticsCopied");
|
|
1427
2636
|
diagnosticsFeedback.hidden = false;
|
|
1428
2637
|
}, () => {
|
|
1429
2638
|
diagnosticsDetails.open = true;
|
|
1430
|
-
diagnosticsFeedback.textContent = "
|
|
2639
|
+
diagnosticsFeedback.textContent = t("diagnosticsCopyManual");
|
|
1431
2640
|
diagnosticsFeedback.hidden = false;
|
|
1432
2641
|
});
|
|
1433
2642
|
});
|
|
@@ -1452,8 +2661,8 @@ window.__ModuleLoader__.load({
|
|
|
1452
2661
|
if (!panel.contains(event.target) && !document.querySelector(".dsh-mobile-control__trigger")?.contains(event.target)) setOpen(false);
|
|
1453
2662
|
};
|
|
1454
2663
|
document.addEventListener("pointerdown", dismiss);
|
|
1455
|
-
|
|
1456
|
-
status.textContent = String(error);
|
|
2664
|
+
controlRequestJson("/api/mobile-access/lan/control").then(render, (error) => {
|
|
2665
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
1457
2666
|
});
|
|
1458
2667
|
loadRemote();
|
|
1459
2668
|
const remotePoll = window.setInterval(() => {
|
|
@@ -1461,6 +2670,7 @@ window.__ModuleLoader__.load({
|
|
|
1461
2670
|
}, 1500);
|
|
1462
2671
|
return {
|
|
1463
2672
|
remove: () => {
|
|
2673
|
+
lifecycle.abort();
|
|
1464
2674
|
window.clearInterval(remotePoll);
|
|
1465
2675
|
window.removeEventListener("focus", retryAfterSetup);
|
|
1466
2676
|
document.removeEventListener("visibilitychange", retryAfterSetup);
|
|
@@ -1489,24 +2699,512 @@ window.__ModuleLoader__.load({
|
|
|
1489
2699
|
redirect: "error"
|
|
1490
2700
|
});
|
|
1491
2701
|
}
|
|
2702
|
+
/** Combine extension and caller abort lifetimes and expose deterministic listener cleanup. */
|
|
2703
|
+
function combineClientSignalLifetime(first, second) {
|
|
2704
|
+
if (first.aborted || second.aborted) {
|
|
2705
|
+
const aborted = new AbortController();
|
|
2706
|
+
aborted.abort(first.aborted ? first.reason : second.reason);
|
|
2707
|
+
return {
|
|
2708
|
+
signal: aborted.signal,
|
|
2709
|
+
cleanup: () => void 0
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
const controller = new AbortController();
|
|
2713
|
+
const cleanup = () => {
|
|
2714
|
+
first.removeEventListener("abort", abortFirst);
|
|
2715
|
+
second.removeEventListener("abort", abortSecond);
|
|
2716
|
+
};
|
|
2717
|
+
const abortFirst = () => {
|
|
2718
|
+
cleanup();
|
|
2719
|
+
controller.abort(first.reason);
|
|
2720
|
+
};
|
|
2721
|
+
const abortSecond = () => {
|
|
2722
|
+
cleanup();
|
|
2723
|
+
controller.abort(second.reason);
|
|
2724
|
+
};
|
|
2725
|
+
first.addEventListener("abort", abortFirst, { once: true });
|
|
2726
|
+
second.addEventListener("abort", abortSecond, { once: true });
|
|
2727
|
+
return {
|
|
2728
|
+
signal: controller.signal,
|
|
2729
|
+
cleanup
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
/** Combine extension and caller abort lifetimes on WebViews without AbortSignal.any. */
|
|
2733
|
+
function combineClientSignals(first, second) {
|
|
2734
|
+
return combineClientSignalLifetime(first, second).signal;
|
|
2735
|
+
}
|
|
2736
|
+
/** Keep a combined request lifetime until a streamed response is consumed or cancelled. */
|
|
2737
|
+
function bindClientResponseLifetime(response, cleanup) {
|
|
2738
|
+
if (response.body === null) {
|
|
2739
|
+
cleanup();
|
|
2740
|
+
return response;
|
|
2741
|
+
}
|
|
2742
|
+
const reader = response.body.getReader();
|
|
2743
|
+
let released = false;
|
|
2744
|
+
const release = () => {
|
|
2745
|
+
if (released) return;
|
|
2746
|
+
released = true;
|
|
2747
|
+
cleanup();
|
|
2748
|
+
};
|
|
2749
|
+
const body = new ReadableStream({
|
|
2750
|
+
async pull(controller) {
|
|
2751
|
+
try {
|
|
2752
|
+
const result = await reader.read();
|
|
2753
|
+
if (result.done) {
|
|
2754
|
+
release();
|
|
2755
|
+
controller.close();
|
|
2756
|
+
} else controller.enqueue(result.value);
|
|
2757
|
+
} catch (error) {
|
|
2758
|
+
release();
|
|
2759
|
+
controller.error(error);
|
|
2760
|
+
}
|
|
2761
|
+
},
|
|
2762
|
+
async cancel(reason) {
|
|
2763
|
+
try {
|
|
2764
|
+
await reader.cancel(reason);
|
|
2765
|
+
} finally {
|
|
2766
|
+
release();
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
try {
|
|
2771
|
+
const retained = new Response(body, {
|
|
2772
|
+
headers: response.headers,
|
|
2773
|
+
status: response.status,
|
|
2774
|
+
statusText: response.statusText
|
|
2775
|
+
});
|
|
2776
|
+
Object.defineProperties(retained, {
|
|
2777
|
+
redirected: {
|
|
2778
|
+
configurable: true,
|
|
2779
|
+
value: response.redirected
|
|
2780
|
+
},
|
|
2781
|
+
type: {
|
|
2782
|
+
configurable: true,
|
|
2783
|
+
value: response.type
|
|
2784
|
+
},
|
|
2785
|
+
url: {
|
|
2786
|
+
configurable: true,
|
|
2787
|
+
value: response.url
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
return retained;
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
reader.cancel(error);
|
|
2793
|
+
release();
|
|
2794
|
+
throw error;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
/** Dispose an old UI when its replacement Host generation cannot be activated. */
|
|
2798
|
+
function failClosedExtensionGenerationReplacement(hasActive, activeGeneration, replacementGeneration, dispose) {
|
|
2799
|
+
if (!hasActive || activeGeneration === replacementGeneration) return false;
|
|
2800
|
+
dispose();
|
|
2801
|
+
return true;
|
|
2802
|
+
}
|
|
2803
|
+
/** Resolve one SDK route and prove its normalized path remains in the current extension namespace. */
|
|
2804
|
+
function extensionRouteUrl(id, path, baseUrl) {
|
|
2805
|
+
const rawPathname = path.split(/[?#]/u, 1)[0] ?? "";
|
|
2806
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/u.test(id) || !path.startsWith("/") || path.startsWith("//") || rawPathname.includes("\\") || rawPathname.includes("\0") || /%(?:2f|5c)/iu.test(rawPathname)) throw new TypeError("extension routes must be relative");
|
|
2807
|
+
const origin = new URL(baseUrl).origin;
|
|
2808
|
+
const prefix = `/mobile-access/extensions/${id}/routes`;
|
|
2809
|
+
let target;
|
|
2810
|
+
try {
|
|
2811
|
+
target = new URL(`${prefix}${path}`, origin);
|
|
2812
|
+
} catch {
|
|
2813
|
+
throw new TypeError("extension routes must be relative");
|
|
2814
|
+
}
|
|
2815
|
+
let decodedPathname;
|
|
2816
|
+
try {
|
|
2817
|
+
decodedPathname = decodeURIComponent(target.pathname);
|
|
2818
|
+
} catch {
|
|
2819
|
+
throw new TypeError("extension routes must be relative");
|
|
2820
|
+
}
|
|
2821
|
+
if (target.origin !== origin || target.hash !== "" || decodedPathname.includes("\\") || decodedPathname !== prefix && !decodedPathname.startsWith(`${prefix}/`)) throw new TypeError("extension routes must be relative");
|
|
2822
|
+
if (decodedPathname.slice(prefix.length).split("/").some((part) => part === "." || part === "..")) throw new TypeError("extension routes must be relative");
|
|
2823
|
+
return target;
|
|
2824
|
+
}
|
|
2825
|
+
/** Resolve one generation-pinned static asset URL in the current extension namespace. */
|
|
2826
|
+
function extensionAssetUrl(id, generation, path, baseUrl) {
|
|
2827
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/u.test(id) || generation !== void 0 && !/^[a-f\d]{64}$/u.test(generation)) throw new TypeError("extension asset path is invalid");
|
|
2828
|
+
const normalized = path.replaceAll("\\", "/");
|
|
2829
|
+
if (normalized.length === 0 || normalized.startsWith("/") || normalized.split("/").some((part) => part === "" || part === "." || part === "..")) throw new TypeError("extension asset path is invalid");
|
|
2830
|
+
const target = new URL(`/mobile-access/extensions/${id}/assets/${normalized.split("/").map(encodeURIComponent).join("/")}`, new URL(baseUrl).origin);
|
|
2831
|
+
if (generation !== void 0) target.searchParams.set("generation", generation);
|
|
2832
|
+
return target;
|
|
2833
|
+
}
|
|
2834
|
+
/** Add the immutable Host generation selected for one activated mobile UI. */
|
|
2835
|
+
function extensionGenerationHeaders(generation, headers) {
|
|
2836
|
+
const result = new Headers(headers);
|
|
2837
|
+
if (generation !== void 0) result.set("x-dsh-mobile-extension-generation", generation);
|
|
2838
|
+
return result;
|
|
2839
|
+
}
|
|
2840
|
+
function registerUniqueDisposable(entries, claimedIds, id, mount) {
|
|
2841
|
+
if (claimedIds.has(id)) throw new Error(`duplicate lifecycle id: ${id}`);
|
|
2842
|
+
claimedIds.add(id);
|
|
2843
|
+
let mounted;
|
|
2844
|
+
try {
|
|
2845
|
+
mounted = mount();
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
claimedIds.delete(id);
|
|
2848
|
+
throw error;
|
|
2849
|
+
}
|
|
2850
|
+
let disposed = false;
|
|
2851
|
+
const dispose = () => {
|
|
2852
|
+
if (disposed) return;
|
|
2853
|
+
disposed = true;
|
|
2854
|
+
mounted.dispose();
|
|
2855
|
+
};
|
|
2856
|
+
const entry = {
|
|
2857
|
+
...mounted,
|
|
2858
|
+
dispose
|
|
2859
|
+
};
|
|
2860
|
+
entries.set(id, entry);
|
|
2861
|
+
return () => {
|
|
2862
|
+
if (entries.get(id) === entry) {
|
|
2863
|
+
entries.delete(id);
|
|
2864
|
+
claimedIds.delete(id);
|
|
2865
|
+
}
|
|
2866
|
+
dispose();
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
/** Dispose resources omitted by a successfully fetched authoritative manifest. */
|
|
2870
|
+
function reconcileRemovedExtensions(currentIds, seen, dispose) {
|
|
2871
|
+
for (const id of new Set(currentIds)) if (!seen.has(id)) dispose(id);
|
|
2872
|
+
}
|
|
2873
|
+
/** Reconcile managed resources against one validated authoritative id set. */
|
|
2874
|
+
function publishAuthoritativeExtensionIds(authoritativeIds, seen, managedIdSources, dispose) {
|
|
2875
|
+
const current = new Set(authoritativeIds);
|
|
2876
|
+
for (const source of managedIdSources) for (const id of source) current.add(id);
|
|
2877
|
+
reconcileRemovedExtensions(current, seen, dispose);
|
|
2878
|
+
authoritativeIds.clear();
|
|
2879
|
+
for (const id of seen) authoritativeIds.add(id);
|
|
2880
|
+
}
|
|
2881
|
+
function validManifestResourceUrl(value) {
|
|
2882
|
+
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") && !value.includes("://") && !value.split("/").some((part) => part === ".." || part === ".");
|
|
2883
|
+
}
|
|
2884
|
+
/** Validate the manifest before treating it as authoritative state. */
|
|
2885
|
+
function parseMobileExtensionManifest(payload) {
|
|
2886
|
+
if (typeof payload !== "object" || payload === null) return void 0;
|
|
2887
|
+
const candidate = payload;
|
|
2888
|
+
if (candidate.protocol !== 1 || !Array.isArray(candidate.extensions) || typeof candidate.legacy !== "object" || candidate.legacy === null) return void 0;
|
|
2889
|
+
const legacy = candidate.legacy;
|
|
2890
|
+
if (typeof legacy.scriptRevision !== "string" || typeof legacy.styleRevision !== "string") return void 0;
|
|
2891
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2892
|
+
const extensions = [];
|
|
2893
|
+
for (const value of candidate.extensions) {
|
|
2894
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2895
|
+
const entry = value;
|
|
2896
|
+
if (typeof entry.id !== "string" || !/^[a-z][a-z0-9-]{0,63}$/u.test(entry.id) || ids.has(entry.id)) return void 0;
|
|
2897
|
+
if (entry.generation !== void 0 && (typeof entry.generation !== "string" || !/^[a-f\d]{64}$/u.test(entry.generation))) return void 0;
|
|
2898
|
+
if (entry.scriptUrl !== void 0 && !validManifestResourceUrl(entry.scriptUrl)) return void 0;
|
|
2899
|
+
if (entry.styleUrl !== void 0 && !validManifestResourceUrl(entry.styleUrl)) return void 0;
|
|
2900
|
+
if (entry.assetsUrl !== void 0 && !validManifestResourceUrl(entry.assetsUrl)) return void 0;
|
|
2901
|
+
ids.add(entry.id);
|
|
2902
|
+
extensions.push({
|
|
2903
|
+
id: entry.id,
|
|
2904
|
+
...entry.generation === void 0 ? {} : { generation: entry.generation },
|
|
2905
|
+
...entry.scriptUrl === void 0 ? {} : { scriptUrl: entry.scriptUrl },
|
|
2906
|
+
...entry.styleUrl === void 0 ? {} : { styleUrl: entry.styleUrl },
|
|
2907
|
+
...entry.assetsUrl === void 0 ? {} : { assetsUrl: entry.assetsUrl }
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
return {
|
|
2911
|
+
extensions,
|
|
2912
|
+
legacy: {
|
|
2913
|
+
scriptRevision: legacy.scriptRevision,
|
|
2914
|
+
styleRevision: legacy.styleRevision
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
/** A missing manifest is authoritative before optional legacy resources finish refreshing. */
|
|
2919
|
+
async function handleMissingExtensionManifest(clearManifestResources, refreshLegacyResources, signal) {
|
|
2920
|
+
clearManifestResources();
|
|
2921
|
+
const results = await refreshLegacyResources();
|
|
2922
|
+
return !signal.aborted && results.every(Boolean);
|
|
2923
|
+
}
|
|
2924
|
+
function refreshAborted(signal) {
|
|
2925
|
+
return signal.aborted;
|
|
2926
|
+
}
|
|
2927
|
+
/** Run one coalesced refresh cycle, slowing down when the page is hidden. */
|
|
2928
|
+
function startLifecycleRefreshScheduler(refresh, options = {}, runtime = {
|
|
2929
|
+
document,
|
|
2930
|
+
window
|
|
2931
|
+
}) {
|
|
2932
|
+
const visibleIntervalMs = options.visibleIntervalMs ?? 45e3;
|
|
2933
|
+
const hiddenIntervalMs = options.hiddenIntervalMs ?? 3e5;
|
|
2934
|
+
const cycleTimeoutMs = options.cycleTimeoutMs ?? 3e4;
|
|
2935
|
+
let timer;
|
|
2936
|
+
let cycleTimer;
|
|
2937
|
+
let running = false;
|
|
2938
|
+
let queued = false;
|
|
2939
|
+
let disposed = false;
|
|
2940
|
+
let controller;
|
|
2941
|
+
const clearTimer = () => {
|
|
2942
|
+
if (timer === void 0) return;
|
|
2943
|
+
runtime.window.clearTimeout(timer);
|
|
2944
|
+
timer = void 0;
|
|
2945
|
+
};
|
|
2946
|
+
const clearCycleTimer = () => {
|
|
2947
|
+
if (cycleTimer === void 0) return;
|
|
2948
|
+
runtime.window.clearTimeout(cycleTimer);
|
|
2949
|
+
cycleTimer = void 0;
|
|
2950
|
+
};
|
|
2951
|
+
const schedule = () => {
|
|
2952
|
+
if (disposed) return;
|
|
2953
|
+
clearTimer();
|
|
2954
|
+
const delay = runtime.document.visibilityState === "hidden" ? hiddenIntervalMs : visibleIntervalMs;
|
|
2955
|
+
timer = runtime.window.setTimeout(run, delay);
|
|
2956
|
+
};
|
|
2957
|
+
const run = () => {
|
|
2958
|
+
if (disposed) return;
|
|
2959
|
+
clearTimer();
|
|
2960
|
+
if (running) {
|
|
2961
|
+
queued = true;
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
running = true;
|
|
2965
|
+
const current = new AbortController();
|
|
2966
|
+
controller = current;
|
|
2967
|
+
const refreshPromise = Promise.resolve().then(() => refresh(current.signal));
|
|
2968
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
2969
|
+
cycleTimer = runtime.window.setTimeout(() => {
|
|
2970
|
+
cycleTimer = void 0;
|
|
2971
|
+
current.abort(new DOMException("mobile extension refresh timed out", "TimeoutError"));
|
|
2972
|
+
resolve();
|
|
2973
|
+
}, cycleTimeoutMs);
|
|
2974
|
+
});
|
|
2975
|
+
Promise.race([refreshPromise, timeoutPromise]).catch(() => {}).finally(() => {
|
|
2976
|
+
clearCycleTimer();
|
|
2977
|
+
if (controller === current) controller = void 0;
|
|
2978
|
+
running = false;
|
|
2979
|
+
if (disposed) return;
|
|
2980
|
+
if (queued) {
|
|
2981
|
+
queued = false;
|
|
2982
|
+
run();
|
|
2983
|
+
} else schedule();
|
|
2984
|
+
});
|
|
2985
|
+
refreshPromise.catch(() => {});
|
|
2986
|
+
};
|
|
2987
|
+
const onVisibilityChange = () => {
|
|
2988
|
+
if (runtime.document.visibilityState === "hidden") schedule();
|
|
2989
|
+
else run();
|
|
2990
|
+
};
|
|
2991
|
+
runtime.document.addEventListener("visibilitychange", onVisibilityChange);
|
|
2992
|
+
runtime.window.addEventListener("focus", run);
|
|
2993
|
+
runtime.window.addEventListener("online", run);
|
|
2994
|
+
run();
|
|
2995
|
+
const stop = () => {
|
|
2996
|
+
disposed = true;
|
|
2997
|
+
queued = false;
|
|
2998
|
+
controller?.abort();
|
|
2999
|
+
controller = void 0;
|
|
3000
|
+
clearCycleTimer();
|
|
3001
|
+
clearTimer();
|
|
3002
|
+
runtime.document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
3003
|
+
runtime.window.removeEventListener("focus", run);
|
|
3004
|
+
runtime.window.removeEventListener("online", run);
|
|
3005
|
+
};
|
|
3006
|
+
return Object.assign(stop, { refresh: run });
|
|
3007
|
+
}
|
|
3008
|
+
/** Maintain one authenticated same-origin extension event stream with bounded reconnect backoff. */
|
|
3009
|
+
function startExtensionChangeStream(changed, runtime = {
|
|
3010
|
+
window,
|
|
3011
|
+
create: (url) => new EventSource(url, { withCredentials: true })
|
|
3012
|
+
}) {
|
|
3013
|
+
let source;
|
|
3014
|
+
let timer;
|
|
3015
|
+
let disposed = false;
|
|
3016
|
+
let retryMs = 1e3;
|
|
3017
|
+
const clearTimer = () => {
|
|
3018
|
+
if (timer === void 0) return;
|
|
3019
|
+
runtime.window.clearTimeout(timer);
|
|
3020
|
+
timer = void 0;
|
|
3021
|
+
};
|
|
3022
|
+
const connect = () => {
|
|
3023
|
+
if (disposed || source !== void 0) return;
|
|
3024
|
+
clearTimer();
|
|
3025
|
+
const next = runtime.create("/mobile-access/extensions/events");
|
|
3026
|
+
source = next;
|
|
3027
|
+
next.onopen = () => {
|
|
3028
|
+
retryMs = 1e3;
|
|
3029
|
+
};
|
|
3030
|
+
next.addEventListener("extensions-changed", changed);
|
|
3031
|
+
next.onerror = () => {
|
|
3032
|
+
if (source !== next) return;
|
|
3033
|
+
next.close();
|
|
3034
|
+
source = void 0;
|
|
3035
|
+
if (disposed) return;
|
|
3036
|
+
const delay = retryMs;
|
|
3037
|
+
retryMs = Math.min(3e4, retryMs * 2);
|
|
3038
|
+
timer = runtime.window.setTimeout(connect, delay);
|
|
3039
|
+
};
|
|
3040
|
+
};
|
|
3041
|
+
const reconnectNow = () => {
|
|
3042
|
+
if (disposed) return;
|
|
3043
|
+
source?.close();
|
|
3044
|
+
source = void 0;
|
|
3045
|
+
retryMs = 1e3;
|
|
3046
|
+
connect();
|
|
3047
|
+
};
|
|
3048
|
+
runtime.window.addEventListener("online", reconnectNow);
|
|
3049
|
+
connect();
|
|
3050
|
+
return () => {
|
|
3051
|
+
disposed = true;
|
|
3052
|
+
clearTimer();
|
|
3053
|
+
source?.close();
|
|
3054
|
+
source = void 0;
|
|
3055
|
+
runtime.window.removeEventListener("online", reconnectNow);
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
/** Keep at most one activation in flight per id and commit only its latest generation. */
|
|
3059
|
+
var PerIdActivationLifecycle = class {
|
|
3060
|
+
active = /* @__PURE__ */ new Map();
|
|
3061
|
+
pending = /* @__PURE__ */ new Map();
|
|
3062
|
+
generations = /* @__PURE__ */ new Map();
|
|
3063
|
+
disposed = false;
|
|
3064
|
+
hasActive(id) {
|
|
3065
|
+
return this.active.has(id);
|
|
3066
|
+
}
|
|
3067
|
+
getActive(id) {
|
|
3068
|
+
return this.active.get(id)?.value;
|
|
3069
|
+
}
|
|
3070
|
+
pendingCount() {
|
|
3071
|
+
return this.pending.size;
|
|
3072
|
+
}
|
|
3073
|
+
async activate(id, key, cycleSignal, create) {
|
|
3074
|
+
if (this.disposed || cycleSignal?.aborted === true) return false;
|
|
3075
|
+
const existing = this.pending.get(id);
|
|
3076
|
+
if (existing !== void 0) {
|
|
3077
|
+
if (existing.key === key && !existing.controller.signal.aborted) return this.waitFor(existing, cycleSignal);
|
|
3078
|
+
existing.cancel();
|
|
3079
|
+
}
|
|
3080
|
+
const generation = (this.generations.get(id) ?? 0) + 1;
|
|
3081
|
+
this.generations.set(id, generation);
|
|
3082
|
+
const controller = new AbortController();
|
|
3083
|
+
let work;
|
|
3084
|
+
try {
|
|
3085
|
+
work = create(controller, generation);
|
|
3086
|
+
} catch {
|
|
3087
|
+
controller.abort(new DOMException("mobile extension activation failed to start", "AbortError"));
|
|
3088
|
+
return false;
|
|
3089
|
+
}
|
|
3090
|
+
let cancelled = false;
|
|
3091
|
+
let pending = {};
|
|
3092
|
+
const cancel = () => {
|
|
3093
|
+
if (cancelled) return;
|
|
3094
|
+
cancelled = true;
|
|
3095
|
+
if (this.pending.get(id) === pending) this.pending.delete(id);
|
|
3096
|
+
if (this.generations.get(id) === generation) this.generations.set(id, generation + 1);
|
|
3097
|
+
controller.abort(new DOMException("mobile extension activation cancelled", "AbortError"));
|
|
3098
|
+
work.cancel();
|
|
3099
|
+
};
|
|
3100
|
+
const completion = Promise.resolve(work.result).then((value) => {
|
|
3101
|
+
const current = this.pending.get(id);
|
|
3102
|
+
if (!(!this.disposed && current === pending && current.generation === this.generations.get(id) && !controller.signal.aborted)) {
|
|
3103
|
+
try {
|
|
3104
|
+
work.dispose(value);
|
|
3105
|
+
} catch {}
|
|
3106
|
+
return false;
|
|
3107
|
+
}
|
|
3108
|
+
try {
|
|
3109
|
+
work.commit?.(value);
|
|
3110
|
+
} catch {
|
|
3111
|
+
try {
|
|
3112
|
+
work.dispose(value);
|
|
3113
|
+
} catch {}
|
|
3114
|
+
return false;
|
|
3115
|
+
}
|
|
3116
|
+
const previous = this.active.get(id);
|
|
3117
|
+
let valueDisposed = false;
|
|
3118
|
+
const dispose = () => {
|
|
3119
|
+
if (valueDisposed) return;
|
|
3120
|
+
valueDisposed = true;
|
|
3121
|
+
controller.abort(new DOMException("mobile extension deactivated", "AbortError"));
|
|
3122
|
+
try {
|
|
3123
|
+
work.dispose(value);
|
|
3124
|
+
} catch {}
|
|
3125
|
+
};
|
|
3126
|
+
this.active.set(id, {
|
|
3127
|
+
value,
|
|
3128
|
+
dispose
|
|
3129
|
+
});
|
|
3130
|
+
previous?.dispose();
|
|
3131
|
+
return true;
|
|
3132
|
+
}, () => {
|
|
3133
|
+
cancel();
|
|
3134
|
+
return false;
|
|
3135
|
+
}).finally(() => {
|
|
3136
|
+
if (this.pending.get(id) === pending) this.pending.delete(id);
|
|
3137
|
+
});
|
|
3138
|
+
Object.assign(pending, {
|
|
3139
|
+
key,
|
|
3140
|
+
generation,
|
|
3141
|
+
controller,
|
|
3142
|
+
cancel,
|
|
3143
|
+
completion
|
|
3144
|
+
});
|
|
3145
|
+
this.pending.set(id, pending);
|
|
3146
|
+
return this.waitFor(pending, cycleSignal);
|
|
3147
|
+
}
|
|
3148
|
+
remove(id) {
|
|
3149
|
+
this.generations.set(id, (this.generations.get(id) ?? 0) + 1);
|
|
3150
|
+
this.active.get(id)?.dispose();
|
|
3151
|
+
this.active.delete(id);
|
|
3152
|
+
this.pending.get(id)?.cancel();
|
|
3153
|
+
}
|
|
3154
|
+
dispose() {
|
|
3155
|
+
if (this.disposed) return;
|
|
3156
|
+
this.disposed = true;
|
|
3157
|
+
for (const current of this.active.values()) current.dispose();
|
|
3158
|
+
this.active.clear();
|
|
3159
|
+
for (const current of this.pending.values()) current.cancel();
|
|
3160
|
+
}
|
|
3161
|
+
waitFor(pending, signal) {
|
|
3162
|
+
if (signal === void 0) return pending.completion;
|
|
3163
|
+
if (signal.aborted) {
|
|
3164
|
+
pending.cancel();
|
|
3165
|
+
return Promise.resolve(false);
|
|
3166
|
+
}
|
|
3167
|
+
return new Promise((resolve) => {
|
|
3168
|
+
let settled = false;
|
|
3169
|
+
const finish = (value) => {
|
|
3170
|
+
if (settled) return;
|
|
3171
|
+
settled = true;
|
|
3172
|
+
signal.removeEventListener("abort", onAbort);
|
|
3173
|
+
resolve(value);
|
|
3174
|
+
};
|
|
3175
|
+
const onAbort = () => {
|
|
3176
|
+
pending.cancel();
|
|
3177
|
+
finish(false);
|
|
3178
|
+
};
|
|
3179
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3180
|
+
pending.completion.then(finish);
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
};
|
|
1492
3184
|
function installCustomAssets() {
|
|
1493
3185
|
const legacyStyle = element("style");
|
|
1494
3186
|
legacyStyle.dataset.plugin = "dsh-mobile-custom";
|
|
1495
3187
|
document.head.append(legacyStyle);
|
|
3188
|
+
const legacyCssState = {
|
|
3189
|
+
etag: "",
|
|
3190
|
+
modified: ""
|
|
3191
|
+
};
|
|
1496
3192
|
const previous = window.dshMobile;
|
|
1497
3193
|
let legacyMount = queuedLegacyMount;
|
|
1498
3194
|
let legacySource = "";
|
|
1499
3195
|
let legacyRoot;
|
|
1500
3196
|
let legacyDispose;
|
|
1501
3197
|
const definitions = /* @__PURE__ */ new Map();
|
|
1502
|
-
const
|
|
3198
|
+
const activations = new PerIdActivationLifecycle();
|
|
1503
3199
|
const styleNodes = /* @__PURE__ */ new Map();
|
|
1504
3200
|
const styleEtags = /* @__PURE__ */ new Map();
|
|
1505
3201
|
const scriptDigests = /* @__PURE__ */ new Map();
|
|
3202
|
+
const activeHostGenerations = /* @__PURE__ */ new Map();
|
|
3203
|
+
const activationKeys = /* @__PURE__ */ new Map();
|
|
1506
3204
|
const manifestExtensionIds = /* @__PURE__ */ new Set();
|
|
1507
3205
|
const managedDefinitionIds = /* @__PURE__ */ new Set();
|
|
1508
|
-
const activationQueues = /* @__PURE__ */ new Map();
|
|
1509
3206
|
let manifestEtag = "";
|
|
3207
|
+
let disposed = false;
|
|
1510
3208
|
let expectedDefinitionId;
|
|
1511
3209
|
const SURFACE_HOST_STYLES = {
|
|
1512
3210
|
"sidebar-action": "position:fixed;z-index:1100;top:calc(env(safe-area-inset-top) + 8px);left:8px;display:flex;flex-direction:column;gap:6px;pointer-events:none",
|
|
@@ -1553,9 +3251,34 @@ window.__ModuleLoader__.load({
|
|
|
1553
3251
|
return value;
|
|
1554
3252
|
}
|
|
1555
3253
|
};
|
|
1556
|
-
const invokeNative = async (action, input) => {
|
|
3254
|
+
const invokeNative = async (action, input, signal) => {
|
|
3255
|
+
const abortReason = () => signal.reason ?? new DOMException("mobile extension disposed", "AbortError");
|
|
3256
|
+
if (signal.aborted) throw abortReason();
|
|
1557
3257
|
const bridge = window.__DSH_MOBILE_NATIVE__;
|
|
1558
|
-
if (bridge !== void 0) return
|
|
3258
|
+
if (bridge !== void 0) return new Promise((resolve, reject) => {
|
|
3259
|
+
let settled = false;
|
|
3260
|
+
const finish = (callback) => {
|
|
3261
|
+
if (settled) return;
|
|
3262
|
+
settled = true;
|
|
3263
|
+
signal.removeEventListener("abort", onAbort);
|
|
3264
|
+
callback();
|
|
3265
|
+
};
|
|
3266
|
+
const onAbort = () => {
|
|
3267
|
+
finish(() => {
|
|
3268
|
+
reject(abortReason());
|
|
3269
|
+
});
|
|
3270
|
+
};
|
|
3271
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3272
|
+
Promise.resolve().then(() => bridge.invoke(action, input)).then((value) => {
|
|
3273
|
+
finish(() => {
|
|
3274
|
+
resolve(materializeNativeFile(value));
|
|
3275
|
+
});
|
|
3276
|
+
}, (error) => {
|
|
3277
|
+
finish(() => {
|
|
3278
|
+
reject(error);
|
|
3279
|
+
});
|
|
3280
|
+
});
|
|
3281
|
+
});
|
|
1559
3282
|
if (action === "share" && typeof navigator.share === "function") {
|
|
1560
3283
|
await navigator.share(input ?? {});
|
|
1561
3284
|
return { ok: true };
|
|
@@ -1570,65 +3293,156 @@ window.__ModuleLoader__.load({
|
|
|
1570
3293
|
inputElement.type = "file";
|
|
1571
3294
|
inputElement.accept = action === "camera.capture" ? "image/*" : "*/*";
|
|
1572
3295
|
if (action === "camera.capture") inputElement.capture = "environment";
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
3296
|
+
inputElement.hidden = true;
|
|
3297
|
+
return new Promise((resolve, reject) => {
|
|
3298
|
+
let settled = false;
|
|
3299
|
+
let cleaned = false;
|
|
3300
|
+
let cleanupTimer = 0;
|
|
3301
|
+
let watchdogTimer = 0;
|
|
3302
|
+
const cleanup = () => {
|
|
3303
|
+
if (cleaned) return;
|
|
3304
|
+
cleaned = true;
|
|
3305
|
+
if (cleanupTimer !== 0) window.clearTimeout(cleanupTimer);
|
|
3306
|
+
if (watchdogTimer !== 0) window.clearTimeout(watchdogTimer);
|
|
3307
|
+
window.removeEventListener("focus", scheduleCancel);
|
|
3308
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
3309
|
+
signal.removeEventListener("abort", onAbort);
|
|
3310
|
+
inputElement.removeEventListener("change", onChange);
|
|
3311
|
+
inputElement.removeEventListener("cancel", onCancel);
|
|
3312
|
+
inputElement.remove();
|
|
3313
|
+
};
|
|
3314
|
+
const finish = (callback) => {
|
|
3315
|
+
if (settled) return;
|
|
3316
|
+
settled = true;
|
|
3317
|
+
cleanup();
|
|
3318
|
+
callback();
|
|
3319
|
+
};
|
|
3320
|
+
const onChange = () => {
|
|
3321
|
+
const file = inputElement.files?.[0];
|
|
3322
|
+
finish(() => {
|
|
3323
|
+
resolve(file);
|
|
3324
|
+
});
|
|
3325
|
+
};
|
|
3326
|
+
const onCancel = () => {
|
|
3327
|
+
finish(() => {
|
|
3328
|
+
resolve(void 0);
|
|
3329
|
+
});
|
|
3330
|
+
};
|
|
3331
|
+
const onAbort = () => {
|
|
3332
|
+
finish(() => {
|
|
3333
|
+
reject(abortReason());
|
|
3334
|
+
});
|
|
3335
|
+
};
|
|
3336
|
+
const scheduleCancel = () => {
|
|
3337
|
+
if (!settled && cleanupTimer === 0) cleanupTimer = window.setTimeout(onCancel, 1e3);
|
|
3338
|
+
};
|
|
3339
|
+
const onVisibilityChange = () => {
|
|
3340
|
+
if (document.visibilityState === "visible") scheduleCancel();
|
|
3341
|
+
};
|
|
3342
|
+
inputElement.addEventListener("change", onChange, { once: true });
|
|
3343
|
+
inputElement.addEventListener("cancel", onCancel, { once: true });
|
|
3344
|
+
window.addEventListener("focus", scheduleCancel);
|
|
3345
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
3346
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3347
|
+
watchdogTimer = window.setTimeout(onCancel, 3e5);
|
|
3348
|
+
try {
|
|
3349
|
+
document.body.append(inputElement);
|
|
3350
|
+
inputElement.click();
|
|
3351
|
+
} catch (error) {
|
|
3352
|
+
finish(() => {
|
|
3353
|
+
reject(error);
|
|
3354
|
+
});
|
|
3355
|
+
}
|
|
1576
3356
|
});
|
|
1577
3357
|
}
|
|
1578
3358
|
throw new Error("native capability is unavailable");
|
|
1579
3359
|
};
|
|
1580
|
-
const makeApi = (id, controller) => {
|
|
1581
|
-
const
|
|
3360
|
+
const makeApi = (id, hostGeneration, controller, surfaces, surfaceIds) => {
|
|
3361
|
+
const ensureCurrent = () => {
|
|
3362
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
3363
|
+
};
|
|
3364
|
+
const requestSignal = (signal) => signal === void 0 || signal === null ? {
|
|
3365
|
+
signal: controller.signal,
|
|
3366
|
+
cleanup: () => void 0
|
|
3367
|
+
} : combineClientSignalLifetime(controller.signal, signal);
|
|
1582
3368
|
const mountSurface = (surface) => {
|
|
3369
|
+
ensureCurrent();
|
|
1583
3370
|
if (!/^[a-z][a-z0-9-]{0,63}$/u.test(surface.id) || surface.label.length > 120) throw new Error("invalid mobile surface");
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
3371
|
+
return registerUniqueDisposable(surfaces, surfaceIds, surface.id, () => {
|
|
3372
|
+
const container = element("section");
|
|
3373
|
+
container.dataset.dshMobileSurface = surface.id;
|
|
3374
|
+
container.hidden = surface.placement === "page" || surface.placement === "overlay";
|
|
3375
|
+
container.style.cssText = surface.placement === "page" || surface.placement === "overlay" ? "position:absolute;inset:0;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);padding:16px;pointer-events:auto" : "pointer-events:auto";
|
|
3376
|
+
const host = () => surface.placement === "page" || surface.placement === "overlay" ? shellLayer() : surfaceHost(surface.placement) ?? shellLayer();
|
|
3377
|
+
const mounted = surface.mount(container);
|
|
3378
|
+
const dispose = () => {
|
|
3379
|
+
try {
|
|
3380
|
+
if (typeof mounted === "function") mounted();
|
|
3381
|
+
} finally {
|
|
3382
|
+
container.remove();
|
|
3383
|
+
}
|
|
3384
|
+
};
|
|
3385
|
+
return {
|
|
3386
|
+
dispose,
|
|
3387
|
+
container,
|
|
3388
|
+
host
|
|
3389
|
+
};
|
|
1597
3390
|
});
|
|
1598
|
-
return () => {
|
|
1599
|
-
if (surfaces.get(surface.id)?.dispose === dispose) surfaces.delete(surface.id);
|
|
1600
|
-
dispose();
|
|
1601
|
-
};
|
|
1602
3391
|
};
|
|
1603
3392
|
return {
|
|
1604
3393
|
host: {
|
|
1605
|
-
invoke: (action, input) =>
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
3394
|
+
invoke: (action, input) => {
|
|
3395
|
+
ensureCurrent();
|
|
3396
|
+
return mobileRequest(`/mobile-access/extensions/${encodeURIComponent(id)}/actions/${encodeURIComponent(action)}`, {
|
|
3397
|
+
method: "POST",
|
|
3398
|
+
headers: extensionGenerationHeaders(hostGeneration),
|
|
3399
|
+
body: JSON.stringify(input ?? {}),
|
|
3400
|
+
signal: controller.signal
|
|
3401
|
+
}).then(async (response) => {
|
|
3402
|
+
const value = await response.json();
|
|
3403
|
+
if (!response.ok) throw new Error(typeof value === "object" && value !== null && "error" in value ? String(value.error) : `HTTP ${String(response.status)}`);
|
|
3404
|
+
return value;
|
|
3405
|
+
});
|
|
3406
|
+
},
|
|
1613
3407
|
fetch: (path, init) => {
|
|
1614
|
-
|
|
1615
|
-
|
|
3408
|
+
ensureCurrent();
|
|
3409
|
+
const target = extensionRouteUrl(id, path, location.href);
|
|
3410
|
+
const headers = extensionGenerationHeaders(hostGeneration, init?.headers);
|
|
3411
|
+
const lifetime = requestSignal(init?.signal);
|
|
3412
|
+
return mobileRequest(target.href, {
|
|
3413
|
+
...init,
|
|
3414
|
+
headers,
|
|
3415
|
+
signal: lifetime.signal
|
|
3416
|
+
}).then((response) => bindClientResponseLifetime(response, lifetime.cleanup), (error) => {
|
|
3417
|
+
lifetime.cleanup();
|
|
3418
|
+
throw error;
|
|
3419
|
+
});
|
|
3420
|
+
},
|
|
3421
|
+
assetUrl: (path) => {
|
|
3422
|
+
ensureCurrent();
|
|
3423
|
+
return extensionAssetUrl(id, hostGeneration, path, location.href).href;
|
|
1616
3424
|
}
|
|
1617
3425
|
},
|
|
1618
3426
|
ui: {
|
|
1619
3427
|
registerSurface: mountSurface,
|
|
1620
|
-
open: (
|
|
1621
|
-
|
|
3428
|
+
open: (surfaceId) => {
|
|
3429
|
+
ensureCurrent();
|
|
3430
|
+
const entry = surfaces.get(surfaceId);
|
|
1622
3431
|
if (entry !== void 0) entry.container.hidden = false;
|
|
1623
3432
|
},
|
|
1624
|
-
close: (
|
|
1625
|
-
|
|
3433
|
+
close: (surfaceId) => {
|
|
3434
|
+
ensureCurrent();
|
|
3435
|
+
const entry = surfaces.get(surfaceId);
|
|
1626
3436
|
if (entry !== void 0) entry.container.hidden = true;
|
|
1627
3437
|
},
|
|
1628
|
-
toast
|
|
3438
|
+
toast: (message) => {
|
|
3439
|
+
ensureCurrent();
|
|
3440
|
+
toast(message);
|
|
3441
|
+
}
|
|
1629
3442
|
},
|
|
1630
3443
|
native: {
|
|
1631
3444
|
capabilities: async () => {
|
|
3445
|
+
ensureCurrent();
|
|
1632
3446
|
const bridge = window.__DSH_MOBILE_NATIVE__;
|
|
1633
3447
|
return bridge === void 0 ? [
|
|
1634
3448
|
"files.pick",
|
|
@@ -1638,63 +3452,59 @@ window.__ModuleLoader__.load({
|
|
|
1638
3452
|
"clipboard.write"
|
|
1639
3453
|
] : bridge.capabilities();
|
|
1640
3454
|
},
|
|
1641
|
-
invoke: invokeNative
|
|
3455
|
+
invoke: (action, input) => invokeNative(action, input, controller.signal)
|
|
1642
3456
|
},
|
|
1643
3457
|
signal: controller.signal,
|
|
1644
3458
|
document,
|
|
1645
3459
|
window
|
|
1646
3460
|
};
|
|
1647
3461
|
};
|
|
1648
|
-
const
|
|
1649
|
-
const
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
3462
|
+
const activateDefinition = (definition, cycleSignal, commitGeneration, hostGeneration) => {
|
|
3463
|
+
const previousKey = activationKeys.get(definition.id);
|
|
3464
|
+
const key = previousKey?.definition === definition && previousKey.generation === hostGeneration ? previousKey : {
|
|
3465
|
+
definition,
|
|
3466
|
+
...hostGeneration === void 0 ? {} : { generation: hostGeneration }
|
|
3467
|
+
};
|
|
3468
|
+
activationKeys.set(definition.id, key);
|
|
3469
|
+
return activations.activate(definition.id, key, cycleSignal, (controller) => {
|
|
3470
|
+
const surfaces = /* @__PURE__ */ new Map();
|
|
3471
|
+
const surfaceIds = /* @__PURE__ */ new Set();
|
|
3472
|
+
let pendingDisposed = false;
|
|
3473
|
+
const disposePending = () => {
|
|
3474
|
+
if (pendingDisposed) return;
|
|
3475
|
+
pendingDisposed = true;
|
|
3476
|
+
for (const surface of surfaces.values()) try {
|
|
3477
|
+
surface.dispose();
|
|
3478
|
+
} catch {}
|
|
1658
3479
|
surfaces.clear();
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
3480
|
+
surfaceIds.clear();
|
|
3481
|
+
};
|
|
3482
|
+
return {
|
|
3483
|
+
result: Promise.resolve().then(() => definition.activate(makeApi(definition.id, hostGeneration, controller, surfaces, surfaceIds))).then((cleanup) => ({
|
|
3484
|
+
controller,
|
|
3485
|
+
surfaces,
|
|
3486
|
+
...typeof cleanup === "function" ? { cleanup } : {}
|
|
3487
|
+
})),
|
|
3488
|
+
cancel: disposePending,
|
|
3489
|
+
commit: (value) => {
|
|
3490
|
+
if (definitions.get(definition.id) !== definition || controller.signal.aborted) throw new Error("stale mobile extension activation");
|
|
3491
|
+
for (const surface of value.surfaces.values()) surface.host().append(surface.container);
|
|
3492
|
+
activeHostGenerations.set(definition.id, hostGeneration);
|
|
3493
|
+
commitGeneration?.();
|
|
3494
|
+
},
|
|
3495
|
+
dispose: (value) => {
|
|
3496
|
+
controller.abort(new DOMException("mobile extension disposed", "AbortError"));
|
|
3497
|
+
try {
|
|
3498
|
+
value.cleanup?.();
|
|
3499
|
+
} finally {
|
|
3500
|
+
disposePending();
|
|
3501
|
+
}
|
|
1674
3502
|
}
|
|
1675
|
-
}
|
|
1676
|
-
previousActive?.dispose();
|
|
1677
|
-
return true;
|
|
1678
|
-
} catch {
|
|
1679
|
-
const current = active.get(definition.id);
|
|
1680
|
-
if (current?.controller === controller) {
|
|
1681
|
-
current.dispose();
|
|
1682
|
-
active.delete(definition.id);
|
|
1683
|
-
if (previousActive !== void 0) active.set(definition.id, previousActive);
|
|
1684
|
-
}
|
|
1685
|
-
return false;
|
|
1686
|
-
}
|
|
1687
|
-
};
|
|
1688
|
-
const activateDefinition = (definition) => {
|
|
1689
|
-
const task = (activationQueues.get(definition.id) ?? Promise.resolve(true)).then(() => definitions.get(definition.id) === definition ? activateDefinitionNow(definition) : false, () => definitions.get(definition.id) === definition ? activateDefinitionNow(definition) : false);
|
|
1690
|
-
activationQueues.set(definition.id, task);
|
|
1691
|
-
task.finally(() => {
|
|
1692
|
-
if (activationQueues.get(definition.id) === task) activationQueues.delete(definition.id);
|
|
3503
|
+
};
|
|
1693
3504
|
});
|
|
1694
|
-
return task;
|
|
1695
3505
|
};
|
|
1696
3506
|
const define = (definition) => {
|
|
1697
|
-
if (definition.apiVersion !== 1 || !/^[a-z][a-z0-9-]{0,63}$/u.test(definition.id) || typeof definition.activate !== "function") return;
|
|
3507
|
+
if (disposed || definition.apiVersion !== 1 || !/^[a-z][a-z0-9-]{0,63}$/u.test(definition.id) || typeof definition.activate !== "function") return;
|
|
1698
3508
|
if (expectedDefinitionId !== void 0 && definition.id !== expectedDefinitionId) return;
|
|
1699
3509
|
definitions.set(definition.id, definition);
|
|
1700
3510
|
if (started && expectedDefinitionId === void 0) activateDefinition(definition);
|
|
@@ -1720,22 +3530,37 @@ window.__ModuleLoader__.load({
|
|
|
1720
3530
|
credentials: "same-origin",
|
|
1721
3531
|
cache: "no-store",
|
|
1722
3532
|
headers,
|
|
1723
|
-
|
|
3533
|
+
signal
|
|
1724
3534
|
});
|
|
1725
3535
|
if (response.status === 304) return true;
|
|
1726
3536
|
if (!response.ok) return false;
|
|
1727
|
-
|
|
1728
|
-
|
|
3537
|
+
const nextEtag = response.headers.get("etag") ?? "";
|
|
3538
|
+
const nextModified = response.headers.get("last-modified") ?? "";
|
|
1729
3539
|
const next = await response.text();
|
|
1730
|
-
if (
|
|
3540
|
+
if (refreshAborted(signal)) return false;
|
|
3541
|
+
if (next === legacySource) {
|
|
3542
|
+
legacyJsEtag = nextEtag;
|
|
3543
|
+
legacyJsModified = nextModified;
|
|
3544
|
+
return true;
|
|
3545
|
+
}
|
|
1731
3546
|
legacyMount = void 0;
|
|
1732
3547
|
const script = element("script");
|
|
1733
3548
|
script.textContent = `${next}\n//# sourceURL=dsh-mobile-custom.js`;
|
|
1734
3549
|
document.head.append(script);
|
|
1735
3550
|
script.remove();
|
|
3551
|
+
if (refreshAborted(signal)) {
|
|
3552
|
+
legacyMount = previousMount;
|
|
3553
|
+
return false;
|
|
3554
|
+
}
|
|
1736
3555
|
const mount = legacyMount;
|
|
1737
3556
|
if (mount === void 0) {
|
|
3557
|
+
legacyDispose?.();
|
|
3558
|
+
legacyDispose = void 0;
|
|
3559
|
+
legacyRoot?.remove();
|
|
3560
|
+
legacyRoot = void 0;
|
|
1738
3561
|
legacySource = next;
|
|
3562
|
+
legacyJsEtag = nextEtag;
|
|
3563
|
+
legacyJsModified = nextModified;
|
|
1739
3564
|
return true;
|
|
1740
3565
|
}
|
|
1741
3566
|
const nextRoot = element("div");
|
|
@@ -1748,12 +3573,21 @@ window.__ModuleLoader__.load({
|
|
|
1748
3573
|
root: nextRoot,
|
|
1749
3574
|
window
|
|
1750
3575
|
});
|
|
3576
|
+
if (refreshAborted(signal)) {
|
|
3577
|
+
if (typeof nextDispose === "function") nextDispose();
|
|
3578
|
+
pendingRoot.remove();
|
|
3579
|
+
pendingRoot = void 0;
|
|
3580
|
+
legacyMount = previousMount;
|
|
3581
|
+
return false;
|
|
3582
|
+
}
|
|
1751
3583
|
legacyDispose?.();
|
|
1752
3584
|
legacyRoot?.remove();
|
|
1753
3585
|
legacyRoot = nextRoot;
|
|
1754
3586
|
pendingRoot = void 0;
|
|
1755
3587
|
legacyDispose = typeof nextDispose === "function" ? nextDispose : void 0;
|
|
1756
3588
|
legacySource = next;
|
|
3589
|
+
legacyJsEtag = nextEtag;
|
|
3590
|
+
legacyJsModified = nextModified;
|
|
1757
3591
|
return true;
|
|
1758
3592
|
} catch {
|
|
1759
3593
|
pendingRoot?.remove();
|
|
@@ -1763,6 +3597,26 @@ window.__ModuleLoader__.load({
|
|
|
1763
3597
|
};
|
|
1764
3598
|
let legacyScriptRevision = "";
|
|
1765
3599
|
let legacyStyleRevision = "";
|
|
3600
|
+
const disposeManifestExtension = (id) => {
|
|
3601
|
+
activations.remove(id);
|
|
3602
|
+
styleNodes.get(id)?.remove();
|
|
3603
|
+
styleNodes.delete(id);
|
|
3604
|
+
styleEtags.delete(id);
|
|
3605
|
+
scriptDigests.delete(id);
|
|
3606
|
+
activeHostGenerations.delete(id);
|
|
3607
|
+
activationKeys.delete(id);
|
|
3608
|
+
if (managedDefinitionIds.delete(id)) definitions.delete(id);
|
|
3609
|
+
};
|
|
3610
|
+
const managedManifestIdSources = () => [
|
|
3611
|
+
styleNodes.keys(),
|
|
3612
|
+
styleEtags.keys(),
|
|
3613
|
+
scriptDigests.keys(),
|
|
3614
|
+
managedDefinitionIds
|
|
3615
|
+
];
|
|
3616
|
+
const clearManifestExtensions = () => {
|
|
3617
|
+
publishAuthoritativeExtensionIds(manifestExtensionIds, /* @__PURE__ */ new Set(), managedManifestIdSources(), disposeManifestExtension);
|
|
3618
|
+
manifestEtag = "";
|
|
3619
|
+
};
|
|
1766
3620
|
const refreshExtensions = async (signal) => {
|
|
1767
3621
|
try {
|
|
1768
3622
|
const headers = {};
|
|
@@ -1771,30 +3625,57 @@ window.__ModuleLoader__.load({
|
|
|
1771
3625
|
credentials: "same-origin",
|
|
1772
3626
|
cache: "no-store",
|
|
1773
3627
|
headers,
|
|
1774
|
-
|
|
3628
|
+
signal
|
|
1775
3629
|
});
|
|
1776
|
-
if (response.status === 404)
|
|
3630
|
+
if (response.status === 404) {
|
|
3631
|
+
legacyScriptRevision = "";
|
|
3632
|
+
legacyStyleRevision = "";
|
|
3633
|
+
return handleMissingExtensionManifest(clearManifestExtensions, () => Promise.all([refreshLegacy(signal), refreshCssLegacy(legacyStyle, signal, legacyCssState)]), signal);
|
|
3634
|
+
}
|
|
1777
3635
|
if (response.status === 304) return true;
|
|
1778
3636
|
if (!response.ok) return false;
|
|
1779
3637
|
const nextManifestEtag = response.headers.get("etag") ?? "";
|
|
1780
|
-
const payload = await response.json();
|
|
1781
|
-
|
|
1782
|
-
const
|
|
3638
|
+
const payload = parseMobileExtensionManifest(await response.json());
|
|
3639
|
+
if (refreshAborted(signal) || payload === void 0) return false;
|
|
3640
|
+
const entries = payload.extensions;
|
|
3641
|
+
const seen = new Set(entries.map((entry) => entry.id));
|
|
3642
|
+
const scriptRevision = payload.legacy.scriptRevision;
|
|
3643
|
+
const styleRevision = payload.legacy.styleRevision;
|
|
1783
3644
|
let refreshComplete = true;
|
|
1784
3645
|
if (scriptRevision === "" || scriptRevision !== legacyScriptRevision) {
|
|
1785
3646
|
if (await refreshLegacy(signal)) legacyScriptRevision = scriptRevision;
|
|
1786
3647
|
else refreshComplete = false;
|
|
1787
3648
|
}
|
|
1788
3649
|
if (styleRevision === "" || styleRevision !== legacyStyleRevision) {
|
|
1789
|
-
if (await refreshCssLegacy(legacyStyle, signal)) legacyStyleRevision = styleRevision;
|
|
3650
|
+
if (await refreshCssLegacy(legacyStyle, signal, legacyCssState)) legacyStyleRevision = styleRevision;
|
|
1790
3651
|
else refreshComplete = false;
|
|
1791
3652
|
}
|
|
1792
|
-
const
|
|
1793
|
-
|
|
3653
|
+
const commitStyle = (id, change, css, etag) => {
|
|
3654
|
+
if (change === "retain") return;
|
|
3655
|
+
const oldStyle = styleNodes.get(id);
|
|
3656
|
+
if (change === "remove") {
|
|
3657
|
+
oldStyle?.remove();
|
|
3658
|
+
styleNodes.delete(id);
|
|
3659
|
+
styleEtags.delete(id);
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
if (oldStyle?.textContent !== css) {
|
|
3663
|
+
const node = element("style");
|
|
3664
|
+
node.dataset.dshMobileExtensionStyle = id;
|
|
3665
|
+
node.textContent = css ?? "";
|
|
3666
|
+
document.head.append(node);
|
|
3667
|
+
styleNodes.set(id, node);
|
|
3668
|
+
oldStyle?.remove();
|
|
3669
|
+
}
|
|
3670
|
+
if (etag !== void 0 && etag !== "") styleEtags.set(id, etag);
|
|
3671
|
+
};
|
|
1794
3672
|
for (const entry of entries) {
|
|
1795
|
-
|
|
1796
|
-
|
|
3673
|
+
const hadActiveGeneration = activations.hasActive(entry.id);
|
|
3674
|
+
const previousHostGeneration = activeHostGenerations.get(entry.id);
|
|
3675
|
+
let previousDefinition;
|
|
3676
|
+
let evaluatedDefinition = false;
|
|
1797
3677
|
try {
|
|
3678
|
+
let styleChange = entry.styleUrl === void 0 ? "remove" : "retain";
|
|
1798
3679
|
let pendingCss;
|
|
1799
3680
|
let pendingStyleEtag;
|
|
1800
3681
|
const cssUrl = typeof entry.styleUrl === "string" ? entry.styleUrl : void 0;
|
|
@@ -1806,16 +3687,25 @@ window.__ModuleLoader__.load({
|
|
|
1806
3687
|
credentials: "same-origin",
|
|
1807
3688
|
cache: "no-store",
|
|
1808
3689
|
headers: cssHeaders,
|
|
1809
|
-
|
|
3690
|
+
signal
|
|
1810
3691
|
});
|
|
1811
3692
|
if (cssResponse.status !== 304) {
|
|
1812
3693
|
if (!cssResponse.ok) throw new Error("mobile extension style failed to load");
|
|
1813
3694
|
pendingStyleEtag = cssResponse.headers.get("etag") ?? void 0;
|
|
1814
3695
|
pendingCss = await cssResponse.text();
|
|
3696
|
+
styleChange = "replace";
|
|
3697
|
+
if (refreshAborted(signal)) return false;
|
|
1815
3698
|
}
|
|
1816
3699
|
}
|
|
1817
3700
|
const scriptUrl = typeof entry.scriptUrl === "string" ? entry.scriptUrl : void 0;
|
|
1818
|
-
if (scriptUrl
|
|
3701
|
+
if (scriptUrl === void 0) {
|
|
3702
|
+
if (refreshAborted(signal)) return false;
|
|
3703
|
+
commitStyle(entry.id, styleChange, pendingCss, pendingStyleEtag);
|
|
3704
|
+
activations.remove(entry.id);
|
|
3705
|
+
activeHostGenerations.delete(entry.id);
|
|
3706
|
+
scriptDigests.delete(entry.id);
|
|
3707
|
+
if (managedDefinitionIds.delete(entry.id)) definitions.delete(entry.id);
|
|
3708
|
+
} else {
|
|
1819
3709
|
const scriptHeaders = {};
|
|
1820
3710
|
const storedDigest = scriptDigests.get(entry.id);
|
|
1821
3711
|
if (storedDigest !== void 0) scriptHeaders["if-none-match"] = storedDigest;
|
|
@@ -1823,15 +3713,18 @@ window.__ModuleLoader__.load({
|
|
|
1823
3713
|
credentials: "same-origin",
|
|
1824
3714
|
cache: "no-store",
|
|
1825
3715
|
headers: scriptHeaders,
|
|
1826
|
-
|
|
3716
|
+
signal
|
|
1827
3717
|
});
|
|
3718
|
+
let nextDigest;
|
|
1828
3719
|
if (scriptResponse.status !== 304) {
|
|
1829
3720
|
if (!scriptResponse.ok) throw new Error("mobile extension script failed to load");
|
|
1830
3721
|
const source = await scriptResponse.text();
|
|
3722
|
+
if (refreshAborted(signal)) return false;
|
|
1831
3723
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(source));
|
|
3724
|
+
if (refreshAborted(signal)) return false;
|
|
1832
3725
|
const key = [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
1833
3726
|
if (scriptDigests.get(entry.id) !== key) {
|
|
1834
|
-
|
|
3727
|
+
previousDefinition = definitions.get(entry.id);
|
|
1835
3728
|
try {
|
|
1836
3729
|
expectedDefinitionId = entry.id;
|
|
1837
3730
|
try {
|
|
@@ -1844,9 +3737,8 @@ window.__ModuleLoader__.load({
|
|
|
1844
3737
|
}
|
|
1845
3738
|
const nextDefinition = definitions.get(entry.id);
|
|
1846
3739
|
if (nextDefinition === void 0 || nextDefinition === previousDefinition) throw new Error("mobile extension did not define its manifest id");
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
scriptDigests.set(entry.id, key);
|
|
3740
|
+
evaluatedDefinition = true;
|
|
3741
|
+
nextDigest = key;
|
|
1850
3742
|
} catch (error) {
|
|
1851
3743
|
if (previousDefinition === void 0) definitions.delete(entry.id);
|
|
1852
3744
|
else definitions.set(entry.id, previousDefinition);
|
|
@@ -1855,37 +3747,35 @@ window.__ModuleLoader__.load({
|
|
|
1855
3747
|
}
|
|
1856
3748
|
}
|
|
1857
3749
|
const definition = definitions.get(entry.id);
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
if (
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
styleNodes.set(entry.id, node);
|
|
1868
|
-
oldStyle?.remove();
|
|
3750
|
+
const commitGeneration = () => {
|
|
3751
|
+
commitStyle(entry.id, styleChange, pendingCss, pendingStyleEtag);
|
|
3752
|
+
};
|
|
3753
|
+
if (definition === void 0) throw new Error("mobile extension definition is unavailable");
|
|
3754
|
+
if (evaluatedDefinition || !activations.hasActive(entry.id) || activeHostGenerations.get(entry.id) !== entry.generation) {
|
|
3755
|
+
if (!await activateDefinition(definition, signal, commitGeneration, entry.generation)) throw new Error("mobile extension activation failed");
|
|
3756
|
+
} else {
|
|
3757
|
+
if (refreshAborted(signal)) return false;
|
|
3758
|
+
commitGeneration();
|
|
1869
3759
|
}
|
|
1870
|
-
|
|
3760
|
+
managedDefinitionIds.add(entry.id);
|
|
3761
|
+
if (nextDigest !== void 0) scriptDigests.set(entry.id, nextDigest);
|
|
1871
3762
|
}
|
|
1872
3763
|
} catch {
|
|
3764
|
+
if (evaluatedDefinition) {
|
|
3765
|
+
if (previousDefinition === void 0) definitions.delete(entry.id);
|
|
3766
|
+
else definitions.set(entry.id, previousDefinition);
|
|
3767
|
+
}
|
|
3768
|
+
failClosedExtensionGenerationReplacement(hadActiveGeneration, previousHostGeneration, entry.generation, () => {
|
|
3769
|
+
disposeManifestExtension(entry.id);
|
|
3770
|
+
});
|
|
1873
3771
|
refreshComplete = false;
|
|
1874
3772
|
}
|
|
1875
3773
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
styleNodes.delete(id);
|
|
1882
|
-
styleEtags.delete(id);
|
|
1883
|
-
scriptDigests.delete(id);
|
|
1884
|
-
if (managedDefinitionIds.delete(id)) definitions.delete(id);
|
|
1885
|
-
}
|
|
1886
|
-
manifestExtensionIds.clear();
|
|
1887
|
-
for (const id of seen) manifestExtensionIds.add(id);
|
|
1888
|
-
manifestEtag = refreshComplete ? nextManifestEtag : "";
|
|
3774
|
+
if (refreshAborted(signal)) return false;
|
|
3775
|
+
if (refreshComplete) {
|
|
3776
|
+
publishAuthoritativeExtensionIds(manifestExtensionIds, seen, managedManifestIdSources(), disposeManifestExtension);
|
|
3777
|
+
manifestEtag = nextManifestEtag;
|
|
3778
|
+
} else manifestEtag = "";
|
|
1889
3779
|
return refreshComplete;
|
|
1890
3780
|
} catch {
|
|
1891
3781
|
return false;
|
|
@@ -1893,76 +3783,60 @@ window.__ModuleLoader__.load({
|
|
|
1893
3783
|
};
|
|
1894
3784
|
started = true;
|
|
1895
3785
|
for (const definition of definitions.values()) activateDefinition(definition);
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
let refreshController;
|
|
1903
|
-
const scheduleRefresh = (delayMs) => {
|
|
1904
|
-
if (stopped || document.visibilityState === "hidden" || !navigator.onLine) return;
|
|
1905
|
-
if (refreshTimer !== void 0) clearTimeout(refreshTimer);
|
|
1906
|
-
refreshTimer = window.setTimeout(runRefresh, delayMs);
|
|
1907
|
-
};
|
|
1908
|
-
const runRefresh = () => {
|
|
1909
|
-
if (stopped || refreshController !== void 0) return;
|
|
1910
|
-
refreshController = new AbortController();
|
|
1911
|
-
const controller = refreshController;
|
|
1912
|
-
const timeout = window.setTimeout(() => {
|
|
1913
|
-
controller.abort();
|
|
1914
|
-
}, refreshTimeoutMs);
|
|
1915
|
-
refreshExtensions(controller.signal).then((success) => {
|
|
1916
|
-
refreshFailures = success ? 0 : refreshFailures + 1;
|
|
1917
|
-
}).finally(() => {
|
|
1918
|
-
clearTimeout(timeout);
|
|
1919
|
-
if (refreshController === controller) refreshController = void 0;
|
|
1920
|
-
const backoff = Math.min(6e4, refreshIntervalMs * 2 ** Math.min(refreshFailures, 3));
|
|
1921
|
-
scheduleRefresh(backoff);
|
|
1922
|
-
});
|
|
1923
|
-
};
|
|
1924
|
-
const resumeRefresh = () => {
|
|
1925
|
-
if (document.visibilityState !== "hidden" && navigator.onLine) scheduleRefresh(0);
|
|
1926
|
-
};
|
|
1927
|
-
document.addEventListener("visibilitychange", resumeRefresh);
|
|
1928
|
-
window.addEventListener("online", resumeRefresh);
|
|
1929
|
-
scheduleRefresh(0);
|
|
3786
|
+
const stopRefresh = startLifecycleRefreshScheduler(async (signal) => {
|
|
3787
|
+
await refreshExtensions(signal);
|
|
3788
|
+
});
|
|
3789
|
+
const stopEvents = startExtensionChangeStream(() => {
|
|
3790
|
+
stopRefresh.refresh();
|
|
3791
|
+
});
|
|
1930
3792
|
return () => {
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
document.removeEventListener("visibilitychange", resumeRefresh);
|
|
1935
|
-
window.removeEventListener("online", resumeRefresh);
|
|
3793
|
+
disposed = true;
|
|
3794
|
+
stopEvents();
|
|
3795
|
+
stopRefresh();
|
|
1936
3796
|
started = false;
|
|
1937
3797
|
legacyDispose?.();
|
|
3798
|
+
legacyDispose = void 0;
|
|
1938
3799
|
legacyRoot?.remove();
|
|
3800
|
+
legacyRoot = void 0;
|
|
1939
3801
|
legacyStyle.remove();
|
|
1940
|
-
|
|
3802
|
+
activations.dispose();
|
|
1941
3803
|
for (const node of styleNodes.values()) node.remove();
|
|
3804
|
+
styleNodes.clear();
|
|
1942
3805
|
document.querySelector("[data-dsh-mobile-extension-layer]")?.remove();
|
|
1943
3806
|
for (const host of document.querySelectorAll("[data-dsh-mobile-surface-host]")) host.remove();
|
|
1944
3807
|
if (previous === void 0) delete window.dshMobile;
|
|
1945
3808
|
else window.dshMobile = previous;
|
|
1946
3809
|
};
|
|
1947
3810
|
}
|
|
1948
|
-
|
|
1949
|
-
let cssModified = "";
|
|
1950
|
-
async function refreshCssLegacy(style, signal) {
|
|
3811
|
+
async function refreshCssLegacy(style, signal, state) {
|
|
1951
3812
|
try {
|
|
1952
3813
|
const headers = {};
|
|
1953
|
-
if (
|
|
1954
|
-
if (
|
|
1955
|
-
|
|
3814
|
+
if (state.etag !== "") headers["if-none-match"] = state.etag;
|
|
3815
|
+
if (state.modified !== "") headers["if-modified-since"] = state.modified;
|
|
3816
|
+
let response = await fetch("/mobile-access/custom.css", {
|
|
1956
3817
|
credentials: "same-origin",
|
|
1957
3818
|
cache: "no-store",
|
|
1958
3819
|
headers,
|
|
1959
|
-
|
|
3820
|
+
signal
|
|
1960
3821
|
});
|
|
3822
|
+
if (response.status === 304 && style.textContent === "") {
|
|
3823
|
+
state.etag = "";
|
|
3824
|
+
state.modified = "";
|
|
3825
|
+
response = await fetch("/mobile-access/custom.css", {
|
|
3826
|
+
credentials: "same-origin",
|
|
3827
|
+
cache: "no-store",
|
|
3828
|
+
signal
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
1961
3831
|
if (response.status === 304) return true;
|
|
1962
3832
|
if (response.ok) {
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
3833
|
+
const nextEtag = response.headers.get("etag") ?? "";
|
|
3834
|
+
const nextModified = response.headers.get("last-modified") ?? "";
|
|
3835
|
+
const css = await response.text();
|
|
3836
|
+
if (refreshAborted(signal)) return false;
|
|
3837
|
+
style.textContent = css;
|
|
3838
|
+
state.etag = nextEtag;
|
|
3839
|
+
state.modified = nextModified;
|
|
1966
3840
|
return true;
|
|
1967
3841
|
}
|
|
1968
3842
|
return false;
|
|
@@ -2008,31 +3882,39 @@ window.__ModuleLoader__.load({
|
|
|
2008
3882
|
document.head.append(style);
|
|
2009
3883
|
if (!loopback) {
|
|
2010
3884
|
const removeCustom = installCustomAssets();
|
|
2011
|
-
const removeSurface = installNativeMobileSurface
|
|
3885
|
+
const removeSurface = installDshLanguageBoundSurface(installNativeMobileSurface);
|
|
2012
3886
|
return () => {
|
|
2013
3887
|
removeCustom();
|
|
2014
3888
|
removeSurface();
|
|
2015
3889
|
style.remove();
|
|
2016
3890
|
};
|
|
2017
3891
|
}
|
|
2018
|
-
const
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
3892
|
+
const removeControl = installDshLanguageBoundSurface(() => {
|
|
3893
|
+
const control = installControl();
|
|
3894
|
+
const triggerLocale = selectedMobileControlLocale();
|
|
3895
|
+
const t = controlTranslator(triggerLocale);
|
|
3896
|
+
const disposeSlot = ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
3897
|
+
name: "sidebar.footer.action",
|
|
3898
|
+
id: "dsh-mobile"
|
|
3899
|
+
}, ({ wide }) => (0, react.createElement)("button", {
|
|
3900
|
+
"aria-expanded": false,
|
|
3901
|
+
"aria-label": t("mobileAccess"),
|
|
3902
|
+
className: `dsh-mobile-control__trigger${wide ? "" : " is-rail"}`,
|
|
3903
|
+
lang: triggerLocale,
|
|
3904
|
+
type: "button",
|
|
3905
|
+
title: t("mobileAccess"),
|
|
3906
|
+
onClick: control.toggle
|
|
3907
|
+
}, (0, react.createElement)("span", {
|
|
3908
|
+
"aria-hidden": true,
|
|
3909
|
+
className: "dsh-mobile-control__trigger-icon"
|
|
3910
|
+
}), wide ? (0, react.createElement)("span", { className: "dsh-mobile-control__trigger-label" }, t("mobileAccess")) : void 0)));
|
|
3911
|
+
return () => {
|
|
3912
|
+
disposeSlot();
|
|
3913
|
+
control.remove();
|
|
3914
|
+
};
|
|
3915
|
+
});
|
|
2033
3916
|
return () => {
|
|
2034
|
-
|
|
2035
|
-
control.remove();
|
|
3917
|
+
removeControl();
|
|
2036
3918
|
style.remove();
|
|
2037
3919
|
};
|
|
2038
3920
|
}, "dsh-mobile: stock mobile adaptation and local control");
|
|
@@ -2040,9 +3922,36 @@ window.__ModuleLoader__.load({
|
|
|
2040
3922
|
/** Client services required by the mobile adaptation. */
|
|
2041
3923
|
const inject = ["slots"];
|
|
2042
3924
|
//#endregion
|
|
3925
|
+
exports.DIAGNOSTIC_REASON_MESSAGES = DIAGNOSTIC_REASON_MESSAGES;
|
|
3926
|
+
exports.MOBILE_CONTROL_MESSAGES = MOBILE_CONTROL_MESSAGES;
|
|
3927
|
+
exports.PerIdActivationLifecycle = PerIdActivationLifecycle;
|
|
2043
3928
|
exports.apply = apply;
|
|
3929
|
+
exports.bindClientResponseLifetime = bindClientResponseLifetime;
|
|
3930
|
+
exports.combineClientSignalLifetime = combineClientSignalLifetime;
|
|
3931
|
+
exports.combineClientSignals = combineClientSignals;
|
|
3932
|
+
exports.diagnosticEntriesForRender = diagnosticEntriesForRender;
|
|
3933
|
+
exports.diagnosticOverallForChecks = diagnosticOverallForChecks;
|
|
3934
|
+
exports.diagnosticServerCopy = diagnosticServerCopy;
|
|
3935
|
+
exports.extensionAssetUrl = extensionAssetUrl;
|
|
3936
|
+
exports.extensionGenerationHeaders = extensionGenerationHeaders;
|
|
3937
|
+
exports.extensionRouteUrl = extensionRouteUrl;
|
|
3938
|
+
exports.failClosedExtensionGenerationReplacement = failClosedExtensionGenerationReplacement;
|
|
3939
|
+
exports.handleMissingExtensionManifest = handleMissingExtensionManifest;
|
|
2044
3940
|
exports.inject = inject;
|
|
3941
|
+
exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
|
|
3942
|
+
exports.normalizeDiagnosticOverall = normalizeDiagnosticOverall;
|
|
3943
|
+
exports.normalizeDiagnosticStatus = normalizeDiagnosticStatus;
|
|
3944
|
+
exports.parseMobileExtensionManifest = parseMobileExtensionManifest;
|
|
3945
|
+
exports.publishAuthoritativeExtensionIds = publishAuthoritativeExtensionIds;
|
|
3946
|
+
exports.reconcileRemovedExtensions = reconcileRemovedExtensions;
|
|
3947
|
+
exports.registerUniqueDisposable = registerUniqueDisposable;
|
|
3948
|
+
exports.renderDiagnosticPayloadSafely = renderDiagnosticPayloadSafely;
|
|
3949
|
+
exports.selectMobileControlLocale = selectMobileControlLocale;
|
|
3950
|
+
exports.selectedMobileControlLocale = selectedMobileControlLocale;
|
|
3951
|
+
exports.startExtensionChangeStream = startExtensionChangeStream;
|
|
3952
|
+
exports.startLifecycleRefreshScheduler = startLifecycleRefreshScheduler;
|
|
2045
3953
|
exports.trustAuthenticatedGatewayConnection = trustAuthenticatedGatewayConnection;
|
|
3954
|
+
exports.validateDiagnosticChecks = validateDiagnosticChecks;
|
|
2046
3955
|
return module.exports;
|
|
2047
3956
|
}
|
|
2048
3957
|
});
|