dsh-mobile 0.3.1 → 0.3.3
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 +21 -0
- package/FUNNEL_THIRD_PARTY_LICENSES.txt +2551 -0
- package/README.en.md +26 -13
- package/README.md +24 -11
- package/SECURITY.md +6 -3
- package/THIRD_PARTY_NOTICES.md +7 -1
- package/bin/dsh-mobile-funnel-win32-x64.exe +0 -0
- package/lib/cli.js +4 -1
- package/lib/cli.js.map +1 -0
- package/lib/client.js +2997 -450
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +259 -11
- package/lib/index.mjs +2341 -407
- package/lib/index.mjs.map +1 -0
- package/lib/mobile-layout.js +54 -10
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +10 -6
- package/assets/brand/app-icon-master.png +0 -0
- package/assets/brand/repository-hero.png +0 -0
package/lib/client.js
CHANGED
|
@@ -5,6 +5,925 @@ 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 latest Android app",
|
|
14
|
+
downloadAndroidVersion: "Download latest Android app · v{version}",
|
|
15
|
+
downloadAndroidAria: "Download the latest Android app from GitHub Releases",
|
|
16
|
+
downloadAndroidVersionAria: "Download Android app v{version} from GitHub Releases",
|
|
17
|
+
updatePlugin: "Update plugin",
|
|
18
|
+
updatePluginAria: "Update DSH Mobile to v{version}",
|
|
19
|
+
updatingPlugin: "Updating…",
|
|
20
|
+
pluginUpdatedRestart: "Updated to v{version}. Restart DSH to apply it.",
|
|
21
|
+
pluginUpdateFailed: "Update failed: {error}",
|
|
22
|
+
lan: "Local network",
|
|
23
|
+
remote: "Remote",
|
|
24
|
+
lanAccess: "Local network access",
|
|
25
|
+
remoteAccess: "Remote access",
|
|
26
|
+
browserAccess: "Browser access",
|
|
27
|
+
remoteAddress: "Remote address",
|
|
28
|
+
loadingStatus: "Loading status…",
|
|
29
|
+
loadingRemoteStatus: "Loading remote status…",
|
|
30
|
+
generateCopyKey: "Generate and copy key",
|
|
31
|
+
copyPairLink: "Copy pairing link",
|
|
32
|
+
managePairedDevices: "Manage paired devices",
|
|
33
|
+
clearAllDevices: "Clear all devices",
|
|
34
|
+
pairingQr: "Pairing QR code",
|
|
35
|
+
remoteIntro: "Choose the remote channel that suits you. Switching or disabling remote access does not affect the local network.",
|
|
36
|
+
chooseProvider: "Choose connection method",
|
|
37
|
+
currentProvider: "Current connection",
|
|
38
|
+
remoteStateOff: "Off",
|
|
39
|
+
remoteStateConnecting: "Connecting",
|
|
40
|
+
remoteStateAttention: "Needs attention",
|
|
41
|
+
providerInfoAria: "View remote connection security and network information",
|
|
42
|
+
providerGroupAria: "Remote connection method",
|
|
43
|
+
providerSafeTitle: "You remain protected",
|
|
44
|
+
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.",
|
|
45
|
+
builtIn: "Built in",
|
|
46
|
+
mainlandPreferred: "Preferred in mainland China",
|
|
47
|
+
tailscaleDescription: "Wider coverage; mainland China networks may be unstable. The first connection requires login and Funnel authorization.",
|
|
48
|
+
cpolarDescription: "Installs the official component on demand and is suitable for mainland China networks.",
|
|
49
|
+
prepareCpolar: "Prepare cpolar",
|
|
50
|
+
checkingComponent: "Checking component…",
|
|
51
|
+
installOfficial: "Install official component",
|
|
52
|
+
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.",
|
|
53
|
+
registerCpolar: "Register with cpolar",
|
|
54
|
+
openDashboard: "Open dashboard to get token",
|
|
55
|
+
tokenPlaceholder: "Paste cpolar Authtoken",
|
|
56
|
+
saveConnect: "Save and connect",
|
|
57
|
+
saving: "Saving…",
|
|
58
|
+
componentDetails: "Component source and cleanup",
|
|
59
|
+
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.",
|
|
60
|
+
pluginPrivateDirectory: "Plugin private directory",
|
|
61
|
+
officialDownload: "Official download page",
|
|
62
|
+
terms: "Terms of service",
|
|
63
|
+
purgeCpolar: "Completely remove cpolar component and configuration",
|
|
64
|
+
tailscaleHelp: "How to use Tailscale",
|
|
65
|
+
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.",
|
|
66
|
+
funnelGuideAria: "Steps to enable Tailscale Funnel",
|
|
67
|
+
funnelGuideTitle: "Remote access setup · Step 2",
|
|
68
|
+
funnelGuideSummary: "Tailscale login is complete. Funnel must still be allowed for this computer; the official page also enables HTTPS.",
|
|
69
|
+
funnelStep1: "Open the official Tailscale authorization page for this node.",
|
|
70
|
+
funnelStep2: "Confirm Funnel; you do not need to sign in to DSH again.",
|
|
71
|
+
funnelStep3: "Return to DSH; the plugin checks and connects automatically.",
|
|
72
|
+
funnelGuideNote: "Requires an Owner, Admin, or Network admin account.",
|
|
73
|
+
continueFunnel: "Continue Funnel authorization",
|
|
74
|
+
retryNow: "Done, retry now",
|
|
75
|
+
enableRemote: "Enable remote access",
|
|
76
|
+
disableRemote: "Disable remote access",
|
|
77
|
+
continueLogin: "Continue login",
|
|
78
|
+
reconnect: "Reconnect",
|
|
79
|
+
generateRemoteQr: "Generate remote pairing QR code",
|
|
80
|
+
manageRemoteDevices: "Manage remote devices",
|
|
81
|
+
resetRemoteLogin: "Sign out and clear remote login",
|
|
82
|
+
resetRemoteDevices: "Disable and clear remote devices",
|
|
83
|
+
lanOn: "Local network access is on.",
|
|
84
|
+
lanOff: "Local network access is off.",
|
|
85
|
+
enableLan: "Enable local network access",
|
|
86
|
+
disableLan: "Disable local network access",
|
|
87
|
+
extensionsLoaded: "Extensions: {loaded} loaded",
|
|
88
|
+
extensionsFailed: "Extensions: {loaded} loaded, {failed} failed",
|
|
89
|
+
keyGenerationFailed: "Could not generate a pairing key.",
|
|
90
|
+
keyCopied: "Pairing key copied. Paste it into the Android app.",
|
|
91
|
+
linkCopied: "Pairing link copied. Send it to the phone and paste it into the app or open it in a browser.",
|
|
92
|
+
copySecret: "Copy {kind}: {value}",
|
|
93
|
+
pairingKey: "pairing key",
|
|
94
|
+
pairingLink: "pairing link",
|
|
95
|
+
noDevices: "No paired devices.",
|
|
96
|
+
noRemoteDevices: "No remote paired devices.",
|
|
97
|
+
device: "Device",
|
|
98
|
+
expires: "Expires {time}",
|
|
99
|
+
revoke: "Revoke",
|
|
100
|
+
confirmResetDevices: "Remove all paired devices? Connected devices will be disconnected immediately.",
|
|
101
|
+
ready: "Ready",
|
|
102
|
+
installed: "Installed",
|
|
103
|
+
installWithSize: "Install official component · {size} MB",
|
|
104
|
+
cpolarUnsupported: "Only Windows x64 is currently supported. You can still use the built-in Tailscale Funnel.",
|
|
105
|
+
cpolarNotInstalled: "Not installed. A fixed version is downloaded from the official cpolar website only after you click the button below.",
|
|
106
|
+
cpolarNeedsToken: "Official component {version} is verified. Save the account token to continue.",
|
|
107
|
+
cpolarReady: "Official component {version} and the local account configuration are ready.",
|
|
108
|
+
remoteOff: "Remote access is disabled. Local network access is unaffected.",
|
|
109
|
+
remoteUnavailableCpolar: "cpolar is not installed or its local account is not configured.",
|
|
110
|
+
remoteUnavailableTailscale: "This computer is missing the Funnel runtime component. Reinstall the complete plugin package.",
|
|
111
|
+
remoteStartingCpolar: "Connecting to a cpolar node…",
|
|
112
|
+
remoteStartingTailscale: "Starting the secure Tailscale channel…",
|
|
113
|
+
remoteNeedsLogin: "Complete a one-time Tailscale login in the browser. The plugin never reads your password.",
|
|
114
|
+
remoteConnectingCpolar: "Public address allocated; starting the DSH authentication gateway…",
|
|
115
|
+
remoteConnectingTailscale: "Login complete; creating the public HTTPS address…",
|
|
116
|
+
remoteReady: "Remote access is ready. Only paired devices can access DSH.",
|
|
117
|
+
remoteError: "The remote connection was not established. Reconnect; local network access still works.",
|
|
118
|
+
funnelPermission: "Login is complete. Authorize Funnel to establish the remote connection automatically.",
|
|
119
|
+
funnelHttps: "Login is complete. Authorize Funnel; the official page also enables HTTPS.",
|
|
120
|
+
funnelStart: "Login is complete. Finish the initial Tailscale Funnel authorization.",
|
|
121
|
+
tailscaleDnsMissing: "Tailscale has not provided a remote address. Reconnect and confirm login is complete.",
|
|
122
|
+
gatewayStartFailed: "The remote gateway failed to start. Reconnect; local network access is unaffected.",
|
|
123
|
+
controlChannelFailed: "The remote component connection was interrupted. Reconnect.",
|
|
124
|
+
cpolarMissing: "The official cpolar component is not installed. Complete the preparation steps above.",
|
|
125
|
+
cpolarInvalid: "cpolar component verification failed. Remove it completely and reinstall.",
|
|
126
|
+
cpolarConfigMissing: "No cpolar account token is saved. Complete the preparation steps above.",
|
|
127
|
+
cpolarConfigInvalid: "The local cpolar configuration is invalid. Save the account token again.",
|
|
128
|
+
cpolarPortUnavailable: "Could not allocate a local remote-gateway port. Try again.",
|
|
129
|
+
cpolarLaunchFailed: "The cpolar client failed to start.",
|
|
130
|
+
cpolarTimeout: "Timed out connecting to a cpolar node. Reconnect.",
|
|
131
|
+
cpolarStopped: "The cpolar connection stopped.",
|
|
132
|
+
cpolarExited: "The cpolar connection exited unexpectedly. Reconnect.",
|
|
133
|
+
cpolarOutputInvalid: "cpolar returned an unrecognized status.",
|
|
134
|
+
cpolarOriginInvalid: "The public address returned by cpolar failed validation.",
|
|
135
|
+
setupOpened: "The official Tailscale page is open. Return to DSH after enabling Funnel; reconnection is automatic.",
|
|
136
|
+
switchProviderConfirm: "Switching connection method first disables the current remote channel. Local network access and paired devices are unaffected. Continue?",
|
|
137
|
+
switchingCpolar: "Switching to cpolar…",
|
|
138
|
+
switchingTailscale: "Switching to Tailscale Funnel…",
|
|
139
|
+
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.",
|
|
140
|
+
downloading: "Downloading and verifying…",
|
|
141
|
+
installingCpolar: "Installing the official cpolar component. Keep DSH running until it completes.",
|
|
142
|
+
installFailed: "Component installation failed: {error}",
|
|
143
|
+
invalidToken: "Paste the complete Authtoken from the cpolar dashboard.",
|
|
144
|
+
configuredConnecting: "Account configuration saved; connecting through cpolar…",
|
|
145
|
+
configureFailed: "Configuration failed: {error}",
|
|
146
|
+
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.",
|
|
147
|
+
purging: "Stopping the channel and removing cpolar files managed by DSH Mobile…",
|
|
148
|
+
purgeFailed: "Cleanup failed: {error}",
|
|
149
|
+
reconnectingCpolar: "Reconnecting to a cpolar node…",
|
|
150
|
+
reconnectingTailscale: "Checking Tailscale settings and reconnecting…",
|
|
151
|
+
remoteQrReady: "Remote pairing QR code generated. Scan it from “Remote access” in the app.",
|
|
152
|
+
resetCpolarConfirm: "Disable the cpolar remote channel and remove all remote paired devices? Your cpolar account and other tunnels are unchanged.",
|
|
153
|
+
resetTailscaleConfirm: "Sign out of Tailscale on this computer and remove all remote paired devices? Local network configuration is unchanged.",
|
|
154
|
+
requestFailed: "Request failed: {error}"
|
|
155
|
+
},
|
|
156
|
+
it: {
|
|
157
|
+
mobileAccess: "Accesso mobile",
|
|
158
|
+
collapseMobileAccess: "Riduci Accesso mobile",
|
|
159
|
+
downloadAndroid: "Scarica l’ultima app Android",
|
|
160
|
+
downloadAndroidVersion: "Scarica l’ultima app Android · v{version}",
|
|
161
|
+
downloadAndroidAria: "Scarica l’ultima app Android da GitHub Releases",
|
|
162
|
+
downloadAndroidVersionAria: "Scarica l’app Android v{version} da GitHub Releases",
|
|
163
|
+
updatePlugin: "Aggiorna plugin",
|
|
164
|
+
updatePluginAria: "Aggiorna DSH Mobile alla v{version}",
|
|
165
|
+
updatingPlugin: "Aggiornamento…",
|
|
166
|
+
pluginUpdatedRestart: "Aggiornato alla v{version}. Riavvia DSH per applicarlo.",
|
|
167
|
+
pluginUpdateFailed: "Aggiornamento non riuscito: {error}",
|
|
168
|
+
lan: "Rete locale",
|
|
169
|
+
remote: "Remoto",
|
|
170
|
+
lanAccess: "Accesso rete locale",
|
|
171
|
+
remoteAccess: "Accesso remoto",
|
|
172
|
+
browserAccess: "Accesso browser",
|
|
173
|
+
remoteAddress: "Indirizzo remoto",
|
|
174
|
+
loadingStatus: "Lettura stato…",
|
|
175
|
+
loadingRemoteStatus: "Lettura stato remoto…",
|
|
176
|
+
generateCopyKey: "Genera e copia chiave",
|
|
177
|
+
copyPairLink: "Copia link di abbinamento",
|
|
178
|
+
managePairedDevices: "Gestisci dispositivi abbinati",
|
|
179
|
+
clearAllDevices: "Rimuovi tutti i dispositivi",
|
|
180
|
+
pairingQr: "Codice QR di abbinamento",
|
|
181
|
+
remoteIntro: "Scegli il canale remoto più adatto. Cambiare o disattivare l’accesso remoto non influisce sulla rete locale.",
|
|
182
|
+
chooseProvider: "Scegli metodo di connessione",
|
|
183
|
+
currentProvider: "Connessione attuale",
|
|
184
|
+
remoteStateOff: "Disattivata",
|
|
185
|
+
remoteStateConnecting: "Connessione",
|
|
186
|
+
remoteStateAttention: "Da controllare",
|
|
187
|
+
providerInfoAria: "Informazioni su sicurezza e rete della connessione remota",
|
|
188
|
+
providerGroupAria: "Metodo di connessione remota",
|
|
189
|
+
providerSafeTitle: "La protezione resta attiva",
|
|
190
|
+
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.",
|
|
191
|
+
builtIn: "Integrato",
|
|
192
|
+
mainlandPreferred: "Preferito in Cina continentale",
|
|
193
|
+
tailscaleDescription: "Copertura più ampia; le reti della Cina continentale possono essere instabili. Il primo collegamento richiede accesso e autorizzazione Funnel.",
|
|
194
|
+
cpolarDescription: "Installa su richiesta il componente ufficiale, adatto alle reti della Cina continentale.",
|
|
195
|
+
prepareCpolar: "Prepara cpolar",
|
|
196
|
+
checkingComponent: "Controllo componente…",
|
|
197
|
+
installOfficial: "Installa componente ufficiale",
|
|
198
|
+
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.",
|
|
199
|
+
registerCpolar: "Registrati su cpolar",
|
|
200
|
+
openDashboard: "Apri la dashboard per il token",
|
|
201
|
+
tokenPlaceholder: "Incolla Authtoken cpolar",
|
|
202
|
+
saveConnect: "Salva e connetti",
|
|
203
|
+
saving: "Salvataggio…",
|
|
204
|
+
componentDetails: "Origine e rimozione del componente",
|
|
205
|
+
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.",
|
|
206
|
+
pluginPrivateDirectory: "Directory privata del plugin",
|
|
207
|
+
officialDownload: "Pagina download ufficiale",
|
|
208
|
+
terms: "Termini di servizio",
|
|
209
|
+
purgeCpolar: "Rimuovi completamente componente e configurazione cpolar",
|
|
210
|
+
tailscaleHelp: "Guida a Tailscale",
|
|
211
|
+
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.",
|
|
212
|
+
funnelGuideAria: "Passaggi per abilitare Tailscale Funnel",
|
|
213
|
+
funnelGuideTitle: "Configurazione accesso remoto · Passaggio 2",
|
|
214
|
+
funnelGuideSummary: "L’accesso Tailscale è completato. Devi ancora consentire Funnel per questo computer; la pagina ufficiale abilita anche HTTPS.",
|
|
215
|
+
funnelStep1: "Apri la pagina ufficiale di autorizzazione Tailscale per questo nodo.",
|
|
216
|
+
funnelStep2: "Conferma Funnel; non serve accedere di nuovo a DSH.",
|
|
217
|
+
funnelStep3: "Torna in DSH; il plugin controlla e si connette automaticamente.",
|
|
218
|
+
funnelGuideNote: "Richiede un account Owner, Admin o Network admin.",
|
|
219
|
+
continueFunnel: "Continua autorizzazione Funnel",
|
|
220
|
+
retryNow: "Fatto, riprova ora",
|
|
221
|
+
enableRemote: "Attiva accesso remoto",
|
|
222
|
+
disableRemote: "Disattiva accesso remoto",
|
|
223
|
+
continueLogin: "Continua accesso",
|
|
224
|
+
reconnect: "Riconnetti",
|
|
225
|
+
generateRemoteQr: "Genera QR di abbinamento remoto",
|
|
226
|
+
manageRemoteDevices: "Gestisci dispositivi remoti",
|
|
227
|
+
resetRemoteLogin: "Esci e rimuovi accesso remoto",
|
|
228
|
+
resetRemoteDevices: "Disattiva e rimuovi dispositivi remoti",
|
|
229
|
+
lanOn: "Accesso dalla rete locale attivo.",
|
|
230
|
+
lanOff: "Accesso dalla rete locale disattivato.",
|
|
231
|
+
enableLan: "Attiva accesso rete locale",
|
|
232
|
+
disableLan: "Disattiva accesso rete locale",
|
|
233
|
+
extensionsLoaded: "Estensioni: {loaded} caricate",
|
|
234
|
+
extensionsFailed: "Estensioni: {loaded} caricate, {failed} non riuscite",
|
|
235
|
+
keyGenerationFailed: "Impossibile generare la chiave di abbinamento.",
|
|
236
|
+
keyCopied: "Chiave di abbinamento copiata. Incollala nell’app Android.",
|
|
237
|
+
linkCopied: "Link di abbinamento copiato. Invialo al telefono e incollalo nell’app oppure aprilo nel browser.",
|
|
238
|
+
copySecret: "Copia {kind}: {value}",
|
|
239
|
+
pairingKey: "chiave di abbinamento",
|
|
240
|
+
pairingLink: "link di abbinamento",
|
|
241
|
+
noDevices: "Nessun dispositivo abbinato.",
|
|
242
|
+
noRemoteDevices: "Nessun dispositivo remoto abbinato.",
|
|
243
|
+
device: "Dispositivo",
|
|
244
|
+
expires: "Scade {time}",
|
|
245
|
+
revoke: "Revoca",
|
|
246
|
+
confirmResetDevices: "Rimuovere tutti i dispositivi abbinati? I dispositivi connessi verranno disconnessi subito.",
|
|
247
|
+
ready: "Pronto",
|
|
248
|
+
installed: "Installato",
|
|
249
|
+
installWithSize: "Installa componente ufficiale · {size} MB",
|
|
250
|
+
cpolarUnsupported: "Attualmente è supportato solo Windows x64. Puoi comunque usare Tailscale Funnel integrato.",
|
|
251
|
+
cpolarNotInstalled: "Non installato. Una versione fissa viene scaricata dal sito ufficiale cpolar solo dopo aver premuto il pulsante.",
|
|
252
|
+
cpolarNeedsToken: "Componente ufficiale {version} verificato. Salva il token account per continuare.",
|
|
253
|
+
cpolarReady: "Componente ufficiale {version} e configurazione account locale pronti.",
|
|
254
|
+
remoteOff: "Accesso remoto disattivato. La rete locale non è interessata.",
|
|
255
|
+
remoteUnavailableCpolar: "cpolar non è installato o l’account locale non è configurato.",
|
|
256
|
+
remoteUnavailableTailscale: "Su questo computer manca il componente runtime Funnel. Reinstalla il pacchetto completo del plugin.",
|
|
257
|
+
remoteStartingCpolar: "Connessione a un nodo cpolar…",
|
|
258
|
+
remoteStartingTailscale: "Avvio del canale sicuro Tailscale…",
|
|
259
|
+
remoteNeedsLogin: "Completa una volta l’accesso Tailscale nel browser. Il plugin non legge mai la password.",
|
|
260
|
+
remoteConnectingCpolar: "Indirizzo pubblico assegnato; avvio del gateway di autenticazione DSH…",
|
|
261
|
+
remoteConnectingTailscale: "Accesso completato; creazione dell’indirizzo HTTPS pubblico…",
|
|
262
|
+
remoteReady: "Accesso remoto pronto. Solo i dispositivi abbinati possono accedere a DSH.",
|
|
263
|
+
remoteError: "Connessione remota non stabilita. Riconnettiti; la rete locale continua a funzionare.",
|
|
264
|
+
funnelPermission: "Accesso completato. Autorizza Funnel per stabilire automaticamente la connessione remota.",
|
|
265
|
+
funnelHttps: "Accesso completato. Autorizza Funnel; la pagina ufficiale abilita anche HTTPS.",
|
|
266
|
+
funnelStart: "Accesso completato. Termina la prima autorizzazione Tailscale Funnel.",
|
|
267
|
+
tailscaleDnsMissing: "Tailscale non ha fornito un indirizzo remoto. Riconnettiti e verifica di aver completato l’accesso.",
|
|
268
|
+
gatewayStartFailed: "Avvio del gateway remoto non riuscito. Riconnettiti; la rete locale non è interessata.",
|
|
269
|
+
controlChannelFailed: "Connessione al componente remoto interrotta. Riconnettiti.",
|
|
270
|
+
cpolarMissing: "Il componente ufficiale cpolar non è installato. Completa i passaggi sopra.",
|
|
271
|
+
cpolarInvalid: "Verifica del componente cpolar non riuscita. Rimuovilo completamente e reinstallalo.",
|
|
272
|
+
cpolarConfigMissing: "Nessun token account cpolar salvato. Completa i passaggi sopra.",
|
|
273
|
+
cpolarConfigInvalid: "Configurazione locale cpolar non valida. Salva nuovamente il token.",
|
|
274
|
+
cpolarPortUnavailable: "Impossibile assegnare una porta locale al gateway remoto. Riprova.",
|
|
275
|
+
cpolarLaunchFailed: "Avvio del client cpolar non riuscito.",
|
|
276
|
+
cpolarTimeout: "Connessione al nodo cpolar scaduta. Riconnettiti.",
|
|
277
|
+
cpolarStopped: "Connessione cpolar arrestata.",
|
|
278
|
+
cpolarExited: "Connessione cpolar terminata inaspettatamente. Riconnettiti.",
|
|
279
|
+
cpolarOutputInvalid: "cpolar ha restituito uno stato non riconosciuto.",
|
|
280
|
+
cpolarOriginInvalid: "L’indirizzo pubblico restituito da cpolar non ha superato la verifica.",
|
|
281
|
+
setupOpened: "La pagina ufficiale Tailscale è aperta. Torna in DSH dopo aver abilitato Funnel; la riconnessione è automatica.",
|
|
282
|
+
switchProviderConfirm: "Il cambio di metodo disattiva prima il canale remoto corrente. Rete locale e dispositivi abbinati non sono interessati. Continuare?",
|
|
283
|
+
switchingCpolar: "Passaggio a cpolar…",
|
|
284
|
+
switchingTailscale: "Passaggio a Tailscale Funnel…",
|
|
285
|
+
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.",
|
|
286
|
+
downloading: "Download e verifica…",
|
|
287
|
+
installingCpolar: "Installazione del componente ufficiale cpolar. Mantieni DSH in esecuzione fino al termine.",
|
|
288
|
+
installFailed: "Installazione componente non riuscita: {error}",
|
|
289
|
+
invalidToken: "Incolla l’Authtoken completo dalla dashboard cpolar.",
|
|
290
|
+
configuredConnecting: "Configurazione account salvata; connessione tramite cpolar…",
|
|
291
|
+
configureFailed: "Configurazione non riuscita: {error}",
|
|
292
|
+
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.",
|
|
293
|
+
purging: "Arresto del canale e rimozione dei file cpolar gestiti da DSH Mobile…",
|
|
294
|
+
purgeFailed: "Pulizia non riuscita: {error}",
|
|
295
|
+
reconnectingCpolar: "Riconnessione a un nodo cpolar…",
|
|
296
|
+
reconnectingTailscale: "Controllo impostazioni Tailscale e riconnessione…",
|
|
297
|
+
remoteQrReady: "QR di abbinamento remoto generato. Scansionalo da “Accesso remoto” nell’app.",
|
|
298
|
+
resetCpolarConfirm: "Disattivare il canale remoto cpolar e rimuovere tutti i dispositivi remoti? Account cpolar e altri tunnel restano invariati.",
|
|
299
|
+
resetTailscaleConfirm: "Uscire da Tailscale su questo computer e rimuovere tutti i dispositivi remoti? La configurazione della rete locale resta invariata.",
|
|
300
|
+
requestFailed: "Richiesta non riuscita: {error}"
|
|
301
|
+
},
|
|
302
|
+
zh: {}
|
|
303
|
+
};
|
|
304
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
|
|
305
|
+
mobileAccess: "移动访问",
|
|
306
|
+
collapseMobileAccess: "收起移动访问",
|
|
307
|
+
downloadAndroid: "下载最新 Android App",
|
|
308
|
+
downloadAndroidVersion: "下载最新 Android App · v{version}",
|
|
309
|
+
downloadAndroidAria: "从 GitHub Releases 下载最新版 Android App",
|
|
310
|
+
downloadAndroidVersionAria: "从 GitHub Releases 下载 Android App v{version}",
|
|
311
|
+
updatePlugin: "更新插件",
|
|
312
|
+
updatePluginAria: "将 DSH Mobile 更新至 v{version}",
|
|
313
|
+
updatingPlugin: "正在更新…",
|
|
314
|
+
pluginUpdatedRestart: "插件已更新至 v{version},重启 DSH 后生效。",
|
|
315
|
+
pluginUpdateFailed: "更新失败:{error}",
|
|
316
|
+
lan: "局域网",
|
|
317
|
+
remote: "远程",
|
|
318
|
+
lanAccess: "局域网访问",
|
|
319
|
+
remoteAccess: "远程访问",
|
|
320
|
+
browserAccess: "浏览器访问",
|
|
321
|
+
remoteAddress: "远程地址",
|
|
322
|
+
loadingStatus: "正在读取状态…",
|
|
323
|
+
loadingRemoteStatus: "正在读取远程状态…",
|
|
324
|
+
generateCopyKey: "生成并复制密钥",
|
|
325
|
+
copyPairLink: "复制配对链接",
|
|
326
|
+
managePairedDevices: "管理配对设备",
|
|
327
|
+
clearAllDevices: "清除所有设备",
|
|
328
|
+
pairingQr: "配对二维码",
|
|
329
|
+
remoteIntro: "选择更适合你的远程通道。切换或关闭远程访问不会影响局域网。",
|
|
330
|
+
chooseProvider: "选择连接方式",
|
|
331
|
+
currentProvider: "当前连接",
|
|
332
|
+
remoteStateOff: "未启用",
|
|
333
|
+
remoteStateConnecting: "连接中",
|
|
334
|
+
remoteStateAttention: "需处理",
|
|
335
|
+
providerInfoAria: "查看远程连接安全与网络说明",
|
|
336
|
+
providerGroupAria: "远程连接方式",
|
|
337
|
+
providerSafeTitle: "你始终可以放心",
|
|
338
|
+
providerSafeText: "只有已配对设备能进入 DSH。cpolar 按需安装并可彻底清理;Tailscale 在中国大陆网络下可能连接缓慢、中断或无法使用,国内网络建议优先尝试 cpolar。",
|
|
339
|
+
builtIn: "内置",
|
|
340
|
+
mainlandPreferred: "国内网络优先",
|
|
341
|
+
tailscaleDescription: "覆盖更广;中国大陆网络可能不稳定,首次需登录并允许 Funnel。",
|
|
342
|
+
cpolarDescription: "按需安装官方组件,适合国内网络环境。",
|
|
343
|
+
prepareCpolar: "准备 cpolar",
|
|
344
|
+
checkingComponent: "正在检查组件…",
|
|
345
|
+
installOfficial: "安装官方组件",
|
|
346
|
+
cpolarAccountNote: "登录 cpolar 官网后复制 Authtoken。令牌只保存在本机插件私有目录,不会显示在页面或日志中。",
|
|
347
|
+
registerCpolar: "注册 cpolar",
|
|
348
|
+
openDashboard: "打开控制台获取令牌",
|
|
349
|
+
tokenPlaceholder: "粘贴 cpolar Authtoken",
|
|
350
|
+
saveConnect: "保存并连接",
|
|
351
|
+
saving: "正在保存…",
|
|
352
|
+
componentDetails: "组件来源与清理说明",
|
|
353
|
+
componentDetailsText: "仅在你点击安装后从 cpolar 官网下载并校验固定版本。不会写入系统服务、开机启动、注册表或 PATH。",
|
|
354
|
+
pluginPrivateDirectory: "插件私有目录",
|
|
355
|
+
officialDownload: "官方下载安装页",
|
|
356
|
+
terms: "服务条款",
|
|
357
|
+
purgeCpolar: "彻底移除 cpolar 组件与配置",
|
|
358
|
+
tailscaleHelp: "Tailscale 使用说明",
|
|
359
|
+
tailscaleHelpText: "运行组件已随插件提供。首次连接会打开 Tailscale 官方登录和 Funnel 授权页;插件不会接触你的账号密码。",
|
|
360
|
+
funnelGuideAria: "Tailscale Funnel 启用步骤",
|
|
361
|
+
funnelGuideTitle: "远程访问设置 · 第 2 步",
|
|
362
|
+
funnelGuideSummary: "Tailscale 登录已完成。还需为这台电脑允许 Funnel,官方页面会同时启用 HTTPS。",
|
|
363
|
+
funnelStep1: "打开当前节点的 Tailscale 官方授权页。",
|
|
364
|
+
funnelStep2: "确认启用 Funnel;无需再次登录 DSH。",
|
|
365
|
+
funnelStep3: "返回 DSH,插件会自动检查并建立连接。",
|
|
366
|
+
funnelGuideNote: "需要使用 Owner、Admin 或 Network admin 账号。",
|
|
367
|
+
continueFunnel: "继续完成 Funnel 授权",
|
|
368
|
+
retryNow: "已完成,立即重试",
|
|
369
|
+
enableRemote: "启用远程访问",
|
|
370
|
+
disableRemote: "关闭远程访问",
|
|
371
|
+
continueLogin: "继续登录",
|
|
372
|
+
reconnect: "重新连接",
|
|
373
|
+
generateRemoteQr: "生成远程配对二维码",
|
|
374
|
+
manageRemoteDevices: "管理远程设备",
|
|
375
|
+
resetRemoteLogin: "退出并清除远程登录",
|
|
376
|
+
resetRemoteDevices: "关闭并清除远程设备",
|
|
377
|
+
lanOn: "局域网访问已开启。",
|
|
378
|
+
lanOff: "局域网访问已关闭。",
|
|
379
|
+
enableLan: "开启局域网访问",
|
|
380
|
+
disableLan: "关闭局域网访问",
|
|
381
|
+
extensionsLoaded: "扩展:{loaded} 个已加载",
|
|
382
|
+
extensionsFailed: "扩展:{loaded} 个已加载,{failed} 个加载失败",
|
|
383
|
+
keyGenerationFailed: "无法生成配对密钥。",
|
|
384
|
+
keyCopied: "配对密钥已复制,请粘贴到 Android App。",
|
|
385
|
+
linkCopied: "配对链接已复制,发给手机后 App 粘贴或浏览器打开即可配对。",
|
|
386
|
+
copySecret: "请复制{kind}:{value}",
|
|
387
|
+
pairingKey: "配对密钥",
|
|
388
|
+
pairingLink: "配对链接",
|
|
389
|
+
noDevices: "暂无配对设备。",
|
|
390
|
+
noRemoteDevices: "暂无远程配对设备。",
|
|
391
|
+
device: "设备",
|
|
392
|
+
expires: "到期 {time}",
|
|
393
|
+
revoke: "撤销",
|
|
394
|
+
confirmResetDevices: "确定要移除所有配对设备吗?此操作会立即终止已连接设备。",
|
|
395
|
+
ready: "已就绪",
|
|
396
|
+
installed: "已安装",
|
|
397
|
+
installWithSize: "安装官方组件 · {size} MB",
|
|
398
|
+
cpolarUnsupported: "当前仅支持 Windows x64。你仍可选择内置的 Tailscale Funnel。",
|
|
399
|
+
cpolarNotInstalled: "尚未安装。只有点击下方按钮后,才会从 cpolar 官网下载固定版本。",
|
|
400
|
+
cpolarNeedsToken: "官方组件 {version} 已校验,下一步只需保存账号令牌。",
|
|
401
|
+
cpolarReady: "官方组件 {version} 与本机账号配置已就绪。",
|
|
402
|
+
remoteOff: "远程访问未启用。局域网访问不受影响。",
|
|
403
|
+
remoteUnavailableCpolar: "cpolar 尚未安装或未完成本机账号配置。",
|
|
404
|
+
remoteUnavailableTailscale: "当前电脑缺少 Funnel 运行组件,请重新安装完整插件包。",
|
|
405
|
+
remoteStartingCpolar: "正在连接 cpolar 国内节点…",
|
|
406
|
+
remoteStartingTailscale: "正在启动 Tailscale 安全通道…",
|
|
407
|
+
remoteNeedsLogin: "需要在浏览器完成一次 Tailscale 登录。插件不会读取你的密码。",
|
|
408
|
+
remoteConnectingCpolar: "公网地址已分配,正在启动 DSH 认证网关…",
|
|
409
|
+
remoteConnectingTailscale: "登录完成,正在建立公开 HTTPS 地址…",
|
|
410
|
+
remoteReady: "远程访问已就绪。只有已配对设备可以进入 DSH。",
|
|
411
|
+
remoteError: "远程连接未建立。可重新连接,局域网访问仍可正常使用。",
|
|
412
|
+
funnelPermission: "登录已完成。请继续授权 Funnel,完成后会自动建立远程连接。",
|
|
413
|
+
funnelHttps: "登录已完成。请继续授权 Funnel,官方页面会同时启用 HTTPS。",
|
|
414
|
+
funnelStart: "登录已完成。请继续完成 Tailscale Funnel 的首次授权。",
|
|
415
|
+
tailscaleDnsMissing: "Tailscale 暂未提供远程地址。请重新连接并确认已完成登录。",
|
|
416
|
+
gatewayStartFailed: "远程网关启动失败。请重新连接,局域网访问不受影响。",
|
|
417
|
+
controlChannelFailed: "远程组件连接中断。请重新连接。",
|
|
418
|
+
cpolarMissing: "cpolar 官方组件尚未安装。请先完成上方准备步骤。",
|
|
419
|
+
cpolarInvalid: "cpolar 组件校验失败。请彻底移除后重新安装。",
|
|
420
|
+
cpolarConfigMissing: "cpolar 尚未保存账号令牌。请先完成上方准备步骤。",
|
|
421
|
+
cpolarConfigInvalid: "cpolar 本机配置无效。请重新保存账号令牌。",
|
|
422
|
+
cpolarPortUnavailable: "无法分配本机远程网关端口,请重试。",
|
|
423
|
+
cpolarLaunchFailed: "cpolar 客户端未能启动。",
|
|
424
|
+
cpolarTimeout: "连接 cpolar 国内节点超时,请重新连接。",
|
|
425
|
+
cpolarStopped: "cpolar 连接已停止。",
|
|
426
|
+
cpolarExited: "cpolar 连接意外退出,请重新连接。",
|
|
427
|
+
cpolarOutputInvalid: "cpolar 返回了无法识别的状态。",
|
|
428
|
+
cpolarOriginInvalid: "cpolar 返回的公网地址未通过校验。",
|
|
429
|
+
setupOpened: "Tailscale 官方页面已打开。完成启用后返回 DSH,这里会自动重新连接。",
|
|
430
|
+
switchProviderConfirm: "切换连接方式会先关闭当前远程通道。局域网和配对设备不会受影响,是否继续?",
|
|
431
|
+
switchingCpolar: "正在切换到 cpolar…",
|
|
432
|
+
switchingTailscale: "正在切换到 Tailscale Funnel…",
|
|
433
|
+
installConfirm: "将从 cpolar 官方网站下载并校验固定版本(约 7.3 MB),仅解压到 DSH Mobile 私有目录。不会安装系统服务、写入 PATH/注册表或设置开机启动。是否继续?",
|
|
434
|
+
downloading: "正在下载并校验…",
|
|
435
|
+
installingCpolar: "正在安装 cpolar 官方组件。完成前请保持 DSH 运行。",
|
|
436
|
+
installFailed: "组件安装失败:{error}",
|
|
437
|
+
invalidToken: "请粘贴 cpolar 控制台提供的完整 Authtoken。",
|
|
438
|
+
configuredConnecting: "账号配置已保存,正在建立 cpolar 远程通道…",
|
|
439
|
+
configureFailed: "配置失败:{error}",
|
|
440
|
+
purgeConfirm: "彻底移除 DSH Mobile 私有目录中的 cpolar 组件、令牌配置和运行日志?不会影响局域网、DSH 数据或系统中的其他程序。",
|
|
441
|
+
purging: "正在关闭通道并清理 DSH Mobile 管理的 cpolar 文件…",
|
|
442
|
+
purgeFailed: "清理失败:{error}",
|
|
443
|
+
reconnectingCpolar: "正在重新连接 cpolar 国内节点…",
|
|
444
|
+
reconnectingTailscale: "正在确认 Tailscale 设置并重新连接…",
|
|
445
|
+
remoteQrReady: "远程配对二维码已生成。请在 App 的“远程访问”中扫描。",
|
|
446
|
+
resetCpolarConfirm: "关闭 cpolar 远程通道并移除所有远程配对设备?不会修改你的 cpolar 账号或其他隧道。",
|
|
447
|
+
resetTailscaleConfirm: "退出电脑上的 Tailscale 登录并移除所有远程配对设备?局域网配置不会改变。",
|
|
448
|
+
requestFailed: "请求失败:{error}"
|
|
449
|
+
});
|
|
450
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.en, {
|
|
451
|
+
selfHostedConnections: "Self-hosted connection",
|
|
452
|
+
selfHostedDescription: "For users with a VPS and domain. It does not depend on public tunnel bandwidth.",
|
|
453
|
+
advanced: "Advanced",
|
|
454
|
+
frpName: "Self-hosted FRP",
|
|
455
|
+
frpDescription: "A restricted FRP channel for this DSH gateway only. You manage the VPS and public domain.",
|
|
456
|
+
prepareFrp: "Configure self-hosted FRP",
|
|
457
|
+
frpStep1Title: "1 · Connection details",
|
|
458
|
+
frpStep1Text: "Enter the VPS address, frps control port, shared token, and public HTTPS origin.",
|
|
459
|
+
frpServerAddress: "VPS address",
|
|
460
|
+
frpServerAddressPlaceholder: "frp.example.com",
|
|
461
|
+
frpServerPort: "frps port",
|
|
462
|
+
frpToken: "Shared token",
|
|
463
|
+
frpTokenPlaceholder: "At least 16 characters",
|
|
464
|
+
frpPublicOrigin: "Public HTTPS origin",
|
|
465
|
+
frpPublicOriginPlaceholder: "https://dsh.example.com",
|
|
466
|
+
frpStep2Title: "2 · Prepare the VPS",
|
|
467
|
+
frpStep2Text: "Copy the restricted frps and Caddy template, then apply it on your VPS.",
|
|
468
|
+
copyServerTemplate: "Copy server template",
|
|
469
|
+
templateCopied: "Server template copied. Apply frps.toml and the Caddyfile on the VPS.",
|
|
470
|
+
templateCopyFailed: "Could not copy the template. Check the four fields above.",
|
|
471
|
+
frpStep3Title: "3 · Prepare this computer",
|
|
472
|
+
frpStep3Text: "Download and verify the pinned official frpc client only when requested.",
|
|
473
|
+
installFrpc: "Install official frpc",
|
|
474
|
+
frpInstallConfirm: "Download and verify official frpc 0.70.1 (about {size} MB), extracting only frpc to the DSH Mobile private directory? No service, PATH entry, or startup item is added.",
|
|
475
|
+
installingFrp: "Installing the official frpc component. Keep DSH running until it completes.",
|
|
476
|
+
frpNotInstalled: "frpc is not installed. Nothing is downloaded until you click Install.",
|
|
477
|
+
frpUnsupported: "This desktop platform is not supported by the managed frpc installer.",
|
|
478
|
+
frpComponentReady: "Official frpc {version} is verified.",
|
|
479
|
+
frpStep4Title: "4 · Verify and connect",
|
|
480
|
+
frpStep4Text: "The plugin starts only its DSH HTTP vhost and verifies the public endpoint belongs to this computer.",
|
|
481
|
+
frpAppRequirement: "Custom remote domains require Android app 0.3.3 or later; local network, cpolar, and Tailscale remain compatible with older supported apps.",
|
|
482
|
+
frpSaveConnect: "Save and verify connection",
|
|
483
|
+
frpSavingConnecting: "Saving the private settings and verifying the complete FRP path…",
|
|
484
|
+
frpConfigurationReady: "Server settings are saved. The token remains only in the plugin private directory.",
|
|
485
|
+
frpConfigurationMissing: "Complete all fields, copy the VPS template, then save and connect.",
|
|
486
|
+
frpInputInvalid: "Check the VPS address, port, HTTPS origin, and token.",
|
|
487
|
+
frpComponentDetails: "FRP source and complete cleanup",
|
|
488
|
+
frpComponentDetailsText: "The pinned client is downloaded from the official fatedier/frp release. DSH Mobile never installs a service or accepts arbitrary FRP configuration.",
|
|
489
|
+
frpOfficialRelease: "Official FRP release",
|
|
490
|
+
purgeFrp: "Remove FRP component and private configuration",
|
|
491
|
+
purgeFrpConfirm: "Stop FRP and remove frpc, the shared token, generated configuration, staging files, and logs owned by DSH Mobile? The VPS is unchanged.",
|
|
492
|
+
purgingFrp: "Stopping FRP and removing files managed by DSH Mobile…",
|
|
493
|
+
switchingFrp: "Switching to self-hosted FRP…",
|
|
494
|
+
reconnectingFrp: "Verifying and reconnecting self-hosted FRP…",
|
|
495
|
+
resetFrpConfirm: "Disable self-hosted FRP and remove all remote paired devices? The saved VPS settings and server are unchanged.",
|
|
496
|
+
remoteUnavailableFrp: "frpc is not installed or the private server settings are incomplete.",
|
|
497
|
+
remoteStartingFrp: "Checking the VPS and preparing the restricted FRP channel…",
|
|
498
|
+
remoteConnectingFrp: "frpc is running; verifying the public HTTPS endpoint…",
|
|
499
|
+
frpMissing: "Install the official frpc component first.",
|
|
500
|
+
frpInvalid: "frpc verification failed. Remove it completely and reinstall.",
|
|
501
|
+
frpConfigMissing: "Save the self-hosted FRP connection details first.",
|
|
502
|
+
frpConfigVerifyFailed: "frpc rejected the generated configuration. Check the server details and token.",
|
|
503
|
+
frpVhostPublic: "The VPS plaintext HTTP vhost is publicly reachable. Bind it to 127.0.0.1 and let Caddy provide HTTPS.",
|
|
504
|
+
frpVhostProbeFailed: "The VPS address could not be checked. Confirm DNS and retry.",
|
|
505
|
+
frpLaunchFailed: "frpc failed to start.",
|
|
506
|
+
frpTimeout: "The public endpoint did not become ready. Check frps, Caddy, DNS, and firewall.",
|
|
507
|
+
frpDiscoveryMismatch: "The public domain points to another DSH computer. Check the Caddy and frps mappings.",
|
|
508
|
+
frpDiscoveryInvalid: "The public domain did not return a valid DSH Mobile discovery response.",
|
|
509
|
+
frpStopped: "The FRP connection stopped.",
|
|
510
|
+
frpExited: "frpc exited unexpectedly. Check the VPS configuration and reconnect."
|
|
511
|
+
});
|
|
512
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.it, {
|
|
513
|
+
selfHostedConnections: "Connessione autogestita",
|
|
514
|
+
selfHostedDescription: "Per chi dispone di VPS e dominio. Non dipende dalla banda dei tunnel pubblici.",
|
|
515
|
+
advanced: "Avanzato",
|
|
516
|
+
frpName: "FRP autogestito",
|
|
517
|
+
frpDescription: "Canale FRP limitato al solo gateway DSH. VPS e dominio pubblico restano sotto il tuo controllo.",
|
|
518
|
+
prepareFrp: "Configura FRP autogestito",
|
|
519
|
+
frpStep1Title: "1 · Dati di connessione",
|
|
520
|
+
frpStep1Text: "Inserisci indirizzo VPS, porta di controllo frps, token condiviso e origine HTTPS pubblica.",
|
|
521
|
+
frpServerAddress: "Indirizzo VPS",
|
|
522
|
+
frpServerAddressPlaceholder: "frp.example.com",
|
|
523
|
+
frpServerPort: "Porta frps",
|
|
524
|
+
frpToken: "Token condiviso",
|
|
525
|
+
frpTokenPlaceholder: "Almeno 16 caratteri",
|
|
526
|
+
frpPublicOrigin: "Origine HTTPS pubblica",
|
|
527
|
+
frpPublicOriginPlaceholder: "https://dsh.example.com",
|
|
528
|
+
frpStep2Title: "2 · Prepara il VPS",
|
|
529
|
+
frpStep2Text: "Copia il modello limitato per frps e Caddy e applicalo sul VPS.",
|
|
530
|
+
copyServerTemplate: "Copia modello server",
|
|
531
|
+
templateCopied: "Modello server copiato. Applica frps.toml e Caddyfile sul VPS.",
|
|
532
|
+
templateCopyFailed: "Impossibile copiare il modello. Controlla i quattro campi sopra.",
|
|
533
|
+
frpStep3Title: "3 · Prepara questo computer",
|
|
534
|
+
frpStep3Text: "Scarica e verifica il client frpc ufficiale fissato solo su richiesta.",
|
|
535
|
+
installFrpc: "Installa frpc ufficiale",
|
|
536
|
+
frpInstallConfirm: "Scaricare e verificare frpc ufficiale 0.70.1 (circa {size} MB), estraendo solo frpc nella directory privata DSH Mobile? Non vengono aggiunti servizi, PATH o avvio automatico.",
|
|
537
|
+
installingFrp: "Installazione del componente frpc ufficiale. Mantieni DSH in esecuzione.",
|
|
538
|
+
frpNotInstalled: "frpc non è installato. Non viene scaricato nulla finché non scegli Installa.",
|
|
539
|
+
frpUnsupported: "Questa piattaforma desktop non è supportata dal programma di installazione gestito.",
|
|
540
|
+
frpComponentReady: "frpc ufficiale {version} verificato.",
|
|
541
|
+
frpStep4Title: "4 · Verifica e connetti",
|
|
542
|
+
frpStep4Text: "Il plugin avvia solo il vhost HTTP DSH e verifica che l’endpoint pubblico appartenga a questo computer.",
|
|
543
|
+
frpAppRequirement: "I domini remoti personalizzati richiedono l’app Android 0.3.3 o successiva; rete locale, cpolar e Tailscale restano compatibili con le app precedenti supportate.",
|
|
544
|
+
frpSaveConnect: "Salva e verifica connessione",
|
|
545
|
+
frpSavingConnecting: "Salvataggio privato e verifica del percorso FRP completo…",
|
|
546
|
+
frpConfigurationReady: "Impostazioni server salvate. Il token resta solo nella directory privata del plugin.",
|
|
547
|
+
frpConfigurationMissing: "Completa i campi, copia il modello VPS, quindi salva e connetti.",
|
|
548
|
+
frpInputInvalid: "Controlla indirizzo VPS, porta, origine HTTPS e token.",
|
|
549
|
+
frpComponentDetails: "Origine FRP e rimozione completa",
|
|
550
|
+
frpComponentDetailsText: "Il client fissato viene scaricato dalla release ufficiale fatedier/frp. DSH Mobile non installa servizi e non accetta configurazioni FRP arbitrarie.",
|
|
551
|
+
frpOfficialRelease: "Release FRP ufficiale",
|
|
552
|
+
purgeFrp: "Rimuovi componente FRP e configurazione privata",
|
|
553
|
+
purgeFrpConfirm: "Arrestare FRP e rimuovere frpc, token, configurazione, file temporanei e log gestiti da DSH Mobile? Il VPS non viene modificato.",
|
|
554
|
+
purgingFrp: "Arresto FRP e rimozione dei file gestiti da DSH Mobile…",
|
|
555
|
+
switchingFrp: "Passaggio a FRP autogestito…",
|
|
556
|
+
reconnectingFrp: "Verifica e riconnessione FRP autogestito…",
|
|
557
|
+
resetFrpConfirm: "Disattivare FRP autogestito e rimuovere tutti i dispositivi remoti? Impostazioni VPS e server restano invariati.",
|
|
558
|
+
remoteUnavailableFrp: "frpc non è installato o le impostazioni private sono incomplete.",
|
|
559
|
+
remoteStartingFrp: "Controllo VPS e preparazione del canale FRP limitato…",
|
|
560
|
+
remoteConnectingFrp: "frpc è in esecuzione; verifica dell’endpoint HTTPS pubblico…",
|
|
561
|
+
frpMissing: "Installa prima il componente frpc ufficiale.",
|
|
562
|
+
frpInvalid: "Verifica frpc non riuscita. Rimuovilo e reinstallalo.",
|
|
563
|
+
frpConfigMissing: "Salva prima i dati della connessione FRP.",
|
|
564
|
+
frpConfigVerifyFailed: "frpc ha rifiutato la configurazione generata. Controlla server e token.",
|
|
565
|
+
frpVhostPublic: "Il vhost HTTP in chiaro è raggiungibile pubblicamente. Limitalo a 127.0.0.1 e usa Caddy per HTTPS.",
|
|
566
|
+
frpVhostProbeFailed: "Impossibile controllare il VPS. Verifica il DNS e riprova.",
|
|
567
|
+
frpLaunchFailed: "Avvio di frpc non riuscito.",
|
|
568
|
+
frpTimeout: "Endpoint pubblico non pronto. Controlla frps, Caddy, DNS e firewall.",
|
|
569
|
+
frpDiscoveryMismatch: "Il dominio pubblico punta a un altro computer DSH. Controlla Caddy e frps.",
|
|
570
|
+
frpDiscoveryInvalid: "Il dominio pubblico non ha restituito una risposta DSH Mobile valida.",
|
|
571
|
+
frpStopped: "Connessione FRP arrestata.",
|
|
572
|
+
frpExited: "frpc è terminato inaspettatamente. Controlla il VPS e riconnettiti."
|
|
573
|
+
});
|
|
574
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
|
|
575
|
+
selfHostedConnections: "自建连接",
|
|
576
|
+
selfHostedDescription: "适合已有 VPS 和域名的用户,不受公共服务带宽限制。",
|
|
577
|
+
advanced: "高级",
|
|
578
|
+
frpName: "自建 FRP",
|
|
579
|
+
frpDescription: "只为当前 DSH 网关建立单用途 FRP 通道,VPS 和公开域名由你掌控。",
|
|
580
|
+
prepareFrp: "配置自建 FRP",
|
|
581
|
+
frpStep1Title: "1 · 填写连接信息",
|
|
582
|
+
frpStep1Text: "填写 VPS 地址、frps 控制端口、共享 Token 和公开 HTTPS 地址。",
|
|
583
|
+
frpServerAddress: "VPS 地址",
|
|
584
|
+
frpServerAddressPlaceholder: "frp.example.com",
|
|
585
|
+
frpServerPort: "frps 端口",
|
|
586
|
+
frpToken: "共享 Token",
|
|
587
|
+
frpTokenPlaceholder: "至少 16 个字符",
|
|
588
|
+
frpPublicOrigin: "公开 HTTPS 地址",
|
|
589
|
+
frpPublicOriginPlaceholder: "https://dsh.example.com",
|
|
590
|
+
frpStep2Title: "2 · 准备 VPS",
|
|
591
|
+
frpStep2Text: "复制受限的 frps 与 Caddy 模板,并在 VPS 上应用。",
|
|
592
|
+
copyServerTemplate: "复制服务器模板",
|
|
593
|
+
templateCopied: "服务器模板已复制,请在 VPS 应用 frps.toml 和 Caddyfile。",
|
|
594
|
+
templateCopyFailed: "无法复制模板,请检查上方四项信息。",
|
|
595
|
+
frpStep3Title: "3 · 准备这台电脑",
|
|
596
|
+
frpStep3Text: "仅在你确认后下载并校验固定版本的官方 frpc。",
|
|
597
|
+
installFrpc: "安装官方 frpc",
|
|
598
|
+
frpInstallConfirm: "从 FRP 官方 Release 下载并校验 frpc 0.70.1(约 {size} MB),只提取 frpc 到 DSH Mobile 私有目录?不会安装服务、写入 PATH 或设置开机启动。",
|
|
599
|
+
installingFrp: "正在安装官方 frpc,完成前请保持 DSH 运行。",
|
|
600
|
+
frpNotInstalled: "frpc 尚未安装。点击安装前不会下载任何内容。",
|
|
601
|
+
frpUnsupported: "当前桌面平台暂不支持托管安装 frpc。",
|
|
602
|
+
frpComponentReady: "官方 frpc {version} 已校验。",
|
|
603
|
+
frpStep4Title: "4 · 验证并连接",
|
|
604
|
+
frpStep4Text: "插件只启动 DSH 的 HTTP vhost,并确认公网端点确实属于这台电脑。",
|
|
605
|
+
frpAppRequirement: "自定义远程域名需要 Android App 0.3.3 或更高版本;局域网、cpolar 和 Tailscale 仍兼容此前受支持的 App。",
|
|
606
|
+
frpSaveConnect: "保存并验证连接",
|
|
607
|
+
frpSavingConnecting: "正在保存私有配置并验证完整 FRP 链路…",
|
|
608
|
+
frpConfigurationReady: "服务器配置已保存,Token 只保存在插件私有目录。",
|
|
609
|
+
frpConfigurationMissing: "请填写完整信息、复制 VPS 模板,然后保存并连接。",
|
|
610
|
+
frpInputInvalid: "请检查 VPS 地址、端口、HTTPS 地址和 Token。",
|
|
611
|
+
frpComponentDetails: "FRP 来源与彻底清理",
|
|
612
|
+
frpComponentDetailsText: "固定版本客户端来自 fatedier/frp 官方 Release。DSH Mobile 不安装系统服务,也不接受任意 FRP 配置。",
|
|
613
|
+
frpOfficialRelease: "FRP 官方 Release",
|
|
614
|
+
purgeFrp: "彻底移除 FRP 组件与私有配置",
|
|
615
|
+
purgeFrpConfirm: "关闭 FRP 并移除 DSH Mobile 管理的 frpc、共享 Token、生成配置、临时文件和日志?不会修改 VPS。",
|
|
616
|
+
purgingFrp: "正在关闭 FRP 并清理 DSH Mobile 管理的文件…",
|
|
617
|
+
switchingFrp: "正在切换到自建 FRP…",
|
|
618
|
+
reconnectingFrp: "正在验证并重新连接自建 FRP…",
|
|
619
|
+
resetFrpConfirm: "关闭自建 FRP 并移除所有远程配对设备?已保存的 VPS 配置和服务器不会改变。",
|
|
620
|
+
remoteUnavailableFrp: "frpc 尚未安装或私有服务器配置不完整。",
|
|
621
|
+
remoteStartingFrp: "正在检查 VPS 并准备受限 FRP 通道…",
|
|
622
|
+
remoteConnectingFrp: "frpc 已启动,正在验证公网 HTTPS 端点…",
|
|
623
|
+
frpMissing: "请先安装官方 frpc。",
|
|
624
|
+
frpInvalid: "frpc 校验失败,请彻底清理后重新安装。",
|
|
625
|
+
frpConfigMissing: "请先保存自建 FRP 连接信息。",
|
|
626
|
+
frpConfigVerifyFailed: "frpc 拒绝了生成的配置,请检查服务器信息和 Token。",
|
|
627
|
+
frpVhostPublic: "VPS 明文 HTTP vhost 可被公网访问。请将其限制到 127.0.0.1,并由 Caddy 提供 HTTPS。",
|
|
628
|
+
frpVhostProbeFailed: "无法检查 VPS 地址,请确认 DNS 后重试。",
|
|
629
|
+
frpLaunchFailed: "frpc 未能启动。",
|
|
630
|
+
frpTimeout: "公网端点未能就绪,请检查 frps、Caddy、DNS 和防火墙。",
|
|
631
|
+
frpDiscoveryMismatch: "公开域名连接到了另一台 DSH 电脑,请检查 Caddy 与 frps 映射。",
|
|
632
|
+
frpDiscoveryInvalid: "公开域名没有返回有效的 DSH Mobile 发现信息。",
|
|
633
|
+
frpStopped: "FRP 连接已停止。",
|
|
634
|
+
frpExited: "frpc 意外退出,请检查 VPS 配置后重新连接。"
|
|
635
|
+
});
|
|
636
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.en, {
|
|
637
|
+
requestTimeout: "The operation timed out. Confirm that DSH is still running, then try again.",
|
|
638
|
+
diagnostics: "Diagnostics",
|
|
639
|
+
openDiagnostics: "Open connection diagnostics",
|
|
640
|
+
back: "Back",
|
|
641
|
+
backToMobile: "Back to Mobile access",
|
|
642
|
+
connectionDiagnostics: "Connection diagnostics",
|
|
643
|
+
diagnosticsIntro: "Check versions, gateway, network adapter, firewall, and remote channel. The report is automatically redacted and never reads conversations or credentials.",
|
|
644
|
+
diagnosticsNotRun: "Not checked yet",
|
|
645
|
+
diagnosticsStartHint: "Select the button below to begin.",
|
|
646
|
+
diagnosticsIdleMeta: "Waiting to run · connection status only",
|
|
647
|
+
diagnosticsStart: "Start check",
|
|
648
|
+
diagnosticsCopy: "Copy redacted report",
|
|
649
|
+
diagnosticsAdvanced: "Advanced diagnostic details",
|
|
650
|
+
diagnosticsComplete: "Check complete",
|
|
651
|
+
diagnosticsAttention: "Some items need attention",
|
|
652
|
+
diagnosticsProblem: "Connection problems found",
|
|
653
|
+
diagnosticsCompleteFallback: "The check is complete.",
|
|
654
|
+
diagnosticStatusOk: "OK",
|
|
655
|
+
diagnosticStatusWarning: "Attention",
|
|
656
|
+
diagnosticStatusError: "Problem",
|
|
657
|
+
diagnosticStatusInfo: "Info",
|
|
658
|
+
diagnosticItems: "{count} items",
|
|
659
|
+
diagnosticCheck: "Check",
|
|
660
|
+
diagnosticAction: "Suggested action",
|
|
661
|
+
diagnosticNeedsAction: "Needs action",
|
|
662
|
+
diagnosticDetails: "Check details",
|
|
663
|
+
diagnosticOther: "Other checks",
|
|
664
|
+
diagnosticNoBlockers: "{count} checks · no blocking issues found",
|
|
665
|
+
diagnosticNeedsCount: "{count} checks · {issues} need attention",
|
|
666
|
+
diagnosticsChecking: "Checking…",
|
|
667
|
+
diagnosticsCheckingTitle: "Checking connection",
|
|
668
|
+
diagnosticsCheckingText: "This usually takes a few seconds. The remote channel performs a real reachability test.",
|
|
669
|
+
diagnosticsRunningMeta: "Running · keep DSH online",
|
|
670
|
+
diagnosticsIncomplete: "Check not completed",
|
|
671
|
+
diagnosticsReadFailed: "Could not read diagnostics: {error}",
|
|
672
|
+
diagnosticsUnavailable: "Diagnostics service unavailable · try again later",
|
|
673
|
+
diagnosticsRetry: "Check again",
|
|
674
|
+
diagnosticsCopied: "Redacted report copied. You can paste it directly into an issue.",
|
|
675
|
+
diagnosticsCopyManual: "Clipboard access was denied. Details are expanded for manual copying.",
|
|
676
|
+
diagnosticLabelVersions: "Version compatibility",
|
|
677
|
+
diagnosticLabelNetwork: "Local network adapter",
|
|
678
|
+
diagnosticLabelLan: "Local network gateway",
|
|
679
|
+
diagnosticLabelFirewall: "Windows Firewall",
|
|
680
|
+
diagnosticLabelRemote: "Remote channel",
|
|
681
|
+
diagnosticLabelPhone: "Phone network",
|
|
682
|
+
funnelTimeout: "The Tailscale component timed out while starting. Check the network, then reconnect."
|
|
683
|
+
});
|
|
684
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.it, {
|
|
685
|
+
requestTimeout: "Operazione scaduta. Verifica che DSH sia ancora in esecuzione e riprova.",
|
|
686
|
+
diagnostics: "Diagnostica",
|
|
687
|
+
openDiagnostics: "Apri diagnostica connessione",
|
|
688
|
+
back: "Indietro",
|
|
689
|
+
backToMobile: "Torna ad Accesso mobile",
|
|
690
|
+
connectionDiagnostics: "Diagnostica connessione",
|
|
691
|
+
diagnosticsIntro: "Controlla versioni, gateway, scheda di rete, firewall e canale remoto. Il report viene anonimizzato automaticamente e non legge conversazioni o credenziali.",
|
|
692
|
+
diagnosticsNotRun: "Controllo non eseguito",
|
|
693
|
+
diagnosticsStartHint: "Premi il pulsante sotto per iniziare.",
|
|
694
|
+
diagnosticsIdleMeta: "In attesa · solo stato connessione",
|
|
695
|
+
diagnosticsStart: "Avvia controllo",
|
|
696
|
+
diagnosticsCopy: "Copia report anonimizzato",
|
|
697
|
+
diagnosticsAdvanced: "Dettagli diagnostici avanzati",
|
|
698
|
+
diagnosticsComplete: "Controllo completato",
|
|
699
|
+
diagnosticsAttention: "Alcuni elementi richiedono attenzione",
|
|
700
|
+
diagnosticsProblem: "Rilevati problemi di connessione",
|
|
701
|
+
diagnosticsCompleteFallback: "Controllo completato.",
|
|
702
|
+
diagnosticStatusOk: "OK",
|
|
703
|
+
diagnosticStatusWarning: "Attenzione",
|
|
704
|
+
diagnosticStatusError: "Problema",
|
|
705
|
+
diagnosticStatusInfo: "Informazione",
|
|
706
|
+
diagnosticItems: "{count} elementi",
|
|
707
|
+
diagnosticCheck: "Controllo",
|
|
708
|
+
diagnosticAction: "Suggerimento",
|
|
709
|
+
diagnosticNeedsAction: "Da risolvere",
|
|
710
|
+
diagnosticDetails: "Dettagli controllo",
|
|
711
|
+
diagnosticOther: "Altri controlli",
|
|
712
|
+
diagnosticNoBlockers: "{count} controlli · nessun problema bloccante",
|
|
713
|
+
diagnosticNeedsCount: "{count} controlli · {issues} richiedono attenzione",
|
|
714
|
+
diagnosticsChecking: "Controllo…",
|
|
715
|
+
diagnosticsCheckingTitle: "Controllo connessione",
|
|
716
|
+
diagnosticsCheckingText: "Di solito richiede pochi secondi. Il canale remoto esegue un test reale di raggiungibilità.",
|
|
717
|
+
diagnosticsRunningMeta: "In esecuzione · mantieni DSH online",
|
|
718
|
+
diagnosticsIncomplete: "Controllo non completato",
|
|
719
|
+
diagnosticsReadFailed: "Impossibile leggere la diagnostica: {error}",
|
|
720
|
+
diagnosticsUnavailable: "Servizio diagnostico non disponibile · riprova più tardi",
|
|
721
|
+
diagnosticsRetry: "Ripeti controllo",
|
|
722
|
+
diagnosticsCopied: "Report anonimizzato copiato. Puoi incollarlo direttamente in una issue.",
|
|
723
|
+
diagnosticsCopyManual: "Il browser ha negato la copia. I dettagli sono stati aperti per la copia manuale.",
|
|
724
|
+
diagnosticLabelVersions: "Compatibilità versioni",
|
|
725
|
+
diagnosticLabelNetwork: "Scheda rete locale",
|
|
726
|
+
diagnosticLabelLan: "Gateway rete locale",
|
|
727
|
+
diagnosticLabelFirewall: "Windows Firewall",
|
|
728
|
+
diagnosticLabelRemote: "Canale remoto",
|
|
729
|
+
diagnosticLabelPhone: "Rete telefono",
|
|
730
|
+
funnelTimeout: "Avvio del componente Tailscale scaduto. Controlla la rete e riconnettiti."
|
|
731
|
+
});
|
|
732
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
|
|
733
|
+
requestTimeout: "操作超时,请确认 DSH 仍在运行后重试。",
|
|
734
|
+
diagnostics: "诊断",
|
|
735
|
+
openDiagnostics: "打开连接诊断",
|
|
736
|
+
back: "返回",
|
|
737
|
+
backToMobile: "返回移动访问",
|
|
738
|
+
connectionDiagnostics: "连接诊断",
|
|
739
|
+
diagnosticsIntro: "检查版本、网关、网卡、防火墙和远程通道。报告自动脱敏,不读取对话或凭据。",
|
|
740
|
+
diagnosticsNotRun: "尚未检查",
|
|
741
|
+
diagnosticsStartHint: "点击下方按钮开始。",
|
|
742
|
+
diagnosticsIdleMeta: "等待运行 · 仅收集连接状态",
|
|
743
|
+
diagnosticsStart: "开始检查",
|
|
744
|
+
diagnosticsCopy: "复制脱敏报告",
|
|
745
|
+
diagnosticsAdvanced: "高级诊断详情",
|
|
746
|
+
diagnosticsComplete: "检查完成",
|
|
747
|
+
diagnosticsAttention: "有项目需要留意",
|
|
748
|
+
diagnosticsProblem: "发现连接问题",
|
|
749
|
+
diagnosticsCompleteFallback: "检查已完成。",
|
|
750
|
+
diagnosticStatusOk: "正常",
|
|
751
|
+
diagnosticStatusWarning: "注意",
|
|
752
|
+
diagnosticStatusError: "问题",
|
|
753
|
+
diagnosticStatusInfo: "说明",
|
|
754
|
+
diagnosticItems: "{count} 项",
|
|
755
|
+
diagnosticCheck: "检查项",
|
|
756
|
+
diagnosticAction: "建议",
|
|
757
|
+
diagnosticNeedsAction: "需要处理",
|
|
758
|
+
diagnosticDetails: "检查详情",
|
|
759
|
+
diagnosticOther: "其他检查",
|
|
760
|
+
diagnosticNoBlockers: "{count} 项检查 · 未发现阻断问题",
|
|
761
|
+
diagnosticNeedsCount: "{count} 项检查 · {issues} 项需要处理",
|
|
762
|
+
diagnosticsChecking: "正在检查…",
|
|
763
|
+
diagnosticsCheckingTitle: "正在检查连接",
|
|
764
|
+
diagnosticsCheckingText: "通常几秒内完成。远程通道会执行一次真实可达性测试。",
|
|
765
|
+
diagnosticsRunningMeta: "正在运行 · 请保持 DSH 在线",
|
|
766
|
+
diagnosticsIncomplete: "检查未完成",
|
|
767
|
+
diagnosticsReadFailed: "无法读取诊断结果:{error}",
|
|
768
|
+
diagnosticsUnavailable: "诊断服务暂不可用 · 请稍后重试",
|
|
769
|
+
diagnosticsRetry: "重新检查",
|
|
770
|
+
diagnosticsCopied: "脱敏报告已复制,可直接粘贴到 Issue。",
|
|
771
|
+
diagnosticsCopyManual: "浏览器未允许复制,已展开详情,请手动复制。",
|
|
772
|
+
diagnosticLabelVersions: "版本兼容",
|
|
773
|
+
diagnosticLabelNetwork: "局域网网卡",
|
|
774
|
+
diagnosticLabelLan: "局域网网关",
|
|
775
|
+
diagnosticLabelFirewall: "Windows 防火墙",
|
|
776
|
+
diagnosticLabelRemote: "远程通道",
|
|
777
|
+
diagnosticLabelPhone: "手机网络",
|
|
778
|
+
funnelTimeout: "Tailscale 组件启动超时,请检查网络后重新连接。"
|
|
779
|
+
});
|
|
780
|
+
const LOCALIZED_DIAGNOSTIC_COPY = {
|
|
781
|
+
en: {
|
|
782
|
+
versions: "Installed plugin, DSH, and minimum Android app versions are shown.",
|
|
783
|
+
networkOk: "The configured local network adapter is available.",
|
|
784
|
+
networkError: "The saved local network adapter is unavailable.",
|
|
785
|
+
networkInfo: "A fixed local network configuration is in use.",
|
|
786
|
+
networkAction: "Run dsh-mobile setup again.",
|
|
787
|
+
lanOk: "The local gateway is listening and pairing is available.",
|
|
788
|
+
lanInfo: "Local network access is currently off.",
|
|
789
|
+
lanAction: "Enable local network access when the phone must connect directly.",
|
|
790
|
+
firewallOk: "Local TCP and discovery firewall rules are enabled.",
|
|
791
|
+
firewallWarning: "The complete local network firewall rules were not found.",
|
|
792
|
+
firewallInfo: "The system did not allow the plugin to read firewall status.",
|
|
793
|
+
firewallAction: "Run dsh-mobile setup as administrator and check the firewall rules.",
|
|
794
|
+
remoteOk: "The remote public address passed the reachability check.",
|
|
795
|
+
remoteWarning: "The remote channel needs attention or is still connecting.",
|
|
796
|
+
remoteError: "The remote connection is not currently reachable.",
|
|
797
|
+
remoteInfo: "Remote access is currently off.",
|
|
798
|
+
remoteAction: "Return to Remote access, follow the provider guidance, and reconnect.",
|
|
799
|
+
phone: "The computer cannot determine whether the router isolates the phone.",
|
|
800
|
+
phoneAction: "Confirm the phone and computer use the same network, then disable guest-network or AP isolation.",
|
|
801
|
+
reportTitle: "DSH Mobile diagnostic report",
|
|
802
|
+
generated: "Generated"
|
|
803
|
+
},
|
|
804
|
+
it: {
|
|
805
|
+
versions: "Sono indicate le versioni installate di plugin e DSH e la versione minima dell’app Android.",
|
|
806
|
+
networkOk: "La scheda di rete locale configurata è disponibile.",
|
|
807
|
+
networkError: "La scheda di rete locale salvata non è disponibile.",
|
|
808
|
+
networkInfo: "È in uso una configurazione di rete locale fissa.",
|
|
809
|
+
networkAction: "Esegui di nuovo dsh-mobile setup.",
|
|
810
|
+
lanOk: "Il gateway locale è in ascolto e l’abbinamento è disponibile.",
|
|
811
|
+
lanInfo: "L’accesso dalla rete locale è disattivato.",
|
|
812
|
+
lanAction: "Attiva l’accesso locale quando il telefono deve collegarsi direttamente.",
|
|
813
|
+
firewallOk: "Le regole firewall TCP locale e di rilevamento sono attive.",
|
|
814
|
+
firewallWarning: "Le regole firewall complete per la rete locale non sono state trovate.",
|
|
815
|
+
firewallInfo: "Il sistema non ha consentito al plugin di leggere lo stato del firewall.",
|
|
816
|
+
firewallAction: "Esegui dsh-mobile setup come amministratore e controlla le regole firewall.",
|
|
817
|
+
remoteOk: "L’indirizzo pubblico remoto ha superato il test di raggiungibilità.",
|
|
818
|
+
remoteWarning: "Il canale remoto richiede attenzione o è ancora in connessione.",
|
|
819
|
+
remoteError: "La connessione remota non è al momento raggiungibile.",
|
|
820
|
+
remoteInfo: "L’accesso remoto è disattivato.",
|
|
821
|
+
remoteAction: "Torna ad Accesso remoto, segui le indicazioni del provider e riconnettiti.",
|
|
822
|
+
phone: "Il computer non può stabilire se il router isola il telefono.",
|
|
823
|
+
phoneAction: "Verifica che telefono e computer usino la stessa rete, poi disattiva rete ospiti o isolamento AP.",
|
|
824
|
+
reportTitle: "Report diagnostico DSH Mobile",
|
|
825
|
+
generated: "Generato"
|
|
826
|
+
},
|
|
827
|
+
zh: {
|
|
828
|
+
reportTitle: "DSH Mobile 诊断报告",
|
|
829
|
+
generated: "生成时间"
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
const DIAGNOSTIC_REASON_MESSAGES = {
|
|
833
|
+
en: {
|
|
834
|
+
"versions-current": ["Installed plugin, DSH, and minimum Android app versions are shown.", ""],
|
|
835
|
+
"network-unavailable": ["The saved local network adapter is unavailable.", "Run dsh-mobile setup again."],
|
|
836
|
+
"network-interface": ["Using network interface {interfaceName}.", ""],
|
|
837
|
+
"network-fixed": ["A fixed local network configuration is in use.", ""],
|
|
838
|
+
"lan-ready": ["The local gateway is listening at {endpointSuffix}; pairing is available.", ""],
|
|
839
|
+
"lan-off": ["Local network access is currently off.", "Enable local network access when the phone must connect directly."],
|
|
840
|
+
"firewall-ready": ["Local TCP and discovery firewall rules are enabled.", ""],
|
|
841
|
+
"firewall-missing": ["The complete local network firewall rules were not found.", "Run dsh-mobile setup as administrator and check the firewall rules."],
|
|
842
|
+
"firewall-unknown": ["The system did not allow the plugin to read firewall status.", "If the phone cannot find this computer, run setup as administrator."],
|
|
843
|
+
"remote-off": ["Remote access through {provider} is currently off.", ""],
|
|
844
|
+
"remote-ready": ["{provider} endpoint {endpointSuffix} is reachable in about {latencyMs} ms.", ""],
|
|
845
|
+
"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."],
|
|
846
|
+
"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."],
|
|
847
|
+
"remote-unreachable": ["{provider} reports ready, but endpoint {endpointSuffix} is not reachable.", "Reconnect, then check the provider status if it still fails."],
|
|
848
|
+
"remote-needs-login": ["Tailscale is waiting for login to finish.", "Return to Remote access and continue login."],
|
|
849
|
+
"remote-connecting": ["The {provider} remote channel is still connecting.", "Wait briefly, then check again."],
|
|
850
|
+
"remote-controller-error": ["The {provider} controller reported {controllerCode}.", "Return to Remote access and reconnect."],
|
|
851
|
+
"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."]
|
|
852
|
+
},
|
|
853
|
+
it: {
|
|
854
|
+
"versions-current": ["Sono indicate le versioni installate di plugin e DSH e la versione minima dell’app Android.", ""],
|
|
855
|
+
"network-unavailable": ["La scheda di rete locale salvata non è disponibile.", "Esegui di nuovo dsh-mobile setup."],
|
|
856
|
+
"network-interface": ["È in uso l’interfaccia di rete {interfaceName}.", ""],
|
|
857
|
+
"network-fixed": ["È in uso una configurazione di rete locale fissa.", ""],
|
|
858
|
+
"lan-ready": ["Il gateway locale è in ascolto su {endpointSuffix}; l’abbinamento è disponibile.", ""],
|
|
859
|
+
"lan-off": ["L’accesso dalla rete locale è disattivato.", "Attiva l’accesso locale quando il telefono deve collegarsi direttamente."],
|
|
860
|
+
"firewall-ready": ["Le regole firewall TCP locale e di rilevamento sono attive.", ""],
|
|
861
|
+
"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."],
|
|
862
|
+
"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."],
|
|
863
|
+
"remote-off": ["L’accesso remoto tramite {provider} è disattivato.", ""],
|
|
864
|
+
"remote-ready": ["L’endpoint {provider} {endpointSuffix} è raggiungibile in circa {latencyMs} ms.", ""],
|
|
865
|
+
"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."],
|
|
866
|
+
"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."],
|
|
867
|
+
"remote-unreachable": ["{provider} risulta pronto, ma l’endpoint {endpointSuffix} non è raggiungibile.", "Riconnettiti; se il problema continua, controlla lo stato del provider."],
|
|
868
|
+
"remote-needs-login": ["Tailscale attende il completamento dell’accesso.", "Torna ad Accesso remoto e continua l’accesso."],
|
|
869
|
+
"remote-connecting": ["Il canale remoto {provider} è ancora in connessione.", "Attendi qualche istante e ripeti il controllo."],
|
|
870
|
+
"remote-controller-error": ["Il controller {provider} ha segnalato {controllerCode}.", "Torna ad Accesso remoto e riconnettiti."],
|
|
871
|
+
"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."]
|
|
872
|
+
},
|
|
873
|
+
zh: {
|
|
874
|
+
"versions-current": ["已显示插件、DSH 和 Android App 最低版本。", ""],
|
|
875
|
+
"network-unavailable": ["已保存的局域网网卡当前不可用。", "重新运行 dsh-mobile setup。"],
|
|
876
|
+
"network-interface": ["正在使用网卡 {interfaceName}。", ""],
|
|
877
|
+
"network-fixed": ["当前使用固定局域网配置。", ""],
|
|
878
|
+
"lan-ready": ["局域网网关正在监听 {endpointSuffix},配对入口可用。", ""],
|
|
879
|
+
"lan-off": ["局域网访问当前未开启。", "手机需要直连时开启局域网访问。"],
|
|
880
|
+
"firewall-ready": ["局域网 TCP 与发现防火墙规则已启用。", ""],
|
|
881
|
+
"firewall-missing": ["未找到完整的局域网防火墙规则。", "以管理员身份运行 dsh-mobile setup 并检查防火墙规则。"],
|
|
882
|
+
"firewall-unknown": ["系统未允许插件读取防火墙状态。", "若手机找不到电脑,请以管理员身份运行 setup。"],
|
|
883
|
+
"remote-off": ["{provider} 远程访问当前未启用。", ""],
|
|
884
|
+
"remote-ready": ["{provider} 端点 {endpointSuffix} 可达,往返约 {latencyMs} ms。", ""],
|
|
885
|
+
"remote-rate-limited": ["{provider} 端点 {endpointSuffix} 可达,但本次检查观察到服务限流。", "稍后重试;旧会话会按需加载以减少流量。"],
|
|
886
|
+
"remote-fake-ip": ["Tailscale 地址被当前 VPN 或 DNS 代理接管,TLS 链路未建立。", "切换 VPN 节点或代理模式;仍失败时改用 cpolar。"],
|
|
887
|
+
"remote-unreachable": ["{provider} 显示已就绪,但端点 {endpointSuffix} 暂不可达。", "点击重新连接;仍失败时检查提供方状态。"],
|
|
888
|
+
"remote-needs-login": ["Tailscale 正在等待完成登录。", "返回远程访问并继续登录。"],
|
|
889
|
+
"remote-connecting": ["{provider} 远程通道仍在连接。", "等待片刻后重新检查。"],
|
|
890
|
+
"remote-controller-error": ["{provider} 控制器报告 {controllerCode}。", "返回远程访问并重新连接。"],
|
|
891
|
+
"phone-network-unknown": ["电脑无法判断路由器是否隔离了手机。", "确认手机与电脑使用同一网络,并关闭访客网络或 AP 隔离。"]
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
//#endregion
|
|
895
|
+
//#region src/frp-template.ts
|
|
896
|
+
/** Loopback-only HTTP vhost port used between Caddy and frps. */
|
|
897
|
+
const FRP_VHOST_HTTP_PORT = 7080;
|
|
898
|
+
function publicDnsHostname(value) {
|
|
899
|
+
return value.length <= 253 && value.includes(".") && !/^[0-9.]+$/u.test(value) && !value.includes(":") && value.split(".").every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label));
|
|
900
|
+
}
|
|
901
|
+
/** Build the only supported frps and Caddy configuration from validated user inputs. */
|
|
902
|
+
function createRestrictedFrpServerTemplate(serverPort, token, publicOrigin) {
|
|
903
|
+
if (!Number.isSafeInteger(serverPort) || serverPort < 1 || serverPort > 65535 || token.length < 16 || token.length > 512 || /[\s\u0000-\u001f\u007f]/u.test(token)) throw new Error("frp_template_input_invalid");
|
|
904
|
+
let url;
|
|
905
|
+
try {
|
|
906
|
+
url = new URL(publicOrigin);
|
|
907
|
+
} catch {
|
|
908
|
+
throw new Error("frp_template_input_invalid");
|
|
909
|
+
}
|
|
910
|
+
if (url.protocol !== "https:" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "" || !publicDnsHostname(url.hostname)) throw new Error("frp_template_input_invalid");
|
|
911
|
+
return [
|
|
912
|
+
"# frps.toml",
|
|
913
|
+
`bindPort = ${String(serverPort)}`,
|
|
914
|
+
"proxyBindAddr = \"127.0.0.1\"",
|
|
915
|
+
`vhostHTTPPort = ${String(FRP_VHOST_HTTP_PORT)}`,
|
|
916
|
+
"auth.method = \"token\"",
|
|
917
|
+
`auth.token = ${JSON.stringify(token)}`,
|
|
918
|
+
"",
|
|
919
|
+
"# Caddyfile",
|
|
920
|
+
`${url.hostname} {`,
|
|
921
|
+
` reverse_proxy 127.0.0.1:${String(FRP_VHOST_HTTP_PORT)}`,
|
|
922
|
+
"}",
|
|
923
|
+
""
|
|
924
|
+
].join("\n");
|
|
925
|
+
}
|
|
926
|
+
//#endregion
|
|
8
927
|
//#region src/native-mobile.ts
|
|
9
928
|
/** Mobile feature and compatibility rules applied to DSH React surfaces. */
|
|
10
929
|
const NATIVE_MOBILE_STYLES = `
|
|
@@ -38,7 +957,8 @@ window.__ModuleLoader__.load({
|
|
|
38
957
|
[data-dsh-mobile-header] [class*="_headerUtilities"] { gap:2px !important; }
|
|
39
958
|
[data-dsh-mobile-header] [class*="_sessionLogButton"] { width:40px; min-width:40px; padding:0 !important; overflow:hidden; color:transparent; font-size:0 !important; }
|
|
40
959
|
[data-dsh-mobile-header] [class*="_sessionLogButton"] > * { display:none !important; }
|
|
41
|
-
[data-dsh-mobile-header] [class*="_sessionLogButton"]::after { color:var(--dsw-
|
|
960
|
+
[data-dsh-mobile-header] [class*="_sessionLogButton"]::after { color:var(--dsw-alias-label-primary, #171a21); content:"Log"; font-size:11px; font-weight:600; }
|
|
961
|
+
html[data-dsh-mobile-language="zh"] [data-dsh-mobile-header] [class*="_sessionLogButton"]::after { content:"日志"; }
|
|
42
962
|
[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
963
|
[data-dsh-mobile-header] [class*="_tab"] { padding-bottom:5px !important; }
|
|
44
964
|
[data-dsh-mobile-header] [class*="_tabs"]::-webkit-scrollbar { display:none; }
|
|
@@ -133,8 +1053,15 @@ window.__ModuleLoader__.load({
|
|
|
133
1053
|
[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
1054
|
[data-dsh-mobile-center] [role="dialog"][aria-label*="上下文"] :is(dt,dd),
|
|
135
1055
|
[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); }
|
|
1056
|
+
.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; }
|
|
1057
|
+
.dsh-mobile-branch-toast[data-visible="true"],.dsh-mobile-media-toast[data-visible="true"] { opacity:1; transform:translate(-50%,0); }
|
|
1058
|
+
.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%))); }
|
|
1059
|
+
.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; }
|
|
1060
|
+
.dsh-mobile-media-action:active { opacity:.72; }
|
|
1061
|
+
.dsh-mobile-media-action:focus-visible { outline:2px solid var(--dsw-alias-interactive-border-focus,#4c82f7); outline-offset:1px; }
|
|
1062
|
+
.dsh-mobile-media-action:disabled { cursor:default; opacity:.38; }
|
|
1063
|
+
.dsh-mobile-media-action svg { flex:none; width:16px; height:16px; color:var(--dsw-alias-label-tertiary,currentColor); }
|
|
1064
|
+
.dsh-mobile-media-action span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
138
1065
|
[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
1066
|
/* The desktop composer intentionally wraps whole toolbar groups. On a phone,
|
|
140
1067
|
dynamic model and status labels made that row alternate between one and
|
|
@@ -169,14 +1096,125 @@ window.__ModuleLoader__.load({
|
|
|
169
1096
|
function firstByClassSuffix(root, suffix) {
|
|
170
1097
|
return Array.from(root.querySelectorAll("[class]")).find((element) => classToken(element, suffix));
|
|
171
1098
|
}
|
|
1099
|
+
/** Find the stock DSH application frame without mistaking a feature card for the shell. */
|
|
1100
|
+
function resolveNativeMobileFrame(root, dedicatedCenter) {
|
|
1101
|
+
if (dedicatedCenter !== void 0) return void 0;
|
|
1102
|
+
return Array.from(root.querySelectorAll("[class]")).find((candidate) => {
|
|
1103
|
+
return classToken(candidate, "_frame") && firstByClassSuffix(candidate, "_sidebarCol") !== void 0 && firstByClassSuffix(candidate, "_centerCol") !== void 0;
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
/** Mark every mounted settings dialog so its mobile layout does not depend on the conversation shell. */
|
|
1107
|
+
function markNativeMobileSettings(root) {
|
|
1108
|
+
let marked = 0;
|
|
1109
|
+
for (const dialog of root.querySelectorAll("[role=\"dialog\"]")) {
|
|
1110
|
+
const children = Array.from(dialog.children);
|
|
1111
|
+
const nav = children.find((child) => classToken(child, "_nav"));
|
|
1112
|
+
const content = children.find((child) => classToken(child, "_content"));
|
|
1113
|
+
if (nav === void 0 || content === void 0) continue;
|
|
1114
|
+
dialog.dataset.dshMobileSettings = "true";
|
|
1115
|
+
nav.dataset.dshMobileSettingsNav = "true";
|
|
1116
|
+
content.dataset.dshMobileSettingsContent = "true";
|
|
1117
|
+
firstByClassSuffix(nav, "_navList")?.setAttribute("data-dsh-mobile-settings-list", "true");
|
|
1118
|
+
firstByClassSuffix(content, "_header")?.setAttribute("data-dsh-mobile-settings-header", "true");
|
|
1119
|
+
firstByClassSuffix(content, "_options")?.setAttribute("data-dsh-mobile-settings-options", "true");
|
|
1120
|
+
marked += 1;
|
|
1121
|
+
}
|
|
1122
|
+
return marked;
|
|
1123
|
+
}
|
|
172
1124
|
const AUTO_HISTORY_THRESHOLD_PX = 64;
|
|
1125
|
+
function controlledFileDragEvent(type, files, initialDropEffect) {
|
|
1126
|
+
const event = new Event(type, {
|
|
1127
|
+
bubbles: true,
|
|
1128
|
+
cancelable: true
|
|
1129
|
+
});
|
|
1130
|
+
const dataTransfer = {
|
|
1131
|
+
dropEffect: initialDropEffect,
|
|
1132
|
+
effectAllowed: "copy",
|
|
1133
|
+
files: Object.freeze([...files]),
|
|
1134
|
+
types: Object.freeze(["Files"])
|
|
1135
|
+
};
|
|
1136
|
+
Object.defineProperty(event, "dataTransfer", { value: dataTransfer });
|
|
1137
|
+
return event;
|
|
1138
|
+
}
|
|
1139
|
+
/** Ask the official document dragover listener whether the current composer accepts files. */
|
|
1140
|
+
function preflightComposerImageDrop(target, files) {
|
|
1141
|
+
if (files.length === 0) return false;
|
|
1142
|
+
const event = controlledFileDragEvent("dragover", files, "none");
|
|
1143
|
+
target.dispatchEvent(event);
|
|
1144
|
+
return event.dataTransfer?.dropEffect === "copy";
|
|
1145
|
+
}
|
|
1146
|
+
/** Dispatch a real drop only after a fresh official-listener preflight; the result is not an attachment ACK. */
|
|
1147
|
+
function dispatchComposerImageDrop(target, files) {
|
|
1148
|
+
if (!preflightComposerImageDrop(target, files)) return false;
|
|
1149
|
+
target.dispatchEvent(controlledFileDragEvent("drop", files, "copy"));
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
/** Apply the resolved locale independently from the document's possibly different lang attribute. */
|
|
1153
|
+
function applyNativeMobileLanguageMarker(root, language) {
|
|
1154
|
+
const previous = root.dataset.dshMobileLanguage;
|
|
1155
|
+
root.dataset.dshMobileLanguage = language;
|
|
1156
|
+
return () => {
|
|
1157
|
+
if (root.dataset.dshMobileLanguage !== language) return;
|
|
1158
|
+
if (previous === void 0) delete root.dataset.dshMobileLanguage;
|
|
1159
|
+
else root.dataset.dshMobileLanguage = previous;
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
/** Resolve the supported language used by native-mobile controls. */
|
|
1163
|
+
function resolveNativeMobileLanguage(documentLanguage, browserLanguages) {
|
|
1164
|
+
return [documentLanguage, ...browserLanguages].map((value) => value.trim().toLowerCase().split(/[-_]/u)[0]).find((value) => value === "it" || value === "en" || value === "zh") ?? "en";
|
|
1165
|
+
}
|
|
173
1166
|
/** Whether a user-driven scroll moved upward into the automatic history-loading zone. */
|
|
174
1167
|
function shouldAutoLoadEarlier(previousTop, currentTop) {
|
|
175
1168
|
return currentTop <= AUTO_HISTORY_THRESHOLD_PX && currentTop < previousTop - .5;
|
|
176
1169
|
}
|
|
1170
|
+
/** Check that an asynchronous picker result still belongs to its originating session and composer. */
|
|
1171
|
+
function isComposerMediaOriginCurrent(origin, current) {
|
|
1172
|
+
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;
|
|
1173
|
+
}
|
|
177
1174
|
/** Add mobile semantics without replacing feature trees. */
|
|
178
1175
|
function installNativeMobileSurface() {
|
|
179
1176
|
document.documentElement.classList.add("dsh-native-mobile-active");
|
|
1177
|
+
const browserLanguages = navigator.languages.length > 0 ? navigator.languages : [navigator.language];
|
|
1178
|
+
const language = resolveNativeMobileLanguage(document.documentElement.lang, browserLanguages);
|
|
1179
|
+
const restoreLanguageMarker = applyNativeMobileLanguageMarker(document.documentElement, language);
|
|
1180
|
+
const label = (italian, english, chinese) => language === "it" ? italian : language === "zh" ? chinese : english;
|
|
1181
|
+
const mediaIcon = (kind) => {
|
|
1182
|
+
const namespace = "http://www.w3.org/2000/svg";
|
|
1183
|
+
const icon = document.createElementNS(namespace, "svg");
|
|
1184
|
+
icon.setAttribute("viewBox", "0 0 16 16");
|
|
1185
|
+
icon.setAttribute("fill", "none");
|
|
1186
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1187
|
+
if (kind === "attachment") {
|
|
1188
|
+
const path = document.createElementNS(namespace, "path");
|
|
1189
|
+
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");
|
|
1190
|
+
path.setAttribute("fill", "currentColor");
|
|
1191
|
+
icon.append(path);
|
|
1192
|
+
return icon;
|
|
1193
|
+
}
|
|
1194
|
+
const body = document.createElementNS(namespace, "path");
|
|
1195
|
+
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");
|
|
1196
|
+
body.setAttribute("fill", "currentColor");
|
|
1197
|
+
const lens = document.createElementNS(namespace, "circle");
|
|
1198
|
+
lens.setAttribute("cx", "8");
|
|
1199
|
+
lens.setAttribute("cy", "8.1");
|
|
1200
|
+
lens.setAttribute("r", "2.15");
|
|
1201
|
+
lens.setAttribute("stroke", "currentColor");
|
|
1202
|
+
lens.setAttribute("stroke-width", "1.3");
|
|
1203
|
+
icon.append(body, lens);
|
|
1204
|
+
return icon;
|
|
1205
|
+
};
|
|
1206
|
+
const createMediaAction = (kind, text) => {
|
|
1207
|
+
const button = document.createElement("button");
|
|
1208
|
+
button.type = "button";
|
|
1209
|
+
button.className = "dsh-mobile-media-action";
|
|
1210
|
+
button.lang = language;
|
|
1211
|
+
button.dataset.dshMobileMediaAction = kind;
|
|
1212
|
+
button.append(mediaIcon(kind));
|
|
1213
|
+
const caption = document.createElement("span");
|
|
1214
|
+
caption.textContent = text;
|
|
1215
|
+
button.append(caption);
|
|
1216
|
+
return button;
|
|
1217
|
+
};
|
|
180
1218
|
const setInputMode = (mode) => {
|
|
181
1219
|
document.documentElement.dataset.dshMobileInput = mode;
|
|
182
1220
|
};
|
|
@@ -191,19 +1229,221 @@ window.__ModuleLoader__.load({
|
|
|
191
1229
|
const backdrop = document.createElement("button");
|
|
192
1230
|
backdrop.type = "button";
|
|
193
1231
|
backdrop.className = "dsh-native-mobile-backdrop";
|
|
1232
|
+
backdrop.lang = language;
|
|
194
1233
|
backdrop.hidden = true;
|
|
195
|
-
backdrop.setAttribute("aria-label", "关闭工作区导航");
|
|
1234
|
+
backdrop.setAttribute("aria-label", label("Chiudi navigazione area di lavoro", "Close workspace navigation", "关闭工作区导航"));
|
|
196
1235
|
document.body.append(backdrop);
|
|
197
1236
|
const branchToast = document.createElement("div");
|
|
198
1237
|
branchToast.className = "dsh-mobile-branch-toast";
|
|
1238
|
+
branchToast.lang = language;
|
|
199
1239
|
branchToast.setAttribute("role", "status");
|
|
200
1240
|
branchToast.setAttribute("aria-live", "polite");
|
|
201
1241
|
document.body.append(branchToast);
|
|
1242
|
+
const mediaToast = document.createElement("div");
|
|
1243
|
+
mediaToast.className = "dsh-mobile-media-toast";
|
|
1244
|
+
mediaToast.lang = language;
|
|
1245
|
+
mediaToast.setAttribute("role", "status");
|
|
1246
|
+
mediaToast.setAttribute("aria-live", "polite");
|
|
1247
|
+
document.body.append(mediaToast);
|
|
1248
|
+
const mediaActions = document.createElement("div");
|
|
1249
|
+
mediaActions.className = "dsh-mobile-media-shortcuts";
|
|
1250
|
+
mediaActions.lang = language;
|
|
1251
|
+
mediaActions.dataset.dshMobileMediaShortcuts = "true";
|
|
1252
|
+
mediaActions.setAttribute("role", "group");
|
|
1253
|
+
mediaActions.setAttribute("aria-label", label("Aggiungi immagine", "Add image", "添加图片"));
|
|
1254
|
+
const fileButton = createMediaAction("attachment", label("Scegli immagine", "Choose image", "选择图片"));
|
|
1255
|
+
const cameraButton = createMediaAction("camera", label("Scatta foto", "Take photo", "拍照"));
|
|
1256
|
+
fileButton.disabled = true;
|
|
1257
|
+
cameraButton.disabled = true;
|
|
1258
|
+
mediaActions.append(fileButton, cameraButton);
|
|
202
1259
|
let branchToastTimer = 0;
|
|
1260
|
+
let mediaToastTimer = 0;
|
|
1261
|
+
const showMediaToast = (message) => {
|
|
1262
|
+
mediaToast.textContent = message;
|
|
1263
|
+
mediaToast.dataset.visible = "true";
|
|
1264
|
+
if (mediaToastTimer !== 0) window.clearTimeout(mediaToastTimer);
|
|
1265
|
+
mediaToastTimer = window.setTimeout(() => {
|
|
1266
|
+
mediaToast.removeAttribute("data-visible");
|
|
1267
|
+
mediaToastTimer = 0;
|
|
1268
|
+
}, 2200);
|
|
1269
|
+
};
|
|
1270
|
+
let mediaRequestGeneration = 0;
|
|
1271
|
+
let disposed = false;
|
|
1272
|
+
let boundComposer = null;
|
|
1273
|
+
let boundSessionRoot = null;
|
|
1274
|
+
let boundSessionId = null;
|
|
1275
|
+
const preflightFile = new File([], "dsh-mobile-preflight.png", { type: "image/png" });
|
|
1276
|
+
const canAcceptComposerDrop = () => preflightComposerImageDrop(document, [preflightFile]);
|
|
1277
|
+
const mediaPickerAbortController = new AbortController();
|
|
1278
|
+
const browserPickerCleanups = /* @__PURE__ */ new Set();
|
|
1279
|
+
const sessionTokens = /* @__PURE__ */ new WeakMap();
|
|
1280
|
+
let nextSessionToken = 0;
|
|
1281
|
+
const currentSessionOrigin = () => {
|
|
1282
|
+
const dedicatedRoot = boundComposer?.closest("[data-dsh-mobile-session]") ?? null;
|
|
1283
|
+
const dedicatedId = dedicatedRoot?.getAttribute("data-dsh-mobile-session");
|
|
1284
|
+
if (dedicatedRoot !== null && typeof dedicatedId === "string" && dedicatedId !== "") return {
|
|
1285
|
+
sessionRoot: dedicatedRoot,
|
|
1286
|
+
sessionId: dedicatedId
|
|
1287
|
+
};
|
|
1288
|
+
const selectedRow = document.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]");
|
|
1289
|
+
if (selectedRow === null) return {
|
|
1290
|
+
sessionRoot: null,
|
|
1291
|
+
sessionId: null
|
|
1292
|
+
};
|
|
1293
|
+
let token = sessionTokens.get(selectedRow);
|
|
1294
|
+
if (token === void 0) {
|
|
1295
|
+
token = `stock-${String(++nextSessionToken)}`;
|
|
1296
|
+
sessionTokens.set(selectedRow, token);
|
|
1297
|
+
}
|
|
1298
|
+
const identity = selectedRow.getAttribute("data-session-id") ?? selectedRow.getAttribute("aria-label") ?? selectedRow.textContent?.trim() ?? "";
|
|
1299
|
+
return {
|
|
1300
|
+
sessionRoot: selectedRow,
|
|
1301
|
+
sessionId: `${token}:${identity}`
|
|
1302
|
+
};
|
|
1303
|
+
};
|
|
1304
|
+
const mediaRequestContext = () => {
|
|
1305
|
+
const session = currentSessionOrigin();
|
|
1306
|
+
return {
|
|
1307
|
+
generation: ++mediaRequestGeneration,
|
|
1308
|
+
href: window.location.href,
|
|
1309
|
+
composer: boundComposer,
|
|
1310
|
+
...session
|
|
1311
|
+
};
|
|
1312
|
+
};
|
|
1313
|
+
const mediaRequestIsCurrent = (context) => {
|
|
1314
|
+
const session = currentSessionOrigin();
|
|
1315
|
+
const composer = boundComposer;
|
|
1316
|
+
return isComposerMediaOriginCurrent(context, {
|
|
1317
|
+
generation: mediaRequestGeneration,
|
|
1318
|
+
href: window.location.href,
|
|
1319
|
+
composer,
|
|
1320
|
+
sessionRoot: session.sessionRoot,
|
|
1321
|
+
sessionId: session.sessionId,
|
|
1322
|
+
disposed,
|
|
1323
|
+
composerConnected: context.composer?.isConnected === true
|
|
1324
|
+
});
|
|
1325
|
+
};
|
|
1326
|
+
const deliverImages = (files, context) => {
|
|
1327
|
+
if (files.length === 0 || !mediaRequestIsCurrent(context)) return;
|
|
1328
|
+
dispatchComposerImageDrop(document, files);
|
|
1329
|
+
};
|
|
1330
|
+
const launchBrowserPicker = (camera, context) => {
|
|
1331
|
+
const input = document.createElement("input");
|
|
1332
|
+
input.type = "file";
|
|
1333
|
+
input.accept = "image/png,image/jpeg,image/webp,image/gif";
|
|
1334
|
+
if (camera) input.capture = "environment";
|
|
1335
|
+
input.hidden = true;
|
|
1336
|
+
const signal = mediaPickerAbortController.signal;
|
|
1337
|
+
let cleanupTimer = 0;
|
|
1338
|
+
let watchdogTimer = 0;
|
|
1339
|
+
let cleaned = false;
|
|
1340
|
+
const scheduleCleanup = () => {
|
|
1341
|
+
if (!cleaned && cleanupTimer === 0) cleanupTimer = window.setTimeout(cleanup, 1e3);
|
|
1342
|
+
};
|
|
1343
|
+
const onVisibilityChange = () => {
|
|
1344
|
+
if (document.visibilityState === "visible") scheduleCleanup();
|
|
1345
|
+
};
|
|
1346
|
+
const onChange = () => {
|
|
1347
|
+
if (cleaned) return;
|
|
1348
|
+
const files = input.files === null ? [] : [...input.files];
|
|
1349
|
+
cleanup();
|
|
1350
|
+
deliverImages(files, context);
|
|
1351
|
+
};
|
|
1352
|
+
const cleanup = () => {
|
|
1353
|
+
if (cleaned) return;
|
|
1354
|
+
cleaned = true;
|
|
1355
|
+
if (cleanupTimer !== 0) window.clearTimeout(cleanupTimer);
|
|
1356
|
+
if (watchdogTimer !== 0) window.clearTimeout(watchdogTimer);
|
|
1357
|
+
cleanupTimer = 0;
|
|
1358
|
+
watchdogTimer = 0;
|
|
1359
|
+
input.removeEventListener("change", onChange);
|
|
1360
|
+
input.removeEventListener("cancel", cleanup);
|
|
1361
|
+
window.removeEventListener("focus", scheduleCleanup);
|
|
1362
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1363
|
+
signal.removeEventListener("abort", cleanup);
|
|
1364
|
+
browserPickerCleanups.delete(cleanup);
|
|
1365
|
+
input.remove();
|
|
1366
|
+
};
|
|
1367
|
+
input.addEventListener("change", onChange);
|
|
1368
|
+
input.addEventListener("cancel", cleanup);
|
|
1369
|
+
window.addEventListener("focus", scheduleCleanup);
|
|
1370
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1371
|
+
signal.addEventListener("abort", cleanup, { once: true });
|
|
1372
|
+
watchdogTimer = window.setTimeout(cleanup, 3e5);
|
|
1373
|
+
browserPickerCleanups.add(cleanup);
|
|
1374
|
+
if (signal.aborted) {
|
|
1375
|
+
cleanup();
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
document.body.append(input);
|
|
1380
|
+
input.click();
|
|
1381
|
+
} catch {
|
|
1382
|
+
cleanup();
|
|
1383
|
+
if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aprire il selettore immagini", "Could not open the image picker", "无法打开图片选择器"));
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
const dismissCommandMenu = () => {
|
|
1387
|
+
(boundComposer?.querySelector("button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]"))?.click();
|
|
1388
|
+
};
|
|
1389
|
+
const setMediaActionsDisabled = (disabled, title) => {
|
|
1390
|
+
for (const button of [fileButton, cameraButton]) {
|
|
1391
|
+
if (button.disabled !== disabled) button.disabled = disabled;
|
|
1392
|
+
if (button.title !== title) button.title = title;
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
const quietMediaPointer = (event) => {
|
|
1396
|
+
event.preventDefault();
|
|
1397
|
+
event.stopPropagation();
|
|
1398
|
+
const active = document.activeElement;
|
|
1399
|
+
if (active instanceof HTMLElement && (active.matches("input,textarea") || active.isContentEditable)) active.blur();
|
|
1400
|
+
};
|
|
1401
|
+
const pickImage = (camera) => {
|
|
1402
|
+
if (!canAcceptComposerDrop()) {
|
|
1403
|
+
setMediaActionsDisabled(true, label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
|
|
1404
|
+
dismissCommandMenu();
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const context = mediaRequestContext();
|
|
1408
|
+
dismissCommandMenu();
|
|
1409
|
+
const bridge = window.__DSH_MOBILE_NATIVE__;
|
|
1410
|
+
if (bridge === void 0) {
|
|
1411
|
+
launchBrowserPicker(camera, context);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
const action = camera ? "camera.capture" : "files.pick";
|
|
1415
|
+
const input = camera ? {} : { accept: [
|
|
1416
|
+
"image/png",
|
|
1417
|
+
"image/jpeg",
|
|
1418
|
+
"image/webp",
|
|
1419
|
+
"image/gif"
|
|
1420
|
+
] };
|
|
1421
|
+
Promise.resolve().then(() => bridge.invoke(action, input)).then((value) => {
|
|
1422
|
+
if (!mediaRequestIsCurrent(context)) return;
|
|
1423
|
+
if (value instanceof File) deliverImages([value], context);
|
|
1424
|
+
else showMediaToast(label("Il file selezionato non è utilizzabile", "The selected file is unavailable", "所选文件不可用"));
|
|
1425
|
+
}).catch((error) => {
|
|
1426
|
+
if (!mediaRequestIsCurrent(context)) return;
|
|
1427
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
1428
|
+
if (code === "cancelled") return;
|
|
1429
|
+
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", "无法附加图片"));
|
|
1430
|
+
});
|
|
1431
|
+
};
|
|
1432
|
+
const chooseImage = () => {
|
|
1433
|
+
pickImage(false);
|
|
1434
|
+
};
|
|
1435
|
+
const takePhoto = () => {
|
|
1436
|
+
pickImage(true);
|
|
1437
|
+
};
|
|
1438
|
+
fileButton.addEventListener("pointerdown", quietMediaPointer);
|
|
1439
|
+
cameraButton.addEventListener("pointerdown", quietMediaPointer);
|
|
1440
|
+
fileButton.addEventListener("click", chooseImage);
|
|
1441
|
+
cameraButton.addEventListener("click", takePhoto);
|
|
203
1442
|
const showBranchToast = () => {
|
|
204
1443
|
const header = document.querySelector("[data-dsh-mobile-header]");
|
|
205
1444
|
const title = header === null ? void 0 : header.querySelector("[class*=\"_crumbCurrent\"]")?.textContent?.trim();
|
|
206
|
-
|
|
1445
|
+
const prefix = label("Ramo corrente", "Current branch", "当前分支");
|
|
1446
|
+
branchToast.textContent = title === void 0 ? prefix : `${prefix}: ${title}`;
|
|
207
1447
|
branchToast.dataset.visible = "true";
|
|
208
1448
|
if (branchToastTimer !== 0) window.clearTimeout(branchToastTimer);
|
|
209
1449
|
branchToastTimer = window.setTimeout(() => {
|
|
@@ -213,7 +1453,7 @@ window.__ModuleLoader__.load({
|
|
|
213
1453
|
};
|
|
214
1454
|
const onBranchClick = (event) => {
|
|
215
1455
|
if (!(event.target instanceof Element)) return;
|
|
216
|
-
const branch = event.target.closest("button[aria-label*=\"分支\"],button[aria-label*=\"Branch\"],button[aria-label*=\"branch\"]");
|
|
1456
|
+
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
1457
|
if (branch === null || branch.hasAttribute("disabled") || branch.getAttribute("aria-disabled") === "true") return;
|
|
218
1458
|
window.setTimeout(showBranchToast, 80);
|
|
219
1459
|
};
|
|
@@ -282,17 +1522,32 @@ window.__ModuleLoader__.load({
|
|
|
282
1522
|
});
|
|
283
1523
|
};
|
|
284
1524
|
document.addEventListener("click", animateNavigation);
|
|
1525
|
+
const syncMediaBinding = (composer) => {
|
|
1526
|
+
const previousComposer = boundComposer;
|
|
1527
|
+
boundComposer = composer;
|
|
1528
|
+
const session = currentSessionOrigin();
|
|
1529
|
+
if (composer === previousComposer && session.sessionRoot === boundSessionRoot && session.sessionId === boundSessionId) return;
|
|
1530
|
+
boundSessionRoot = session.sessionRoot;
|
|
1531
|
+
boundSessionId = session.sessionId;
|
|
1532
|
+
mediaRequestGeneration += 1;
|
|
1533
|
+
};
|
|
285
1534
|
const sync = () => {
|
|
286
1535
|
scheduled = 0;
|
|
287
|
-
frame = firstByClassSuffix(document, "_frame");
|
|
288
1536
|
const dedicatedCenter = document.querySelector(".dshm-main") ?? void 0;
|
|
1537
|
+
const nextFrame = resolveNativeMobileFrame(document, dedicatedCenter);
|
|
1538
|
+
if (frame !== nextFrame) frame?.removeAttribute("data-dsh-mobile-frame");
|
|
1539
|
+
frame = nextFrame;
|
|
289
1540
|
if (frame !== void 0) frame.dataset.dshMobileFrame = "true";
|
|
290
1541
|
sidebar = frame === void 0 ? document.querySelector(".dshm-drawer") ?? void 0 : firstByClassSuffix(frame, "_sidebarCol");
|
|
291
1542
|
const center = frame === void 0 ? dedicatedCenter : firstByClassSuffix(frame, "_centerCol");
|
|
292
1543
|
const details = frame === void 0 ? void 0 : firstByClassSuffix(frame, "_detailsCol");
|
|
293
1544
|
const handle = frame === void 0 ? void 0 : firstByClassSuffix(frame, "_handle");
|
|
1545
|
+
markNativeMobileSettings(document);
|
|
294
1546
|
if (center === void 0) {
|
|
295
1547
|
bindHistoryScroller(void 0);
|
|
1548
|
+
syncMediaBinding(null);
|
|
1549
|
+
setMediaActionsDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
|
|
1550
|
+
mediaActions.remove();
|
|
296
1551
|
return;
|
|
297
1552
|
}
|
|
298
1553
|
if (center !== void 0) {
|
|
@@ -326,16 +1581,34 @@ window.__ModuleLoader__.load({
|
|
|
326
1581
|
table.before(wrapper);
|
|
327
1582
|
wrapper.append(table);
|
|
328
1583
|
}
|
|
329
|
-
const
|
|
1584
|
+
const composerCard = center.querySelector("[data-composer-card]");
|
|
1585
|
+
const composerRow = composerCard?.querySelector(":scope > [data-input-scroll]")?.nextElementSibling;
|
|
1586
|
+
if (!(composerRow instanceof HTMLElement)) {
|
|
1587
|
+
syncMediaBinding(null);
|
|
1588
|
+
setMediaActionsDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
|
|
1589
|
+
mediaActions.remove();
|
|
1590
|
+
}
|
|
330
1591
|
if (composerRow instanceof HTMLElement) {
|
|
331
1592
|
composerRow.dataset.dshMobileComposerRow = "true";
|
|
332
1593
|
const groups = Array.from(composerRow.children).filter((child) => child instanceof HTMLElement);
|
|
333
1594
|
const composerTools = groups[0];
|
|
334
1595
|
const composerTrailing = groups.at(-1);
|
|
335
|
-
if (composerTools !== void 0)
|
|
1596
|
+
if (composerTools !== void 0) {
|
|
1597
|
+
composerTools.dataset.dshMobileComposerTools = "true";
|
|
1598
|
+
syncMediaBinding(composerCard ?? null);
|
|
1599
|
+
const composerInput = composerCard?.querySelector("textarea");
|
|
1600
|
+
const composerEditor = composerCard?.querySelector("[contenteditable=\"true\"],[contenteditable=\"plaintext-only\"]");
|
|
1601
|
+
const composerBusy = composerCard?.getAttribute("aria-busy") === "true" || composerInput?.disabled === true || composerInput?.readOnly === true || composerEditor?.getAttribute("aria-disabled") === "true";
|
|
1602
|
+
const attachmentBlocked = !canAcceptComposerDrop();
|
|
1603
|
+
const mediaDisabled = conversation === null || currentSessionOrigin().sessionId === null || composerBusy || attachmentBlocked;
|
|
1604
|
+
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", "附加截图、图片或照片");
|
|
1605
|
+
setMediaActionsDisabled(mediaDisabled, mediaTitle);
|
|
1606
|
+
const commandMenu = composerCard?.querySelector("[data-trigger-menu]") ?? null;
|
|
1607
|
+
if (commandMenu !== null && (mediaActions.parentElement !== commandMenu || commandMenu.firstElementChild !== mediaActions)) commandMenu.prepend(mediaActions);
|
|
1608
|
+
}
|
|
336
1609
|
if (composerTrailing !== void 0 && composerTrailing !== composerTools) {
|
|
337
1610
|
composerTrailing.dataset.dshMobileComposerTrailing = "true";
|
|
338
|
-
const modelTrigger = composerTrailing.querySelector("button[aria-label^=\"选择模型\"],button[aria-label^=\"Select model\"]");
|
|
1611
|
+
const modelTrigger = composerTrailing.querySelector("button[aria-label^=\"选择模型\"],button[aria-label^=\"Select model\"],button[aria-label^=\"Seleziona modello\"]");
|
|
339
1612
|
if (modelTrigger !== null) {
|
|
340
1613
|
modelTrigger.dataset.dshMobileComposerModelTrigger = "true";
|
|
341
1614
|
modelTrigger.parentElement?.setAttribute("data-dsh-mobile-composer-model", "true");
|
|
@@ -350,23 +1623,6 @@ window.__ModuleLoader__.load({
|
|
|
350
1623
|
const lastColumn = frame?.style.gridTemplateColumns.trim().split(/\s+/).at(-1);
|
|
351
1624
|
details.dataset.open = String(lastColumn !== void 0 && lastColumn !== "0px" && lastColumn !== "0");
|
|
352
1625
|
}
|
|
353
|
-
const settings = Array.from(document.querySelectorAll("[role=\"dialog\"]")).find((dialog) => {
|
|
354
|
-
return Array.from(dialog.children).find((child) => child instanceof HTMLElement && classToken(child, "_nav")) !== void 0;
|
|
355
|
-
});
|
|
356
|
-
if (settings !== void 0) {
|
|
357
|
-
settings.dataset.dshMobileSettings = "true";
|
|
358
|
-
const nav = Array.from(settings.children).find((child) => child instanceof HTMLElement && classToken(child, "_nav"));
|
|
359
|
-
const content = Array.from(settings.children).find((child) => child instanceof HTMLElement && classToken(child, "_content"));
|
|
360
|
-
if (nav !== void 0) {
|
|
361
|
-
nav.dataset.dshMobileSettingsNav = "true";
|
|
362
|
-
firstByClassSuffix(nav, "_navList")?.setAttribute("data-dsh-mobile-settings-list", "true");
|
|
363
|
-
}
|
|
364
|
-
if (content !== void 0) {
|
|
365
|
-
content.dataset.dshMobileSettingsContent = "true";
|
|
366
|
-
firstByClassSuffix(content, "_header")?.setAttribute("data-dsh-mobile-settings-header", "true");
|
|
367
|
-
firstByClassSuffix(content, "_options")?.setAttribute("data-dsh-mobile-settings-options", "true");
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
1626
|
if (sidebar === void 0) return;
|
|
371
1627
|
sidebar.dataset.dshMobileSidebar = "true";
|
|
372
1628
|
toggle = firstByClassSuffix(sidebar, "_toggle");
|
|
@@ -392,7 +1648,11 @@ window.__ModuleLoader__.load({
|
|
|
392
1648
|
attributeFilter: [
|
|
393
1649
|
"class",
|
|
394
1650
|
"style",
|
|
395
|
-
"disabled"
|
|
1651
|
+
"disabled",
|
|
1652
|
+
"readonly",
|
|
1653
|
+
"aria-busy",
|
|
1654
|
+
"aria-selected",
|
|
1655
|
+
"data-dsh-mobile-session"
|
|
396
1656
|
]
|
|
397
1657
|
});
|
|
398
1658
|
backdrop.addEventListener("click", () => {
|
|
@@ -400,10 +1660,22 @@ window.__ModuleLoader__.load({
|
|
|
400
1660
|
});
|
|
401
1661
|
sync();
|
|
402
1662
|
return () => {
|
|
1663
|
+
disposed = true;
|
|
1664
|
+
mediaRequestGeneration += 1;
|
|
1665
|
+
mediaPickerAbortController.abort();
|
|
1666
|
+
restoreLanguageMarker();
|
|
1667
|
+
for (const cleanup of [...browserPickerCleanups]) cleanup();
|
|
403
1668
|
observer.disconnect();
|
|
404
1669
|
document.removeEventListener("click", onBranchClick, true);
|
|
1670
|
+
fileButton.removeEventListener("pointerdown", quietMediaPointer);
|
|
1671
|
+
cameraButton.removeEventListener("pointerdown", quietMediaPointer);
|
|
1672
|
+
fileButton.removeEventListener("click", chooseImage);
|
|
1673
|
+
cameraButton.removeEventListener("click", takePhoto);
|
|
405
1674
|
if (branchToastTimer !== 0) window.clearTimeout(branchToastTimer);
|
|
1675
|
+
if (mediaToastTimer !== 0) window.clearTimeout(mediaToastTimer);
|
|
406
1676
|
branchToast.remove();
|
|
1677
|
+
mediaToast.remove();
|
|
1678
|
+
mediaActions.remove();
|
|
407
1679
|
if (scheduled !== 0) cancelAnimationFrame(scheduled);
|
|
408
1680
|
if (transitionFrame !== 0) cancelAnimationFrame(transitionFrame);
|
|
409
1681
|
if (transitionRestartFrame !== 0) cancelAnimationFrame(transitionRestartFrame);
|
|
@@ -433,6 +1705,95 @@ window.__ModuleLoader__.load({
|
|
|
433
1705
|
function isLoopbackHost(hostname) {
|
|
434
1706
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
435
1707
|
}
|
|
1708
|
+
function selectMobileControlLocale(documentLanguage = "", navigatorLanguages = []) {
|
|
1709
|
+
for (const value of [documentLanguage, ...navigatorLanguages]) {
|
|
1710
|
+
const language = value.trim().toLowerCase().split(/[-_]/u)[0];
|
|
1711
|
+
if (language === "it" || language === "en" || language === "zh") return language;
|
|
1712
|
+
}
|
|
1713
|
+
return "en";
|
|
1714
|
+
}
|
|
1715
|
+
function selectedMobileControlLocale() {
|
|
1716
|
+
return selectMobileControlLocale(document.documentElement.lang, navigator.languages?.length ? navigator.languages : [navigator.language]);
|
|
1717
|
+
}
|
|
1718
|
+
/** Remount one plugin-owned surface when DSH changes the document language. */
|
|
1719
|
+
function installDshLanguageBoundSurface(install) {
|
|
1720
|
+
let locale = selectedMobileControlLocale();
|
|
1721
|
+
let dispose = install();
|
|
1722
|
+
const observer = new MutationObserver(() => {
|
|
1723
|
+
const next = selectedMobileControlLocale();
|
|
1724
|
+
if (next === locale) return;
|
|
1725
|
+
dispose();
|
|
1726
|
+
locale = next;
|
|
1727
|
+
dispose = install();
|
|
1728
|
+
});
|
|
1729
|
+
observer.observe(document.documentElement, {
|
|
1730
|
+
attributes: true,
|
|
1731
|
+
attributeFilter: ["lang"]
|
|
1732
|
+
});
|
|
1733
|
+
return () => {
|
|
1734
|
+
observer.disconnect();
|
|
1735
|
+
dispose();
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function controlTranslator(locale = selectedMobileControlLocale()) {
|
|
1739
|
+
const messages = MOBILE_CONTROL_MESSAGES;
|
|
1740
|
+
return (key, values = {}) => {
|
|
1741
|
+
return (messages[locale][key] ?? messages.en[key] ?? key).replace(/\{(\w+)\}/gu, (_match, name) => String(values[name] ?? `{${name}}`));
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function normalizeDiagnosticOverall(value) {
|
|
1745
|
+
return value === "ok" ? "ok" : value === "attention" ? "attention" : "error";
|
|
1746
|
+
}
|
|
1747
|
+
function normalizeDiagnosticStatus(value) {
|
|
1748
|
+
return value === "ok" || value === "warning" || value === "error" || value === "info" ? value : "error";
|
|
1749
|
+
}
|
|
1750
|
+
function diagnosticOverallForChecks(value, statuses) {
|
|
1751
|
+
const normalized = statuses.map(normalizeDiagnosticStatus);
|
|
1752
|
+
const payloadOverall = normalizeDiagnosticOverall(value);
|
|
1753
|
+
if (payloadOverall === "error" || normalized.includes("error")) return "error";
|
|
1754
|
+
if (payloadOverall === "attention" || normalized.includes("warning")) return "attention";
|
|
1755
|
+
return "ok";
|
|
1756
|
+
}
|
|
1757
|
+
function validateDiagnosticChecks(value) {
|
|
1758
|
+
if (!Array.isArray(value)) return {
|
|
1759
|
+
entries: [],
|
|
1760
|
+
malformed: true
|
|
1761
|
+
};
|
|
1762
|
+
const entries = [];
|
|
1763
|
+
let malformed = false;
|
|
1764
|
+
for (const candidate of value) {
|
|
1765
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
|
|
1766
|
+
malformed = true;
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
entries.push(candidate);
|
|
1770
|
+
}
|
|
1771
|
+
return {
|
|
1772
|
+
entries,
|
|
1773
|
+
malformed
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function diagnosticEntriesForRender(data) {
|
|
1777
|
+
const overallKnown = data.overall === "ok" || data.overall === "attention" || data.overall === "error";
|
|
1778
|
+
const validated = validateDiagnosticChecks(data.checks);
|
|
1779
|
+
const statusesKnown = validated.entries.every((entry) => entry.status === "ok" || entry.status === "warning" || entry.status === "error" || entry.status === "info");
|
|
1780
|
+
if (!overallKnown || validated.malformed || validated.entries.length === 0 || !statusesKnown) throw new TypeError("diagnostics envelope is unavailable");
|
|
1781
|
+
return validated.entries;
|
|
1782
|
+
}
|
|
1783
|
+
function renderDiagnosticPayloadSafely(data, render, onFailure) {
|
|
1784
|
+
try {
|
|
1785
|
+
render(data);
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
onFailure(error);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
function diagnosticServerCopy(entry) {
|
|
1791
|
+
return {
|
|
1792
|
+
label: typeof entry.label === "string" ? entry.label : "",
|
|
1793
|
+
detail: typeof entry.detail === "string" ? entry.detail : "",
|
|
1794
|
+
action: typeof entry.action === "string" ? entry.action : ""
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
436
1797
|
/**
|
|
437
1798
|
* Match DSH's client-side privilege hint to the authenticated mobile gateway.
|
|
438
1799
|
* The gateway authenticates the paired device and forwards allowed requests to
|
|
@@ -453,6 +1814,23 @@ window.__ModuleLoader__.load({
|
|
|
453
1814
|
}
|
|
454
1815
|
const CONTROL_REQUEST_TIMEOUT_MS = 15e3;
|
|
455
1816
|
const LONG_CONTROL_REQUEST_TIMEOUT_MS = 13e4;
|
|
1817
|
+
const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
|
|
1818
|
+
function releaseVersion(value) {
|
|
1819
|
+
return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(value) ? value : void 0;
|
|
1820
|
+
}
|
|
1821
|
+
/** Reduce the loopback release response to trusted text and download metadata. */
|
|
1822
|
+
function clientReleaseInfo(data) {
|
|
1823
|
+
const latestVersion = releaseVersion(data.latestVersion);
|
|
1824
|
+
const androidVersion = releaseVersion(data.androidVersion);
|
|
1825
|
+
const expectedAndroidDownloadUrl = androidVersion === void 0 ? void 0 : `${GITHUB_RELEASES_URL}/download/v${androidVersion}/dsh-mobile-android-v${androidVersion}.apk`;
|
|
1826
|
+
const androidDownloadUrl = expectedAndroidDownloadUrl !== void 0 && data.androidDownloadUrl === expectedAndroidDownloadUrl ? expectedAndroidDownloadUrl : GITHUB_RELEASES_URL;
|
|
1827
|
+
return {
|
|
1828
|
+
updateAvailable: data.updateAvailable === true && latestVersion !== void 0,
|
|
1829
|
+
...latestVersion === void 0 ? {} : { latestVersion },
|
|
1830
|
+
...androidVersion === void 0 ? {} : { androidVersion },
|
|
1831
|
+
androidDownloadUrl
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
456
1834
|
async function requestJson(path, init, timeoutMs = CONTROL_REQUEST_TIMEOUT_MS) {
|
|
457
1835
|
const controller = new AbortController();
|
|
458
1836
|
const upstreamSignal = init?.signal;
|
|
@@ -477,7 +1855,7 @@ window.__ModuleLoader__.load({
|
|
|
477
1855
|
if (!response.ok) throw new Error(typeof body.error === "string" ? body.error : `HTTP ${String(response.status)}`);
|
|
478
1856
|
return body;
|
|
479
1857
|
} catch (error) {
|
|
480
|
-
if (controller.signal.aborted && upstreamSignal?.aborted !== true) throw new Error("
|
|
1858
|
+
if (controller.signal.aborted && upstreamSignal?.aborted !== true) throw new Error(controlTranslator()("requestTimeout"));
|
|
481
1859
|
throw error;
|
|
482
1860
|
} finally {
|
|
483
1861
|
clearTimeout(timer);
|
|
@@ -497,38 +1875,60 @@ window.__ModuleLoader__.load({
|
|
|
497
1875
|
if (url.protocol !== "https:" || url.hostname !== "login.tailscale.com" || url.port !== "" || url.username !== "" || url.password !== "") return "";
|
|
498
1876
|
return url.toString();
|
|
499
1877
|
}
|
|
1878
|
+
/** Build the copy-only restricted VPS template without sending the token to the host API. */
|
|
1879
|
+
function createFrpServerTemplateForClipboard(serverPort, token, publicOrigin) {
|
|
1880
|
+
return createRestrictedFrpServerTemplate(serverPort, token, publicOrigin);
|
|
1881
|
+
}
|
|
500
1882
|
function installControl() {
|
|
1883
|
+
const locale = selectedMobileControlLocale();
|
|
1884
|
+
const localeTag = locale === "it" ? "it-IT" : locale === "zh" ? "zh-CN" : "en-US";
|
|
1885
|
+
const t = controlTranslator(locale);
|
|
1886
|
+
const lifecycle = new AbortController();
|
|
1887
|
+
const controlRequestJson = (path, init, timeoutMs) => requestJson(path, {
|
|
1888
|
+
...init,
|
|
1889
|
+
signal: lifecycle.signal
|
|
1890
|
+
}, timeoutMs);
|
|
501
1891
|
const root = element("div", "dsh-mobile-control");
|
|
1892
|
+
root.lang = locale;
|
|
502
1893
|
const panel = element("section", "dsh-mobile-control__panel");
|
|
503
1894
|
panel.hidden = true;
|
|
504
|
-
panel.
|
|
1895
|
+
panel.lang = locale;
|
|
1896
|
+
panel.setAttribute("aria-label", t("mobileAccess"));
|
|
505
1897
|
const header = element("header", "dsh-mobile-control__header");
|
|
506
1898
|
const title = element("h2");
|
|
507
|
-
title.textContent = "
|
|
1899
|
+
title.textContent = t("mobileAccess");
|
|
508
1900
|
const headerActions = element("div", "dsh-mobile-control__header-actions");
|
|
1901
|
+
const updatePlugin = element("button", "dsh-mobile-control__update-plugin");
|
|
1902
|
+
updatePlugin.type = "button";
|
|
1903
|
+
updatePlugin.textContent = t("updatePlugin");
|
|
1904
|
+
updatePlugin.hidden = true;
|
|
509
1905
|
const diagnosticsEntry = element("button", "dsh-mobile-control__diagnostic-entry");
|
|
510
1906
|
diagnosticsEntry.type = "button";
|
|
511
|
-
diagnosticsEntry.textContent = "
|
|
512
|
-
diagnosticsEntry.setAttribute("aria-label", "
|
|
1907
|
+
diagnosticsEntry.textContent = t("diagnostics");
|
|
1908
|
+
diagnosticsEntry.setAttribute("aria-label", t("openDiagnostics"));
|
|
513
1909
|
diagnosticsEntry.setAttribute("aria-pressed", "false");
|
|
514
1910
|
const close = element("button", "dsh-mobile-control__close");
|
|
515
1911
|
close.type = "button";
|
|
516
1912
|
close.textContent = "×";
|
|
517
|
-
close.setAttribute("aria-label", "
|
|
518
|
-
headerActions.append(diagnosticsEntry, close);
|
|
1913
|
+
close.setAttribute("aria-label", t("collapseMobileAccess"));
|
|
1914
|
+
headerActions.append(updatePlugin, diagnosticsEntry, close);
|
|
1915
|
+
const releaseNotice = element("p", "dsh-mobile-control__release-notice");
|
|
1916
|
+
releaseNotice.hidden = true;
|
|
1917
|
+
releaseNotice.setAttribute("role", "status");
|
|
1918
|
+
releaseNotice.setAttribute("aria-live", "polite");
|
|
519
1919
|
const appDownload = element("a", "dsh-mobile-control__app-download");
|
|
520
|
-
appDownload.href =
|
|
1920
|
+
appDownload.href = GITHUB_RELEASES_URL;
|
|
521
1921
|
appDownload.target = "_blank";
|
|
522
1922
|
appDownload.rel = "noopener noreferrer";
|
|
523
|
-
appDownload.textContent = "
|
|
524
|
-
appDownload.setAttribute("aria-label", "
|
|
1923
|
+
appDownload.textContent = t("downloadAndroid");
|
|
1924
|
+
appDownload.setAttribute("aria-label", t("downloadAndroidAria"));
|
|
525
1925
|
const switcher = element("div", "dsh-mobile-control__switcher");
|
|
526
1926
|
const lanTab = element("button", "dsh-mobile-control__tab is-active");
|
|
527
1927
|
lanTab.type = "button";
|
|
528
|
-
lanTab.textContent = "
|
|
1928
|
+
lanTab.textContent = t("lan");
|
|
529
1929
|
const remoteTab = element("button", "dsh-mobile-control__tab");
|
|
530
1930
|
remoteTab.type = "button";
|
|
531
|
-
remoteTab.textContent = "
|
|
1931
|
+
remoteTab.textContent = t("remote");
|
|
532
1932
|
lanTab.setAttribute("aria-pressed", "true");
|
|
533
1933
|
remoteTab.setAttribute("aria-pressed", "false");
|
|
534
1934
|
switcher.append(lanTab, remoteTab);
|
|
@@ -536,7 +1936,7 @@ window.__ModuleLoader__.load({
|
|
|
536
1936
|
const access = element("div", "dsh-mobile-control__access");
|
|
537
1937
|
access.hidden = true;
|
|
538
1938
|
const accessLabel = element("span", "dsh-mobile-control__access-label");
|
|
539
|
-
accessLabel.textContent = "
|
|
1939
|
+
accessLabel.textContent = t("browserAccess");
|
|
540
1940
|
const accessLink = element("a", "dsh-mobile-control__access-link");
|
|
541
1941
|
accessLink.target = "_blank";
|
|
542
1942
|
accessLink.rel = "noreferrer";
|
|
@@ -544,7 +1944,7 @@ window.__ModuleLoader__.load({
|
|
|
544
1944
|
const qrBox = element("div", "dsh-mobile-control__qr");
|
|
545
1945
|
qrBox.hidden = true;
|
|
546
1946
|
const status = element("p", "dsh-mobile-control__status");
|
|
547
|
-
status.textContent = "
|
|
1947
|
+
status.textContent = t("loadingStatus");
|
|
548
1948
|
const extensionStatus = element("p", "dsh-mobile-control__extensions");
|
|
549
1949
|
extensionStatus.hidden = true;
|
|
550
1950
|
const actions = element("div", "dsh-mobile-control__actions");
|
|
@@ -552,31 +1952,31 @@ window.__ModuleLoader__.load({
|
|
|
552
1952
|
toggle.type = "button";
|
|
553
1953
|
const pair = element("button", "dsh-mobile-control__primary");
|
|
554
1954
|
pair.type = "button";
|
|
555
|
-
pair.textContent = "
|
|
1955
|
+
pair.textContent = t("generateCopyKey");
|
|
556
1956
|
const linkPair = element("button", "dsh-mobile-control__secondary");
|
|
557
1957
|
linkPair.type = "button";
|
|
558
|
-
linkPair.textContent = "
|
|
1958
|
+
linkPair.textContent = t("copyPairLink");
|
|
559
1959
|
const manageRow = element("div", "dsh-mobile-control__manage-row");
|
|
560
1960
|
const manageDevices = element("button", "dsh-mobile-control__manage");
|
|
561
1961
|
manageDevices.type = "button";
|
|
562
|
-
manageDevices.textContent = "
|
|
1962
|
+
manageDevices.textContent = t("managePairedDevices");
|
|
563
1963
|
const resetAll = element("button", "dsh-mobile-control__manage");
|
|
564
1964
|
resetAll.type = "button";
|
|
565
|
-
resetAll.textContent = "
|
|
1965
|
+
resetAll.textContent = t("clearAllDevices");
|
|
566
1966
|
manageRow.append(manageDevices, resetAll);
|
|
567
1967
|
const devicePanel = element("div", "dsh-mobile-control__devices");
|
|
568
1968
|
devicePanel.hidden = true;
|
|
569
1969
|
const remoteView = element("div", "dsh-mobile-control__view is-remote");
|
|
570
1970
|
remoteView.hidden = true;
|
|
571
1971
|
const remoteIntro = element("p", "dsh-mobile-control__intro");
|
|
572
|
-
remoteIntro.textContent = "
|
|
1972
|
+
remoteIntro.textContent = t("remoteIntro");
|
|
573
1973
|
const providerSection = element("section", "dsh-mobile-control__provider-section");
|
|
574
1974
|
const providerHeading = element("h3", "dsh-mobile-control__section-title");
|
|
575
|
-
providerHeading.textContent = "
|
|
1975
|
+
providerHeading.textContent = t("chooseProvider");
|
|
576
1976
|
const providerInfo = element("div", "dsh-mobile-control__provider-info");
|
|
577
1977
|
const providerInfoButton = element("button", "dsh-mobile-control__provider-info-button");
|
|
578
1978
|
providerInfoButton.type = "button";
|
|
579
|
-
providerInfoButton.setAttribute("aria-label", "
|
|
1979
|
+
providerInfoButton.setAttribute("aria-label", t("providerInfoAria"));
|
|
580
1980
|
providerInfoButton.setAttribute("aria-expanded", "false");
|
|
581
1981
|
providerInfoButton.setAttribute("aria-controls", "dsh-mobile-provider-info");
|
|
582
1982
|
providerInfoButton.setAttribute("aria-describedby", "dsh-mobile-provider-info");
|
|
@@ -588,15 +1988,15 @@ window.__ModuleLoader__.load({
|
|
|
588
1988
|
providerInfoPopover.setAttribute("role", "tooltip");
|
|
589
1989
|
providerInfoPopover.hidden = true;
|
|
590
1990
|
const providerInfoTitle = element("strong");
|
|
591
|
-
providerInfoTitle.textContent = "
|
|
1991
|
+
providerInfoTitle.textContent = t("providerSafeTitle");
|
|
592
1992
|
const providerInfoText = element("span");
|
|
593
|
-
providerInfoText.textContent = "
|
|
1993
|
+
providerInfoText.textContent = t("providerSafeText");
|
|
594
1994
|
providerInfoButton.append(providerInfoGlyph);
|
|
595
1995
|
providerInfoPopover.append(providerInfoTitle, providerInfoText);
|
|
596
1996
|
providerInfo.append(providerInfoButton, providerInfoPopover);
|
|
597
1997
|
const providerChoices = element("div", "dsh-mobile-control__provider-choices");
|
|
598
1998
|
providerChoices.setAttribute("role", "radiogroup");
|
|
599
|
-
providerChoices.setAttribute("aria-label", "
|
|
1999
|
+
providerChoices.setAttribute("aria-label", t("providerGroupAria"));
|
|
600
2000
|
const tailscaleChoice = element("button", "dsh-mobile-control__provider");
|
|
601
2001
|
tailscaleChoice.type = "button";
|
|
602
2002
|
tailscaleChoice.setAttribute("role", "radio");
|
|
@@ -605,9 +2005,9 @@ window.__ModuleLoader__.load({
|
|
|
605
2005
|
const tailscaleChoiceName = element("strong");
|
|
606
2006
|
tailscaleChoiceName.textContent = "Tailscale Funnel";
|
|
607
2007
|
const tailscaleChoiceBadge = element("span", "dsh-mobile-control__provider-badge");
|
|
608
|
-
tailscaleChoiceBadge.textContent = "
|
|
2008
|
+
tailscaleChoiceBadge.textContent = t("builtIn");
|
|
609
2009
|
const tailscaleChoiceDescription = element("span", "dsh-mobile-control__provider-description");
|
|
610
|
-
tailscaleChoiceDescription.textContent = "
|
|
2010
|
+
tailscaleChoiceDescription.textContent = t("tailscaleDescription");
|
|
611
2011
|
tailscaleChoiceTop.append(tailscaleChoiceName, tailscaleChoiceBadge);
|
|
612
2012
|
tailscaleChoice.append(tailscaleChoiceTop, tailscaleChoiceDescription);
|
|
613
2013
|
const cpolarChoice = element("button", "dsh-mobile-control__provider");
|
|
@@ -618,37 +2018,36 @@ window.__ModuleLoader__.load({
|
|
|
618
2018
|
const cpolarChoiceName = element("strong");
|
|
619
2019
|
cpolarChoiceName.textContent = "cpolar";
|
|
620
2020
|
const cpolarChoiceBadge = element("span", "dsh-mobile-control__provider-badge is-cpolar");
|
|
621
|
-
cpolarChoiceBadge.textContent = "
|
|
2021
|
+
cpolarChoiceBadge.textContent = t("mainlandPreferred");
|
|
622
2022
|
const cpolarChoiceDescription = element("span", "dsh-mobile-control__provider-description");
|
|
623
|
-
cpolarChoiceDescription.textContent = "
|
|
2023
|
+
cpolarChoiceDescription.textContent = t("cpolarDescription");
|
|
624
2024
|
cpolarChoiceTop.append(cpolarChoiceName, cpolarChoiceBadge);
|
|
625
2025
|
cpolarChoice.append(cpolarChoiceTop, cpolarChoiceDescription);
|
|
626
2026
|
providerChoices.append(cpolarChoice, tailscaleChoice);
|
|
627
|
-
providerSection.append(providerHeading, providerInfo, providerChoices);
|
|
628
2027
|
const cpolarSetup = element("section", "dsh-mobile-control__cpolar-setup");
|
|
629
2028
|
cpolarSetup.hidden = true;
|
|
630
2029
|
const cpolarSetupTitle = element("h3", "dsh-mobile-control__section-title");
|
|
631
|
-
cpolarSetupTitle.textContent = "
|
|
2030
|
+
cpolarSetupTitle.textContent = t("prepareCpolar");
|
|
632
2031
|
const cpolarComponentStatus = element("p", "dsh-mobile-control__component-status");
|
|
633
|
-
cpolarComponentStatus.textContent = "
|
|
2032
|
+
cpolarComponentStatus.textContent = t("checkingComponent");
|
|
634
2033
|
const cpolarInstall = element("button", "dsh-mobile-control__primary");
|
|
635
2034
|
cpolarInstall.type = "button";
|
|
636
|
-
cpolarInstall.textContent = "
|
|
2035
|
+
cpolarInstall.textContent = t("installOfficial");
|
|
637
2036
|
const cpolarAccount = element("div", "dsh-mobile-control__cpolar-account");
|
|
638
2037
|
cpolarAccount.hidden = true;
|
|
639
2038
|
const cpolarAccountText = element("p", "dsh-mobile-control__component-note");
|
|
640
|
-
cpolarAccountText.textContent = "
|
|
2039
|
+
cpolarAccountText.textContent = t("cpolarAccountNote");
|
|
641
2040
|
const cpolarAccountLinks = element("div", "dsh-mobile-control__link-row");
|
|
642
2041
|
const cpolarSignup = element("a", "dsh-mobile-control__text-link");
|
|
643
2042
|
cpolarSignup.href = "https://dashboard.cpolar.com/signup";
|
|
644
2043
|
cpolarSignup.target = "_blank";
|
|
645
2044
|
cpolarSignup.rel = "noopener noreferrer";
|
|
646
|
-
cpolarSignup.textContent = "
|
|
2045
|
+
cpolarSignup.textContent = t("registerCpolar");
|
|
647
2046
|
const cpolarDashboard = element("a", "dsh-mobile-control__text-link");
|
|
648
2047
|
cpolarDashboard.href = "https://dashboard.cpolar.com/auth";
|
|
649
2048
|
cpolarDashboard.target = "_blank";
|
|
650
2049
|
cpolarDashboard.rel = "noopener noreferrer";
|
|
651
|
-
cpolarDashboard.textContent = "
|
|
2050
|
+
cpolarDashboard.textContent = t("openDashboard");
|
|
652
2051
|
cpolarAccountLinks.append(cpolarSignup, cpolarDashboard);
|
|
653
2052
|
const cpolarTokenLabel = element("label", "dsh-mobile-control__token-label");
|
|
654
2053
|
cpolarTokenLabel.textContent = "Authtoken";
|
|
@@ -656,48 +2055,182 @@ window.__ModuleLoader__.load({
|
|
|
656
2055
|
cpolarToken.type = "password";
|
|
657
2056
|
cpolarToken.autocomplete = "off";
|
|
658
2057
|
cpolarToken.spellcheck = false;
|
|
659
|
-
cpolarToken.placeholder = "
|
|
2058
|
+
cpolarToken.placeholder = t("tokenPlaceholder");
|
|
660
2059
|
cpolarTokenLabel.append(cpolarToken);
|
|
661
2060
|
const cpolarConfigure = element("button", "dsh-mobile-control__primary dsh-mobile-control__cpolar-connect");
|
|
662
2061
|
cpolarConfigure.type = "button";
|
|
663
|
-
cpolarConfigure.textContent = "
|
|
2062
|
+
cpolarConfigure.textContent = t("saveConnect");
|
|
664
2063
|
cpolarAccount.append(cpolarAccountText, cpolarAccountLinks, cpolarTokenLabel, cpolarConfigure);
|
|
665
2064
|
const cpolarDetails = element("details", "dsh-mobile-control__details");
|
|
666
2065
|
const cpolarDetailsSummary = element("summary");
|
|
667
|
-
cpolarDetailsSummary.textContent = "
|
|
2066
|
+
cpolarDetailsSummary.textContent = t("componentDetails");
|
|
668
2067
|
const cpolarDetailsBody = element("div", "dsh-mobile-control__details-body");
|
|
669
2068
|
const cpolarDetailsText = element("p");
|
|
670
|
-
cpolarDetailsText.textContent = "
|
|
2069
|
+
cpolarDetailsText.textContent = t("componentDetailsText");
|
|
671
2070
|
const cpolarStorage = element("code", "dsh-mobile-control__storage");
|
|
672
|
-
cpolarStorage.textContent = "
|
|
2071
|
+
cpolarStorage.textContent = t("pluginPrivateDirectory");
|
|
673
2072
|
const cpolarOfficial = element("a", "dsh-mobile-control__text-link");
|
|
674
2073
|
cpolarOfficial.href = "https://www.cpolar.com/download";
|
|
675
2074
|
cpolarOfficial.target = "_blank";
|
|
676
2075
|
cpolarOfficial.rel = "noopener noreferrer";
|
|
677
|
-
cpolarOfficial.textContent = "
|
|
2076
|
+
cpolarOfficial.textContent = t("officialDownload");
|
|
678
2077
|
const cpolarTerms = element("a", "dsh-mobile-control__text-link");
|
|
679
2078
|
cpolarTerms.href = "https://www.cpolar.com/tos";
|
|
680
2079
|
cpolarTerms.target = "_blank";
|
|
681
2080
|
cpolarTerms.rel = "noopener noreferrer";
|
|
682
|
-
cpolarTerms.textContent = "
|
|
2081
|
+
cpolarTerms.textContent = t("terms");
|
|
683
2082
|
const cpolarPurge = element("button", "dsh-mobile-control__danger");
|
|
684
2083
|
cpolarPurge.type = "button";
|
|
685
|
-
cpolarPurge.textContent = "
|
|
2084
|
+
cpolarPurge.textContent = t("purgeCpolar");
|
|
686
2085
|
cpolarDetailsBody.append(cpolarDetailsText, cpolarStorage, cpolarOfficial, cpolarTerms, cpolarPurge);
|
|
687
2086
|
cpolarDetails.append(cpolarDetailsSummary, cpolarDetailsBody);
|
|
688
2087
|
cpolarSetup.append(cpolarSetupTitle, cpolarComponentStatus, cpolarInstall, cpolarAccount, cpolarDetails);
|
|
2088
|
+
const selfHosted = element("details", "dsh-mobile-control__self-hosted");
|
|
2089
|
+
const selfHostedSummary = element("summary", "dsh-mobile-control__self-hosted-summary");
|
|
2090
|
+
const selfHostedSummaryText = element("span");
|
|
2091
|
+
const selfHostedSummaryTitle = element("strong");
|
|
2092
|
+
selfHostedSummaryTitle.textContent = t("selfHostedConnections");
|
|
2093
|
+
const selfHostedSummaryDescription = element("span");
|
|
2094
|
+
selfHostedSummaryDescription.textContent = t("selfHostedDescription");
|
|
2095
|
+
const selfHostedBadge = element("span", "dsh-mobile-control__provider-badge is-frp");
|
|
2096
|
+
selfHostedBadge.textContent = t("advanced");
|
|
2097
|
+
selfHostedSummaryText.append(selfHostedSummaryTitle, selfHostedSummaryDescription);
|
|
2098
|
+
selfHostedSummary.append(selfHostedSummaryText, selfHostedBadge);
|
|
2099
|
+
const selfHostedBody = element("div", "dsh-mobile-control__self-hosted-body");
|
|
2100
|
+
const frpChoice = element("button", "dsh-mobile-control__provider is-frp");
|
|
2101
|
+
frpChoice.type = "button";
|
|
2102
|
+
frpChoice.setAttribute("aria-pressed", "false");
|
|
2103
|
+
const frpChoiceTop = element("span", "dsh-mobile-control__provider-top");
|
|
2104
|
+
const frpChoiceName = element("strong");
|
|
2105
|
+
frpChoiceName.textContent = t("frpName");
|
|
2106
|
+
const frpChoiceDescription = element("span", "dsh-mobile-control__provider-description");
|
|
2107
|
+
frpChoiceDescription.textContent = t("frpDescription");
|
|
2108
|
+
frpChoiceTop.append(frpChoiceName);
|
|
2109
|
+
frpChoice.append(frpChoiceTop, frpChoiceDescription);
|
|
2110
|
+
selfHostedBody.append(frpChoice);
|
|
2111
|
+
selfHosted.append(selfHostedSummary, selfHostedBody);
|
|
2112
|
+
providerSection.append(providerHeading, providerInfo, providerChoices, selfHosted);
|
|
2113
|
+
const frpSetup = element("section", "dsh-mobile-control__frp-setup");
|
|
2114
|
+
frpSetup.hidden = true;
|
|
2115
|
+
const frpSetupTitle = element("h3", "dsh-mobile-control__section-title");
|
|
2116
|
+
frpSetupTitle.textContent = t("prepareFrp");
|
|
2117
|
+
const frpStep1 = element("section", "dsh-mobile-control__frp-step");
|
|
2118
|
+
const frpStep1Title = element("strong");
|
|
2119
|
+
frpStep1Title.textContent = t("frpStep1Title");
|
|
2120
|
+
const frpStep1Text = element("p");
|
|
2121
|
+
frpStep1Text.textContent = t("frpStep1Text");
|
|
2122
|
+
const frpFields = element("div", "dsh-mobile-control__frp-fields");
|
|
2123
|
+
const frpServerLabel = element("label", "dsh-mobile-control__field");
|
|
2124
|
+
frpServerLabel.textContent = t("frpServerAddress");
|
|
2125
|
+
const frpServer = element("input");
|
|
2126
|
+
frpServer.type = "text";
|
|
2127
|
+
frpServer.autocomplete = "off";
|
|
2128
|
+
frpServer.spellcheck = false;
|
|
2129
|
+
frpServer.placeholder = t("frpServerAddressPlaceholder");
|
|
2130
|
+
const frpPortLabel = element("label", "dsh-mobile-control__field");
|
|
2131
|
+
frpPortLabel.textContent = t("frpServerPort");
|
|
2132
|
+
const frpPort = element("input");
|
|
2133
|
+
frpPort.type = "number";
|
|
2134
|
+
frpPort.inputMode = "numeric";
|
|
2135
|
+
frpPort.min = "1";
|
|
2136
|
+
frpPort.max = "65535";
|
|
2137
|
+
frpPort.value = "7000";
|
|
2138
|
+
const frpTokenLabel = element("label", "dsh-mobile-control__field");
|
|
2139
|
+
frpTokenLabel.textContent = t("frpToken");
|
|
2140
|
+
const frpToken = element("input");
|
|
2141
|
+
frpToken.type = "password";
|
|
2142
|
+
frpToken.autocomplete = "off";
|
|
2143
|
+
frpToken.spellcheck = false;
|
|
2144
|
+
frpToken.placeholder = t("frpTokenPlaceholder");
|
|
2145
|
+
const frpOriginLabel = element("label", "dsh-mobile-control__field");
|
|
2146
|
+
frpOriginLabel.textContent = t("frpPublicOrigin");
|
|
2147
|
+
const frpOrigin = element("input");
|
|
2148
|
+
frpOrigin.type = "url";
|
|
2149
|
+
frpOrigin.autocomplete = "off";
|
|
2150
|
+
frpOrigin.spellcheck = false;
|
|
2151
|
+
frpOrigin.placeholder = t("frpPublicOriginPlaceholder");
|
|
2152
|
+
frpServerLabel.append(frpServer);
|
|
2153
|
+
frpPortLabel.append(frpPort);
|
|
2154
|
+
frpTokenLabel.append(frpToken);
|
|
2155
|
+
frpOriginLabel.append(frpOrigin);
|
|
2156
|
+
frpFields.append(frpServerLabel, frpPortLabel, frpTokenLabel, frpOriginLabel);
|
|
2157
|
+
frpStep1.append(frpStep1Title, frpStep1Text, frpFields);
|
|
2158
|
+
const frpStep2 = element("section", "dsh-mobile-control__frp-step");
|
|
2159
|
+
const frpStep2Title = element("strong");
|
|
2160
|
+
frpStep2Title.textContent = t("frpStep2Title");
|
|
2161
|
+
const frpStep2Text = element("p");
|
|
2162
|
+
frpStep2Text.textContent = t("frpStep2Text");
|
|
2163
|
+
const frpCopyTemplate = element("button", "dsh-mobile-control__secondary dsh-mobile-control__frp-action");
|
|
2164
|
+
frpCopyTemplate.type = "button";
|
|
2165
|
+
frpCopyTemplate.textContent = t("copyServerTemplate");
|
|
2166
|
+
frpStep2.append(frpStep2Title, frpStep2Text, frpCopyTemplate);
|
|
2167
|
+
const frpStep3 = element("section", "dsh-mobile-control__frp-step");
|
|
2168
|
+
const frpStep3Title = element("strong");
|
|
2169
|
+
frpStep3Title.textContent = t("frpStep3Title");
|
|
2170
|
+
const frpStep3Text = element("p");
|
|
2171
|
+
frpStep3Text.textContent = t("frpStep3Text");
|
|
2172
|
+
const frpComponentStatus = element("p", "dsh-mobile-control__component-status");
|
|
2173
|
+
frpComponentStatus.textContent = t("checkingComponent");
|
|
2174
|
+
const frpInstall = element("button", "dsh-mobile-control__primary dsh-mobile-control__frp-action");
|
|
2175
|
+
frpInstall.type = "button";
|
|
2176
|
+
frpInstall.textContent = t("installFrpc");
|
|
2177
|
+
frpStep3.append(frpStep3Title, frpStep3Text, frpComponentStatus, frpInstall);
|
|
2178
|
+
const frpStep4 = element("section", "dsh-mobile-control__frp-step");
|
|
2179
|
+
const frpStep4Title = element("strong");
|
|
2180
|
+
frpStep4Title.textContent = t("frpStep4Title");
|
|
2181
|
+
const frpStep4Text = element("p");
|
|
2182
|
+
frpStep4Text.textContent = t("frpStep4Text");
|
|
2183
|
+
const frpAppRequirement = element("p", "dsh-mobile-control__frp-requirement");
|
|
2184
|
+
frpAppRequirement.textContent = t("frpAppRequirement");
|
|
2185
|
+
const frpConfigurationStatus = element("p", "dsh-mobile-control__component-status");
|
|
2186
|
+
frpConfigurationStatus.textContent = t("frpConfigurationMissing");
|
|
2187
|
+
const frpConfigure = element("button", "dsh-mobile-control__primary dsh-mobile-control__frp-action");
|
|
2188
|
+
frpConfigure.type = "button";
|
|
2189
|
+
frpConfigure.textContent = t("frpSaveConnect");
|
|
2190
|
+
frpStep4.append(frpStep4Title, frpStep4Text, frpAppRequirement, frpConfigurationStatus, frpConfigure);
|
|
2191
|
+
const frpDetails = element("details", "dsh-mobile-control__details");
|
|
2192
|
+
const frpDetailsSummary = element("summary");
|
|
2193
|
+
frpDetailsSummary.textContent = t("frpComponentDetails");
|
|
2194
|
+
const frpDetailsBody = element("div", "dsh-mobile-control__details-body");
|
|
2195
|
+
const frpDetailsText = element("p");
|
|
2196
|
+
frpDetailsText.textContent = t("frpComponentDetailsText");
|
|
2197
|
+
const frpStorage = element("code", "dsh-mobile-control__storage");
|
|
2198
|
+
frpStorage.textContent = t("pluginPrivateDirectory");
|
|
2199
|
+
const frpOfficial = element("a", "dsh-mobile-control__text-link");
|
|
2200
|
+
frpOfficial.href = "https://github.com/fatedier/frp/releases/tag/v0.70.1";
|
|
2201
|
+
frpOfficial.target = "_blank";
|
|
2202
|
+
frpOfficial.rel = "noopener noreferrer";
|
|
2203
|
+
frpOfficial.textContent = t("frpOfficialRelease");
|
|
2204
|
+
const frpPurge = element("button", "dsh-mobile-control__danger");
|
|
2205
|
+
frpPurge.type = "button";
|
|
2206
|
+
frpPurge.textContent = t("purgeFrp");
|
|
2207
|
+
frpDetailsBody.append(frpDetailsText, frpStorage, frpOfficial, frpPurge);
|
|
2208
|
+
frpDetails.append(frpDetailsSummary, frpDetailsBody);
|
|
2209
|
+
frpSetup.append(frpSetupTitle, frpStep1, frpStep2, frpStep3, frpStep4, frpDetails);
|
|
689
2210
|
const tailscaleInfo = element("details", "dsh-mobile-control__details");
|
|
690
2211
|
const tailscaleInfoSummary = element("summary");
|
|
691
|
-
tailscaleInfoSummary.textContent = "
|
|
2212
|
+
tailscaleInfoSummary.textContent = t("tailscaleHelp");
|
|
692
2213
|
const tailscaleInfoBody = element("div", "dsh-mobile-control__details-body");
|
|
693
2214
|
const tailscaleInfoText = element("p");
|
|
694
|
-
tailscaleInfoText.textContent = "
|
|
2215
|
+
tailscaleInfoText.textContent = t("tailscaleHelpText");
|
|
695
2216
|
tailscaleInfoBody.append(tailscaleInfoText);
|
|
696
2217
|
tailscaleInfo.append(tailscaleInfoSummary, tailscaleInfoBody);
|
|
2218
|
+
const providerSetupHeader = element("div", "dsh-mobile-control__stage-header");
|
|
2219
|
+
const providerSetupHeading = element("h3", "dsh-mobile-control__section-title");
|
|
2220
|
+
providerSetupHeading.textContent = t("currentProvider");
|
|
2221
|
+
const providerSetupName = element("span", "dsh-mobile-control__stage-value");
|
|
2222
|
+
providerSetupName.textContent = "Tailscale Funnel";
|
|
2223
|
+
const remoteStateBadge = element("span", "dsh-mobile-control__state-badge");
|
|
2224
|
+
remoteStateBadge.textContent = t("remoteStateOff");
|
|
2225
|
+
const providerSetupMeta = element("div", "dsh-mobile-control__stage-meta");
|
|
2226
|
+
providerSetupMeta.append(providerSetupName, remoteStateBadge);
|
|
2227
|
+
providerSetupHeader.append(providerSetupHeading, providerSetupMeta);
|
|
2228
|
+
const providerSetupBody = element("div", "dsh-mobile-control__provider-setup-body");
|
|
2229
|
+
providerSetupBody.append(cpolarSetup, frpSetup, tailscaleInfo);
|
|
697
2230
|
const remoteAccess = element("div", "dsh-mobile-control__access");
|
|
698
2231
|
remoteAccess.hidden = true;
|
|
699
2232
|
const remoteAccessLabel = element("span", "dsh-mobile-control__access-label");
|
|
700
|
-
remoteAccessLabel.textContent = "
|
|
2233
|
+
remoteAccessLabel.textContent = t("remoteAddress");
|
|
701
2234
|
const remoteAccessLink = element("a", "dsh-mobile-control__access-link");
|
|
702
2235
|
remoteAccessLink.target = "_blank";
|
|
703
2236
|
remoteAccessLink.rel = "noreferrer";
|
|
@@ -705,68 +2238,70 @@ window.__ModuleLoader__.load({
|
|
|
705
2238
|
const remoteQr = element("div", "dsh-mobile-control__qr");
|
|
706
2239
|
remoteQr.hidden = true;
|
|
707
2240
|
const remoteStatus = element("p", "dsh-mobile-control__status");
|
|
708
|
-
remoteStatus.textContent = "
|
|
2241
|
+
remoteStatus.textContent = t("loadingRemoteStatus");
|
|
709
2242
|
remoteStatus.setAttribute("aria-live", "polite");
|
|
710
2243
|
const remoteGuide = element("section", "dsh-mobile-control__guide");
|
|
711
2244
|
remoteGuide.hidden = true;
|
|
712
|
-
remoteGuide.setAttribute("aria-label", "
|
|
2245
|
+
remoteGuide.setAttribute("aria-label", t("funnelGuideAria"));
|
|
713
2246
|
const remoteGuideTitle = element("h3", "dsh-mobile-control__guide-title");
|
|
714
|
-
remoteGuideTitle.textContent = "
|
|
2247
|
+
remoteGuideTitle.textContent = t("funnelGuideTitle");
|
|
715
2248
|
const remoteGuideSummary = element("p", "dsh-mobile-control__guide-summary");
|
|
716
|
-
remoteGuideSummary.textContent = "
|
|
2249
|
+
remoteGuideSummary.textContent = t("funnelGuideSummary");
|
|
717
2250
|
const remoteGuideSteps = element("ol", "dsh-mobile-control__guide-steps");
|
|
718
2251
|
for (const text of [
|
|
719
|
-
"
|
|
720
|
-
"
|
|
721
|
-
"
|
|
2252
|
+
t("funnelStep1"),
|
|
2253
|
+
t("funnelStep2"),
|
|
2254
|
+
t("funnelStep3")
|
|
722
2255
|
]) {
|
|
723
2256
|
const item = element("li");
|
|
724
2257
|
item.textContent = text;
|
|
725
2258
|
remoteGuideSteps.append(item);
|
|
726
2259
|
}
|
|
727
2260
|
const remoteGuideNote = element("p", "dsh-mobile-control__guide-note");
|
|
728
|
-
remoteGuideNote.textContent = "
|
|
2261
|
+
remoteGuideNote.textContent = t("funnelGuideNote");
|
|
729
2262
|
const remoteGuideActions = element("div", "dsh-mobile-control__guide-actions");
|
|
730
2263
|
const remoteSetup = element("button", "dsh-mobile-control__primary");
|
|
731
2264
|
remoteSetup.type = "button";
|
|
732
|
-
remoteSetup.textContent = "
|
|
2265
|
+
remoteSetup.textContent = t("continueFunnel");
|
|
733
2266
|
const remoteSetupRetry = element("button", "dsh-mobile-control__secondary");
|
|
734
2267
|
remoteSetupRetry.type = "button";
|
|
735
|
-
remoteSetupRetry.textContent = "
|
|
2268
|
+
remoteSetupRetry.textContent = t("retryNow");
|
|
736
2269
|
remoteGuideActions.append(remoteSetup, remoteSetupRetry);
|
|
737
2270
|
remoteGuide.append(remoteGuideTitle, remoteGuideSummary, remoteGuideSteps, remoteGuideNote, remoteGuideActions);
|
|
738
2271
|
const remoteActions = element("div", "dsh-mobile-control__actions");
|
|
739
2272
|
const remoteToggle = element("button", "dsh-mobile-control__primary");
|
|
740
2273
|
remoteToggle.type = "button";
|
|
741
|
-
remoteToggle.textContent = "
|
|
2274
|
+
remoteToggle.textContent = t("enableRemote");
|
|
742
2275
|
const remoteLogin = element("button", "dsh-mobile-control__primary");
|
|
743
2276
|
remoteLogin.type = "button";
|
|
744
|
-
remoteLogin.textContent = "
|
|
2277
|
+
remoteLogin.textContent = t("continueLogin");
|
|
745
2278
|
remoteLogin.hidden = true;
|
|
746
2279
|
const remoteReconnect = element("button", "dsh-mobile-control__secondary");
|
|
747
2280
|
remoteReconnect.type = "button";
|
|
748
|
-
remoteReconnect.textContent = "
|
|
2281
|
+
remoteReconnect.textContent = t("reconnect");
|
|
749
2282
|
remoteReconnect.hidden = true;
|
|
750
2283
|
const remotePair = element("button", "dsh-mobile-control__secondary");
|
|
751
2284
|
remotePair.type = "button";
|
|
752
|
-
remotePair.textContent = "
|
|
2285
|
+
remotePair.textContent = t("generateRemoteQr");
|
|
753
2286
|
remotePair.disabled = true;
|
|
754
2287
|
remoteActions.append(remoteToggle, remoteLogin, remoteReconnect, remotePair);
|
|
755
2288
|
const remoteManageRow = element("div", "dsh-mobile-control__manage-row");
|
|
756
2289
|
const remoteDevices = element("button", "dsh-mobile-control__manage");
|
|
757
2290
|
remoteDevices.type = "button";
|
|
758
|
-
remoteDevices.textContent = "
|
|
2291
|
+
remoteDevices.textContent = t("manageRemoteDevices");
|
|
759
2292
|
remoteDevices.disabled = true;
|
|
760
2293
|
const remoteReset = element("button", "dsh-mobile-control__manage");
|
|
761
2294
|
remoteReset.type = "button";
|
|
762
|
-
remoteReset.textContent = "
|
|
2295
|
+
remoteReset.textContent = t("resetRemoteLogin");
|
|
763
2296
|
remoteManageRow.append(remoteDevices, remoteReset);
|
|
764
2297
|
const remoteDevicePanel = element("div", "dsh-mobile-control__devices");
|
|
765
2298
|
remoteDevicePanel.hidden = true;
|
|
2299
|
+
const remoteWorkspace = element("section", "dsh-mobile-control__remote-workspace");
|
|
2300
|
+
remoteWorkspace.append(providerSetupHeader, remoteStatus, remoteAccess, remoteGuide, providerSetupBody, remoteActions, remoteQr, remoteManageRow, remoteDevicePanel);
|
|
766
2301
|
const diagnosticsView = element("div", "dsh-mobile-control__view is-diagnostics");
|
|
767
2302
|
diagnosticsView.hidden = true;
|
|
768
2303
|
const diagnosticsIntro = element("p", "dsh-mobile-control__intro");
|
|
769
|
-
diagnosticsIntro.textContent = "
|
|
2304
|
+
diagnosticsIntro.textContent = t("diagnosticsIntro");
|
|
770
2305
|
const diagnosticsSummary = element("section", "dsh-mobile-control__diagnostic-summary is-idle");
|
|
771
2306
|
diagnosticsSummary.setAttribute("aria-live", "polite");
|
|
772
2307
|
const diagnosticsSummaryMain = element("div", "dsh-mobile-control__diagnostic-summary-main");
|
|
@@ -774,21 +2309,21 @@ window.__ModuleLoader__.load({
|
|
|
774
2309
|
diagnosticsSummaryIcon.setAttribute("aria-hidden", "true");
|
|
775
2310
|
const diagnosticsSummaryBody = element("div", "dsh-mobile-control__diagnostic-summary-body");
|
|
776
2311
|
const diagnosticsSummaryTitle = element("strong");
|
|
777
|
-
diagnosticsSummaryTitle.textContent = "
|
|
2312
|
+
diagnosticsSummaryTitle.textContent = t("diagnosticsNotRun");
|
|
778
2313
|
const diagnosticsSummaryText = element("span");
|
|
779
|
-
diagnosticsSummaryText.textContent = "
|
|
2314
|
+
diagnosticsSummaryText.textContent = t("diagnosticsStartHint");
|
|
780
2315
|
const diagnosticsSummaryMeta = element("span", "dsh-mobile-control__diagnostic-summary-meta");
|
|
781
|
-
diagnosticsSummaryMeta.textContent = "
|
|
2316
|
+
diagnosticsSummaryMeta.textContent = t("diagnosticsIdleMeta");
|
|
782
2317
|
diagnosticsSummaryBody.append(diagnosticsSummaryTitle, diagnosticsSummaryText);
|
|
783
2318
|
diagnosticsSummaryMain.append(diagnosticsSummaryIcon, diagnosticsSummaryBody);
|
|
784
2319
|
diagnosticsSummary.append(diagnosticsSummaryMain, diagnosticsSummaryMeta);
|
|
785
2320
|
const diagnosticsToolbar = element("div", "dsh-mobile-control__diagnostic-toolbar");
|
|
786
2321
|
const diagnosticsRun = element("button", "dsh-mobile-control__primary dsh-mobile-control__diagnostic-run");
|
|
787
2322
|
diagnosticsRun.type = "button";
|
|
788
|
-
diagnosticsRun.textContent = "
|
|
2323
|
+
diagnosticsRun.textContent = t("diagnosticsStart");
|
|
789
2324
|
const diagnosticsCopy = element("button", "dsh-mobile-control__secondary dsh-mobile-control__diagnostic-copy");
|
|
790
2325
|
diagnosticsCopy.type = "button";
|
|
791
|
-
diagnosticsCopy.textContent = "
|
|
2326
|
+
diagnosticsCopy.textContent = t("diagnosticsCopy");
|
|
792
2327
|
diagnosticsCopy.disabled = true;
|
|
793
2328
|
diagnosticsCopy.hidden = true;
|
|
794
2329
|
diagnosticsToolbar.append(diagnosticsRun, diagnosticsCopy);
|
|
@@ -801,15 +2336,15 @@ window.__ModuleLoader__.load({
|
|
|
801
2336
|
const diagnosticsDetails = element("details", "dsh-mobile-control__details dsh-mobile-control__diagnostic-details");
|
|
802
2337
|
diagnosticsDetails.hidden = true;
|
|
803
2338
|
const diagnosticsDetailsSummary = element("summary");
|
|
804
|
-
diagnosticsDetailsSummary.textContent = "
|
|
2339
|
+
diagnosticsDetailsSummary.textContent = t("diagnosticsAdvanced");
|
|
805
2340
|
const diagnosticsReport = element("pre", "dsh-mobile-control__diagnostic-report");
|
|
806
2341
|
diagnosticsDetails.append(diagnosticsDetailsSummary, diagnosticsReport);
|
|
807
2342
|
header.append(title, headerActions);
|
|
808
2343
|
actions.append(toggle, pair, linkPair);
|
|
809
2344
|
lanView.append(access, qrBox, status, extensionStatus, actions, manageRow, devicePanel);
|
|
810
|
-
remoteView.append(remoteIntro, providerSection,
|
|
2345
|
+
remoteView.append(remoteIntro, providerSection, remoteWorkspace);
|
|
811
2346
|
diagnosticsView.append(diagnosticsIntro, diagnosticsSummary, diagnosticsToolbar, diagnosticsFeedback, diagnosticsChecks, diagnosticsDetails);
|
|
812
|
-
panel.append(header, appDownload, switcher, lanView, remoteView, diagnosticsView);
|
|
2347
|
+
panel.append(header, releaseNotice, appDownload, switcher, lanView, remoteView, diagnosticsView);
|
|
813
2348
|
root.append(panel);
|
|
814
2349
|
document.body.append(root);
|
|
815
2350
|
let running = false;
|
|
@@ -825,11 +2360,32 @@ window.__ModuleLoader__.load({
|
|
|
825
2360
|
let remoteProviderBusy = false;
|
|
826
2361
|
let cpolarInstalled = false;
|
|
827
2362
|
let cpolarConfigured = false;
|
|
2363
|
+
let frpInstalled = false;
|
|
2364
|
+
let frpConfigured = false;
|
|
2365
|
+
let frpDownloadSize = "14.0";
|
|
2366
|
+
let configuredFrpServer = "";
|
|
2367
|
+
let configuredFrpPort = 7e3;
|
|
2368
|
+
let configuredFrpOrigin = "";
|
|
828
2369
|
let providerInfoPinned = false;
|
|
829
2370
|
let providerInfoHovered = false;
|
|
830
2371
|
let previousAccessView = "lan";
|
|
831
2372
|
let diagnosticsBusy = false;
|
|
832
2373
|
let copiedDiagnosticReport = "";
|
|
2374
|
+
let pluginUpdateAvailable = false;
|
|
2375
|
+
let pluginLatestVersion = "";
|
|
2376
|
+
const renderRelease = (data) => {
|
|
2377
|
+
const release = clientReleaseInfo(data);
|
|
2378
|
+
pluginUpdateAvailable = release.updateAvailable;
|
|
2379
|
+
pluginLatestVersion = release.latestVersion ?? "";
|
|
2380
|
+
updatePlugin.hidden = !pluginUpdateAvailable || !diagnosticsView.hidden;
|
|
2381
|
+
updatePlugin.textContent = t("updatePlugin");
|
|
2382
|
+
if (pluginLatestVersion !== "") updatePlugin.setAttribute("aria-label", t("updatePluginAria", { version: pluginLatestVersion }));
|
|
2383
|
+
if (release.androidVersion !== void 0) {
|
|
2384
|
+
appDownload.textContent = t("downloadAndroidVersion", { version: release.androidVersion });
|
|
2385
|
+
appDownload.setAttribute("aria-label", t("downloadAndroidVersionAria", { version: release.androidVersion }));
|
|
2386
|
+
}
|
|
2387
|
+
appDownload.href = release.androidDownloadUrl;
|
|
2388
|
+
};
|
|
833
2389
|
const syncProviderInfo = () => {
|
|
834
2390
|
const open = providerInfoPinned || providerInfoHovered || providerInfo.contains(document.activeElement);
|
|
835
2391
|
providerInfoPopover.hidden = !open;
|
|
@@ -868,11 +2424,12 @@ window.__ModuleLoader__.load({
|
|
|
868
2424
|
lanTab.setAttribute("aria-pressed", String(view === "lan"));
|
|
869
2425
|
remoteTab.setAttribute("aria-pressed", String(view === "remote"));
|
|
870
2426
|
diagnosticsEntry.setAttribute("aria-pressed", String(view === "diagnostics"));
|
|
871
|
-
diagnosticsEntry.textContent = view === "diagnostics" ? "
|
|
872
|
-
diagnosticsEntry.setAttribute("aria-label", view === "diagnostics" ? "
|
|
2427
|
+
diagnosticsEntry.textContent = view === "diagnostics" ? t("back") : t("diagnostics");
|
|
2428
|
+
diagnosticsEntry.setAttribute("aria-label", view === "diagnostics" ? t("backToMobile") : t("openDiagnostics"));
|
|
2429
|
+
updatePlugin.hidden = view === "diagnostics" || !pluginUpdateAvailable;
|
|
873
2430
|
appDownload.hidden = view === "diagnostics";
|
|
874
2431
|
switcher.hidden = view === "diagnostics";
|
|
875
|
-
title.textContent = view === "lan" ? "
|
|
2432
|
+
title.textContent = view === "lan" ? t("lanAccess") : view === "remote" ? t("remoteAccess") : t("connectionDiagnostics");
|
|
876
2433
|
};
|
|
877
2434
|
lanTab.addEventListener("click", () => {
|
|
878
2435
|
selectView("lan");
|
|
@@ -893,16 +2450,19 @@ window.__ModuleLoader__.load({
|
|
|
893
2450
|
accessLink.textContent = origin;
|
|
894
2451
|
accessLink.title = origin;
|
|
895
2452
|
status.classList.toggle("is-running", running);
|
|
896
|
-
status.textContent = running ? "
|
|
2453
|
+
status.textContent = running ? t("lanOn") : t("lanOff");
|
|
897
2454
|
const extensionData = data.extensions;
|
|
898
2455
|
if (extensionData !== null && typeof extensionData === "object") {
|
|
899
2456
|
const loaded = typeof extensionData.loaded === "number" ? extensionData.loaded : 0;
|
|
900
2457
|
const failed = typeof extensionData.failed === "number" ? extensionData.failed : 0;
|
|
901
2458
|
extensionStatus.hidden = false;
|
|
902
|
-
extensionStatus.textContent = failed === 0 ?
|
|
2459
|
+
extensionStatus.textContent = failed === 0 ? t("extensionsLoaded", { loaded }) : t("extensionsFailed", {
|
|
2460
|
+
loaded,
|
|
2461
|
+
failed
|
|
2462
|
+
});
|
|
903
2463
|
} else extensionStatus.hidden = true;
|
|
904
2464
|
if (!running) qrBox.hidden = true;
|
|
905
|
-
toggle.textContent = running ? "
|
|
2465
|
+
toggle.textContent = running ? t("disableLan") : t("enableLan");
|
|
906
2466
|
pair.disabled = !running;
|
|
907
2467
|
linkPair.disabled = !running;
|
|
908
2468
|
manageDevices.disabled = !running;
|
|
@@ -915,7 +2475,7 @@ window.__ModuleLoader__.load({
|
|
|
915
2475
|
return;
|
|
916
2476
|
}
|
|
917
2477
|
const image = element("img");
|
|
918
|
-
image.alt = "
|
|
2478
|
+
image.alt = t("pairingQr");
|
|
919
2479
|
image.width = 176;
|
|
920
2480
|
image.height = 176;
|
|
921
2481
|
image.src = `data:image/svg+xml;base64,${btoa(svg)}`;
|
|
@@ -923,25 +2483,28 @@ window.__ModuleLoader__.load({
|
|
|
923
2483
|
target.append(image);
|
|
924
2484
|
};
|
|
925
2485
|
const openPairing = (target) => {
|
|
926
|
-
|
|
2486
|
+
controlRequestJson("/api/mobile-access/lan/pairing/open", {
|
|
927
2487
|
method: "POST",
|
|
928
2488
|
body: "{}"
|
|
929
2489
|
}).then(async (data) => {
|
|
930
2490
|
const value = target === "key" ? typeof data.appKey === "string" ? data.appKey : "" : typeof data.pairUrl === "string" ? data.pairUrl : "";
|
|
931
2491
|
showQr(typeof data.qrSvg === "string" ? data.qrSvg : "");
|
|
932
2492
|
if (value === "") {
|
|
933
|
-
status.textContent = "
|
|
2493
|
+
status.textContent = t("keyGenerationFailed");
|
|
934
2494
|
return;
|
|
935
2495
|
}
|
|
936
2496
|
try {
|
|
937
2497
|
await navigator.clipboard.writeText(value);
|
|
938
|
-
status.textContent = target === "key" ? "
|
|
2498
|
+
status.textContent = target === "key" ? t("keyCopied") : t("linkCopied");
|
|
939
2499
|
} catch {
|
|
940
|
-
status.textContent =
|
|
2500
|
+
status.textContent = t("copySecret", {
|
|
2501
|
+
kind: target === "key" ? t("pairingKey") : t("pairingLink"),
|
|
2502
|
+
value
|
|
2503
|
+
});
|
|
941
2504
|
status.classList.add("is-key");
|
|
942
2505
|
}
|
|
943
2506
|
}, (error) => {
|
|
944
|
-
status.textContent = String(error);
|
|
2507
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
945
2508
|
}).finally(() => {
|
|
946
2509
|
pair.disabled = !running;
|
|
947
2510
|
linkPair.disabled = !running;
|
|
@@ -949,41 +2512,45 @@ window.__ModuleLoader__.load({
|
|
|
949
2512
|
};
|
|
950
2513
|
toggle.addEventListener("click", () => {
|
|
951
2514
|
toggle.disabled = true;
|
|
952
|
-
|
|
2515
|
+
controlRequestJson("/api/mobile-access/lan/control", {
|
|
953
2516
|
method: "POST",
|
|
954
2517
|
body: JSON.stringify({ running: !running })
|
|
955
2518
|
}).then(render, (error) => {
|
|
956
|
-
status.textContent = String(error);
|
|
2519
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
957
2520
|
}).finally(() => {
|
|
958
2521
|
toggle.disabled = false;
|
|
959
2522
|
});
|
|
960
2523
|
});
|
|
961
|
-
const formatTime = (ms) => typeof ms === "number" ? new Date(ms).toLocaleString() : "";
|
|
2524
|
+
const formatTime = (ms) => typeof ms === "number" ? new Date(ms).toLocaleString(localeTag) : "";
|
|
2525
|
+
const formatMegabytes = (bytes) => new Intl.NumberFormat(localeTag, {
|
|
2526
|
+
minimumFractionDigits: 1,
|
|
2527
|
+
maximumFractionDigits: 1
|
|
2528
|
+
}).format(bytes / 1024 / 1024);
|
|
962
2529
|
const renderDevices = (data) => {
|
|
963
2530
|
const devices = Array.isArray(data.devices) ? data.devices : [];
|
|
964
2531
|
devicePanel.replaceChildren();
|
|
965
2532
|
if (devices.length === 0) {
|
|
966
2533
|
const empty = element("p", "dsh-mobile-control__device-empty");
|
|
967
|
-
empty.textContent = "
|
|
2534
|
+
empty.textContent = t("noDevices");
|
|
968
2535
|
devicePanel.append(empty);
|
|
969
2536
|
return;
|
|
970
2537
|
}
|
|
971
2538
|
for (const device of devices) {
|
|
972
2539
|
const row = element("div", "dsh-mobile-control__device");
|
|
973
2540
|
const label = element("span", "dsh-mobile-control__device-label");
|
|
974
|
-
label.textContent = typeof device.label === "string" ? device.label : "
|
|
2541
|
+
label.textContent = typeof device.label === "string" ? device.label : t("device");
|
|
975
2542
|
const meta = element("span", "dsh-mobile-control__device-meta");
|
|
976
|
-
meta.textContent =
|
|
2543
|
+
meta.textContent = t("expires", { time: formatTime(device.expiresAt) });
|
|
977
2544
|
const revoke = element("button", "dsh-mobile-control__device-revoke");
|
|
978
2545
|
revoke.type = "button";
|
|
979
|
-
revoke.textContent = "
|
|
2546
|
+
revoke.textContent = t("revoke");
|
|
980
2547
|
const id = typeof device.id === "string" ? device.id : "";
|
|
981
2548
|
revoke.addEventListener("click", () => {
|
|
982
|
-
|
|
2549
|
+
controlRequestJson("/api/mobile-access/lan/devices/revoke", {
|
|
983
2550
|
method: "POST",
|
|
984
2551
|
body: JSON.stringify({ deviceId: id })
|
|
985
2552
|
}).then(loadDevices, (error) => {
|
|
986
|
-
status.textContent = String(error);
|
|
2553
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
987
2554
|
});
|
|
988
2555
|
});
|
|
989
2556
|
row.append(label, meta, revoke);
|
|
@@ -991,8 +2558,8 @@ window.__ModuleLoader__.load({
|
|
|
991
2558
|
}
|
|
992
2559
|
};
|
|
993
2560
|
const loadDevices = () => {
|
|
994
|
-
|
|
995
|
-
status.textContent = String(error);
|
|
2561
|
+
controlRequestJson("/api/mobile-access/lan/devices").then(renderDevices, (error) => {
|
|
2562
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
996
2563
|
});
|
|
997
2564
|
};
|
|
998
2565
|
manageDevices.addEventListener("click", () => {
|
|
@@ -1001,51 +2568,85 @@ window.__ModuleLoader__.load({
|
|
|
1001
2568
|
if (show) loadDevices();
|
|
1002
2569
|
});
|
|
1003
2570
|
resetAll.addEventListener("click", () => {
|
|
1004
|
-
if (!window.confirm("
|
|
1005
|
-
|
|
2571
|
+
if (!window.confirm(t("confirmResetDevices"))) return;
|
|
2572
|
+
controlRequestJson("/api/mobile-access/lan/devices/reset", {
|
|
1006
2573
|
method: "POST",
|
|
1007
2574
|
body: JSON.stringify({ confirm: true })
|
|
1008
2575
|
}).then(loadDevices, (error) => {
|
|
1009
|
-
status.textContent = String(error);
|
|
2576
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
1010
2577
|
});
|
|
1011
2578
|
});
|
|
1012
2579
|
const renderRemote = (data) => {
|
|
1013
2580
|
remoteRunning = data.running === true;
|
|
1014
|
-
remoteProvider = data.provider === "cpolar" ? "cpolar" : "tailscale";
|
|
2581
|
+
remoteProvider = data.provider === "cpolar" ? "cpolar" : data.provider === "frp" ? "frp" : "tailscale";
|
|
1015
2582
|
const cpolar = remoteProvider === "cpolar";
|
|
1016
|
-
|
|
2583
|
+
const frp = remoteProvider === "frp";
|
|
2584
|
+
const tailscale = remoteProvider === "tailscale";
|
|
2585
|
+
tailscaleChoice.classList.toggle("is-selected", tailscale);
|
|
1017
2586
|
cpolarChoice.classList.toggle("is-selected", cpolar);
|
|
1018
|
-
|
|
2587
|
+
frpChoice.classList.toggle("is-selected", frp);
|
|
2588
|
+
tailscaleChoice.setAttribute("aria-checked", String(tailscale));
|
|
1019
2589
|
cpolarChoice.setAttribute("aria-checked", String(cpolar));
|
|
2590
|
+
frpChoice.setAttribute("aria-pressed", String(frp));
|
|
2591
|
+
providerSetupName.textContent = cpolar ? "cpolar" : frp ? t("frpName") : "Tailscale Funnel";
|
|
1020
2592
|
tailscaleChoice.disabled = remoteProviderBusy;
|
|
1021
2593
|
cpolarChoice.disabled = remoteProviderBusy;
|
|
2594
|
+
frpChoice.disabled = remoteProviderBusy;
|
|
1022
2595
|
cpolarSetup.hidden = !cpolar;
|
|
1023
|
-
|
|
1024
|
-
|
|
2596
|
+
frpSetup.hidden = !frp;
|
|
2597
|
+
if (frp) selfHosted.open = true;
|
|
2598
|
+
tailscaleInfo.hidden = !tailscale;
|
|
2599
|
+
remoteReset.textContent = tailscale ? t("resetRemoteLogin") : t("resetRemoteDevices");
|
|
1025
2600
|
const providers = data.providers !== null && typeof data.providers === "object" ? data.providers : {};
|
|
1026
2601
|
const cpolarProvider = providers.cpolar !== null && typeof providers.cpolar === "object" ? providers.cpolar : {};
|
|
1027
2602
|
const component = cpolarProvider.component !== null && typeof cpolarProvider.component === "object" ? cpolarProvider.component : {};
|
|
1028
2603
|
cpolarInstalled = component.installed === true;
|
|
1029
2604
|
cpolarConfigured = component.configured === true;
|
|
1030
|
-
cpolarChoiceBadge.textContent = cpolarConfigured ? "
|
|
2605
|
+
cpolarChoiceBadge.textContent = cpolarConfigured ? t("ready") : cpolarInstalled ? t("installed") : t("mainlandPreferred");
|
|
1031
2606
|
const cpolarSupported = component.supported !== false;
|
|
1032
2607
|
const componentVersion = typeof component.version === "string" ? component.version : "";
|
|
1033
2608
|
const componentDownloadBytes = typeof component.downloadBytes === "number" ? component.downloadBytes : 0;
|
|
1034
|
-
const componentStorage = typeof component.storagePath === "string" ? component.storagePath :
|
|
2609
|
+
const componentStorage = typeof component.storagePath === "string" ? component.storagePath : `DSH Mobile ${t("pluginPrivateDirectory")}`;
|
|
1035
2610
|
cpolarStorage.textContent = componentStorage;
|
|
1036
2611
|
cpolarStorage.title = componentStorage;
|
|
1037
2612
|
cpolarInstall.hidden = cpolarInstalled || !cpolarSupported;
|
|
1038
|
-
cpolarInstall.textContent = componentDownloadBytes > 0 ?
|
|
2613
|
+
cpolarInstall.textContent = componentDownloadBytes > 0 ? t("installWithSize", { size: formatMegabytes(componentDownloadBytes) }) : t("installOfficial");
|
|
1039
2614
|
cpolarInstall.disabled = remoteProviderBusy;
|
|
1040
2615
|
cpolarAccount.hidden = !cpolarInstalled || cpolarConfigured;
|
|
1041
2616
|
cpolarConfigure.disabled = remoteProviderBusy;
|
|
1042
2617
|
cpolarPurge.hidden = !cpolarInstalled && !cpolarConfigured;
|
|
1043
|
-
cpolarComponentStatus.textContent = !cpolarSupported ? "
|
|
2618
|
+
cpolarComponentStatus.textContent = !cpolarSupported ? t("cpolarUnsupported") : !cpolarInstalled ? t("cpolarNotInstalled") : !cpolarConfigured ? t("cpolarNeedsToken", { version: componentVersion }) : t("cpolarReady", { version: componentVersion });
|
|
2619
|
+
const frpProvider = providers.frp !== null && typeof providers.frp === "object" ? providers.frp : {};
|
|
2620
|
+
const frpComponent = frpProvider.component !== null && typeof frpProvider.component === "object" ? frpProvider.component : {};
|
|
2621
|
+
const frpConfiguration = frpProvider.configuration !== null && typeof frpProvider.configuration === "object" ? frpProvider.configuration : {};
|
|
2622
|
+
frpInstalled = frpComponent.installed === true;
|
|
2623
|
+
frpConfigured = frpConfiguration.configured === true;
|
|
2624
|
+
const frpSupported = frpComponent.supported !== false;
|
|
2625
|
+
const frpVersion = typeof frpComponent.version === "string" ? frpComponent.version : "";
|
|
2626
|
+
const frpDownloadBytes = typeof frpComponent.downloadBytes === "number" ? frpComponent.downloadBytes : 0;
|
|
2627
|
+
if (frpDownloadBytes > 0) frpDownloadSize = formatMegabytes(frpDownloadBytes);
|
|
2628
|
+
const frpStoragePath = typeof frpComponent.storagePath === "string" ? frpComponent.storagePath : `DSH Mobile ${t("pluginPrivateDirectory")}`;
|
|
2629
|
+
configuredFrpServer = typeof frpConfiguration.serverAddress === "string" ? frpConfiguration.serverAddress : "";
|
|
2630
|
+
configuredFrpPort = typeof frpConfiguration.serverPort === "number" ? frpConfiguration.serverPort : 7e3;
|
|
2631
|
+
configuredFrpOrigin = typeof frpConfiguration.publicOrigin === "string" ? frpConfiguration.publicOrigin : "";
|
|
2632
|
+
if (frpServer.value === "" && configuredFrpServer !== "") frpServer.value = configuredFrpServer;
|
|
2633
|
+
if ((frpPort.value === "" || frpPort.value === "7000") && configuredFrpPort !== 7e3) frpPort.value = String(configuredFrpPort);
|
|
2634
|
+
if (frpOrigin.value === "" && configuredFrpOrigin !== "") frpOrigin.value = configuredFrpOrigin;
|
|
2635
|
+
frpStorage.textContent = frpStoragePath;
|
|
2636
|
+
frpStorage.title = frpStoragePath;
|
|
2637
|
+
frpInstall.hidden = frpInstalled || !frpSupported;
|
|
2638
|
+
frpInstall.textContent = frpDownloadBytes > 0 ? t("installWithSize", { size: formatMegabytes(frpDownloadBytes) }) : t("installFrpc");
|
|
2639
|
+
frpInstall.disabled = remoteProviderBusy;
|
|
2640
|
+
frpConfigure.disabled = remoteProviderBusy || !frpInstalled;
|
|
2641
|
+
frpPurge.hidden = !frpInstalled && !frpConfigured;
|
|
2642
|
+
frpComponentStatus.textContent = !frpSupported ? t("frpUnsupported") : frpInstalled ? t("frpComponentReady", { version: frpVersion }) : t("frpNotInstalled");
|
|
2643
|
+
frpConfigurationStatus.textContent = frpConfigured ? t("frpConfigurationReady") : t("frpConfigurationMissing");
|
|
2644
|
+
selfHostedBadge.textContent = frpConfigured && frpInstalled ? t("ready") : t("advanced");
|
|
1044
2645
|
const state = typeof data.state === "string" ? data.state : "error";
|
|
1045
2646
|
const errorCode = typeof data.errorCode === "string" ? data.errorCode : "";
|
|
1046
2647
|
const remoteOrigin = typeof data.origin === "string" ? data.origin : "";
|
|
1047
2648
|
remoteLoginUrl = typeof data.loginUrl === "string" ? data.loginUrl : "";
|
|
1048
|
-
const candidateSetupUrl =
|
|
2649
|
+
const candidateSetupUrl = tailscale ? officialFunnelSetupUrl(data.setupUrl) : "";
|
|
1049
2650
|
remoteSetupUrl = candidateSetupUrl !== "" ? candidateSetupUrl : {
|
|
1050
2651
|
funnel_permission_required: "https://tailscale.com/s/no-funnel",
|
|
1051
2652
|
funnel_https_required: "https://tailscale.com/s/https",
|
|
@@ -1053,48 +2654,66 @@ window.__ModuleLoader__.load({
|
|
|
1053
2654
|
}[errorCode] ?? "";
|
|
1054
2655
|
const needsFunnelSetup = state === "error" && remoteSetupUrl !== "";
|
|
1055
2656
|
remoteReady = remoteRunning && state === "ready" && remoteOrigin !== "";
|
|
2657
|
+
remoteStateBadge.classList.toggle("is-ready", remoteReady);
|
|
2658
|
+
remoteStateBadge.classList.toggle("is-busy", state === "starting" || state === "connecting" || state === "needs-login");
|
|
2659
|
+
remoteStateBadge.classList.toggle("is-attention", state === "error" || state === "unavailable");
|
|
2660
|
+
remoteStateBadge.textContent = remoteReady ? t("ready") : state === "starting" || state === "connecting" || state === "needs-login" ? t("remoteStateConnecting") : state === "error" || state === "unavailable" ? t("remoteStateAttention") : t("remoteStateOff");
|
|
1056
2661
|
remoteAccess.hidden = !remoteReady;
|
|
1057
2662
|
remoteAccessLink.href = remoteOrigin;
|
|
1058
2663
|
remoteAccessLink.textContent = remoteOrigin;
|
|
1059
2664
|
remoteAccessLink.title = remoteOrigin;
|
|
1060
2665
|
remoteStatus.classList.toggle("is-running", remoteReady);
|
|
1061
2666
|
const labels = {
|
|
1062
|
-
off: "
|
|
1063
|
-
unavailable: cpolar ? "
|
|
1064
|
-
starting: cpolar ? "
|
|
1065
|
-
"needs-login": "
|
|
1066
|
-
connecting: cpolar ? "
|
|
1067
|
-
ready: "
|
|
1068
|
-
error: "
|
|
2667
|
+
off: t("remoteOff"),
|
|
2668
|
+
unavailable: cpolar ? t("remoteUnavailableCpolar") : frp ? t("remoteUnavailableFrp") : t("remoteUnavailableTailscale"),
|
|
2669
|
+
starting: cpolar ? t("remoteStartingCpolar") : frp ? t("remoteStartingFrp") : t("remoteStartingTailscale"),
|
|
2670
|
+
"needs-login": t("remoteNeedsLogin"),
|
|
2671
|
+
connecting: cpolar ? t("remoteConnectingCpolar") : frp ? t("remoteConnectingFrp") : t("remoteConnectingTailscale"),
|
|
2672
|
+
ready: t("remoteReady"),
|
|
2673
|
+
error: t("remoteError")
|
|
1069
2674
|
};
|
|
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
|
-
|
|
2675
|
+
const errorLabels = {
|
|
2676
|
+
funnel_permission_required: t("funnelPermission"),
|
|
2677
|
+
funnel_https_required: t("funnelHttps"),
|
|
2678
|
+
funnel_start_failed: t("funnelStart"),
|
|
2679
|
+
funnel_start_timeout: t("funnelTimeout"),
|
|
2680
|
+
tailscale_dns_missing: t("tailscaleDnsMissing"),
|
|
2681
|
+
gateway_start_failed: t("gatewayStartFailed"),
|
|
2682
|
+
control_channel_failed: t("controlChannelFailed"),
|
|
2683
|
+
cpolar_component_missing: t("cpolarMissing"),
|
|
2684
|
+
cpolar_component_invalid: t("cpolarInvalid"),
|
|
2685
|
+
cpolar_config_missing: t("cpolarConfigMissing"),
|
|
2686
|
+
cpolar_config_invalid: t("cpolarConfigInvalid"),
|
|
2687
|
+
cpolar_port_unavailable: t("cpolarPortUnavailable"),
|
|
2688
|
+
cpolar_launch_failed: t("cpolarLaunchFailed"),
|
|
2689
|
+
cpolar_start_timeout: t("cpolarTimeout"),
|
|
2690
|
+
cpolar_stopped: t("cpolarStopped"),
|
|
2691
|
+
cpolar_exited: t("cpolarExited"),
|
|
2692
|
+
cpolar_invalid_output: t("cpolarOutputInvalid"),
|
|
2693
|
+
cpolar_invalid_origin: t("cpolarOriginInvalid"),
|
|
2694
|
+
frp_component_missing: t("frpMissing"),
|
|
2695
|
+
frp_component_invalid: t("frpInvalid"),
|
|
2696
|
+
frp_config_missing: t("frpConfigMissing"),
|
|
2697
|
+
frp_config_verify_failed: t("frpConfigVerifyFailed"),
|
|
2698
|
+
frp_vhost_publicly_reachable: t("frpVhostPublic"),
|
|
2699
|
+
frp_vhost_probe_failed: t("frpVhostProbeFailed"),
|
|
2700
|
+
frp_launch_failed: t("frpLaunchFailed"),
|
|
2701
|
+
frp_start_timeout: t("frpTimeout"),
|
|
2702
|
+
frp_discovery_mismatch: t("frpDiscoveryMismatch"),
|
|
2703
|
+
frp_discovery_invalid: t("frpDiscoveryInvalid"),
|
|
2704
|
+
frp_stopped: t("frpStopped"),
|
|
2705
|
+
frp_exited: t("frpExited")
|
|
2706
|
+
};
|
|
2707
|
+
remoteStatus.textContent = remoteSetupPending && needsFunnelSetup ? t("setupOpened") : state === "error" ? errorLabels[errorCode] ?? labels.error : labels[state] ?? labels.error;
|
|
1090
2708
|
remoteGuide.hidden = !needsFunnelSetup;
|
|
1091
2709
|
remoteSetup.disabled = remoteSetupUrl === "" || remoteReconnectBusy;
|
|
1092
2710
|
remoteSetupRetry.disabled = remoteReconnectBusy;
|
|
1093
|
-
remoteToggle.textContent = remoteRunning ? "
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
2711
|
+
remoteToggle.textContent = remoteRunning ? t("disableRemote") : t("enableRemote");
|
|
2712
|
+
const providerPrepared = cpolar ? cpolarInstalled && cpolarConfigured : frp ? frpInstalled && frpConfigured : true;
|
|
2713
|
+
remoteToggle.disabled = remoteProviderBusy || !providerPrepared;
|
|
2714
|
+
remoteLogin.hidden = !tailscale || state !== "needs-login" || remoteLoginUrl === "";
|
|
2715
|
+
remoteReconnect.hidden = needsFunnelSetup || state !== "error" && state !== "unavailable" || !providerPrepared;
|
|
2716
|
+
remoteActions.hidden = !providerPrepared;
|
|
1098
2717
|
remotePair.disabled = !remoteReady;
|
|
1099
2718
|
remoteDevices.disabled = !remoteReady;
|
|
1100
2719
|
if (!remoteReady) remoteQr.hidden = true;
|
|
@@ -1104,24 +2723,25 @@ window.__ModuleLoader__.load({
|
|
|
1104
2723
|
const loadRemote = () => {
|
|
1105
2724
|
if (remoteLoadInFlight) return;
|
|
1106
2725
|
remoteLoadInFlight = true;
|
|
1107
|
-
|
|
1108
|
-
remoteStatus.textContent = String(error);
|
|
2726
|
+
controlRequestJson("/api/mobile-access/remote/control").then(renderRemote, (error) => {
|
|
2727
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1109
2728
|
}).finally(() => {
|
|
1110
2729
|
remoteLoadInFlight = false;
|
|
1111
2730
|
});
|
|
1112
2731
|
};
|
|
1113
2732
|
const chooseRemoteProvider = (provider) => {
|
|
1114
2733
|
if (remoteProviderBusy || provider === remoteProvider) return;
|
|
1115
|
-
if (remoteRunning && !window.confirm("
|
|
2734
|
+
if (remoteRunning && !window.confirm(t("switchProviderConfirm"))) return;
|
|
1116
2735
|
remoteProviderBusy = true;
|
|
1117
2736
|
tailscaleChoice.disabled = true;
|
|
1118
2737
|
cpolarChoice.disabled = true;
|
|
1119
|
-
|
|
1120
|
-
|
|
2738
|
+
frpChoice.disabled = true;
|
|
2739
|
+
remoteStatus.textContent = provider === "cpolar" ? t("switchingCpolar") : provider === "frp" ? t("switchingFrp") : t("switchingTailscale");
|
|
2740
|
+
controlRequestJson("/api/mobile-access/remote/provider", {
|
|
1121
2741
|
method: "POST",
|
|
1122
2742
|
body: JSON.stringify({ provider })
|
|
1123
2743
|
}).then(renderRemote, (error) => {
|
|
1124
|
-
remoteStatus.textContent = String(error);
|
|
2744
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1125
2745
|
}).finally(() => {
|
|
1126
2746
|
remoteProviderBusy = false;
|
|
1127
2747
|
loadRemote();
|
|
@@ -1133,18 +2753,21 @@ window.__ModuleLoader__.load({
|
|
|
1133
2753
|
cpolarChoice.addEventListener("click", () => {
|
|
1134
2754
|
chooseRemoteProvider("cpolar");
|
|
1135
2755
|
});
|
|
2756
|
+
frpChoice.addEventListener("click", () => {
|
|
2757
|
+
chooseRemoteProvider("frp");
|
|
2758
|
+
});
|
|
1136
2759
|
cpolarInstall.addEventListener("click", () => {
|
|
1137
2760
|
if (remoteProviderBusy) return;
|
|
1138
|
-
if (!window.confirm("
|
|
2761
|
+
if (!window.confirm(t("installConfirm"))) return;
|
|
1139
2762
|
remoteProviderBusy = true;
|
|
1140
2763
|
cpolarInstall.disabled = true;
|
|
1141
|
-
cpolarInstall.textContent = "
|
|
1142
|
-
remoteStatus.textContent = "
|
|
1143
|
-
|
|
2764
|
+
cpolarInstall.textContent = t("downloading");
|
|
2765
|
+
remoteStatus.textContent = t("installingCpolar");
|
|
2766
|
+
controlRequestJson("/api/mobile-access/remote/cpolar/component/install", {
|
|
1144
2767
|
method: "POST",
|
|
1145
2768
|
body: JSON.stringify({ confirm: true })
|
|
1146
2769
|
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderRemote, (error) => {
|
|
1147
|
-
remoteStatus.textContent =
|
|
2770
|
+
remoteStatus.textContent = t("installFailed", { error: String(error) });
|
|
1148
2771
|
}).finally(() => {
|
|
1149
2772
|
remoteProviderBusy = false;
|
|
1150
2773
|
loadRemote();
|
|
@@ -1154,57 +2777,149 @@ window.__ModuleLoader__.load({
|
|
|
1154
2777
|
if (remoteProviderBusy) return;
|
|
1155
2778
|
const authtoken = cpolarToken.value.trim();
|
|
1156
2779
|
if (authtoken.length < 20 || /\s/u.test(authtoken)) {
|
|
1157
|
-
remoteStatus.textContent = "
|
|
2780
|
+
remoteStatus.textContent = t("invalidToken");
|
|
1158
2781
|
cpolarToken.focus();
|
|
1159
2782
|
return;
|
|
1160
2783
|
}
|
|
1161
2784
|
remoteProviderBusy = true;
|
|
1162
2785
|
cpolarConfigure.disabled = true;
|
|
1163
2786
|
cpolarConfigure.setAttribute("aria-busy", "true");
|
|
1164
|
-
cpolarConfigure.textContent = "
|
|
1165
|
-
|
|
2787
|
+
cpolarConfigure.textContent = t("saving");
|
|
2788
|
+
controlRequestJson("/api/mobile-access/remote/cpolar/configure", {
|
|
1166
2789
|
method: "POST",
|
|
1167
2790
|
body: JSON.stringify({ authtoken })
|
|
1168
2791
|
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(() => {
|
|
1169
2792
|
cpolarToken.value = "";
|
|
1170
|
-
remoteStatus.textContent = "
|
|
1171
|
-
return
|
|
2793
|
+
remoteStatus.textContent = t("configuredConnecting");
|
|
2794
|
+
return controlRequestJson("/api/mobile-access/remote/control", {
|
|
1172
2795
|
method: "POST",
|
|
1173
2796
|
body: JSON.stringify({ running: true })
|
|
1174
2797
|
});
|
|
1175
2798
|
}).then(renderRemote, (error) => {
|
|
1176
|
-
remoteStatus.textContent =
|
|
2799
|
+
remoteStatus.textContent = t("configureFailed", { error: String(error) });
|
|
1177
2800
|
}).finally(() => {
|
|
1178
2801
|
remoteProviderBusy = false;
|
|
1179
2802
|
cpolarConfigure.setAttribute("aria-busy", "false");
|
|
1180
|
-
cpolarConfigure.textContent = "
|
|
2803
|
+
cpolarConfigure.textContent = t("saveConnect");
|
|
1181
2804
|
loadRemote();
|
|
1182
2805
|
});
|
|
1183
2806
|
});
|
|
1184
2807
|
cpolarPurge.addEventListener("click", () => {
|
|
1185
2808
|
if (remoteProviderBusy) return;
|
|
1186
|
-
if (!window.confirm("
|
|
2809
|
+
if (!window.confirm(t("purgeConfirm"))) return;
|
|
1187
2810
|
remoteProviderBusy = true;
|
|
1188
2811
|
cpolarPurge.disabled = true;
|
|
1189
|
-
remoteStatus.textContent = "
|
|
1190
|
-
|
|
2812
|
+
remoteStatus.textContent = t("purging");
|
|
2813
|
+
controlRequestJson("/api/mobile-access/remote/cpolar/component/purge", {
|
|
1191
2814
|
method: "POST",
|
|
1192
2815
|
body: JSON.stringify({ confirm: true })
|
|
1193
2816
|
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderRemote, (error) => {
|
|
1194
|
-
remoteStatus.textContent =
|
|
2817
|
+
remoteStatus.textContent = t("purgeFailed", { error: String(error) });
|
|
1195
2818
|
}).finally(() => {
|
|
1196
2819
|
remoteProviderBusy = false;
|
|
1197
2820
|
cpolarPurge.disabled = false;
|
|
1198
2821
|
loadRemote();
|
|
1199
2822
|
});
|
|
1200
2823
|
});
|
|
2824
|
+
const validFrpServer = (value) => value === value.trim() && value.length > 0 && value.length <= 253 && !/[\s\u0000-\u001f\u007f/\\@?#]/u.test(value);
|
|
2825
|
+
const frpForm = () => ({
|
|
2826
|
+
serverAddress: frpServer.value.trim(),
|
|
2827
|
+
serverPort: Number(frpPort.value),
|
|
2828
|
+
token: frpToken.value,
|
|
2829
|
+
publicOrigin: frpOrigin.value.trim()
|
|
2830
|
+
});
|
|
2831
|
+
const validFrpForm = (form) => {
|
|
2832
|
+
if (!validFrpServer(form.serverAddress)) return false;
|
|
2833
|
+
try {
|
|
2834
|
+
createFrpServerTemplateForClipboard(form.serverPort, form.token, form.publicOrigin);
|
|
2835
|
+
return true;
|
|
2836
|
+
} catch {
|
|
2837
|
+
return false;
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
frpCopyTemplate.addEventListener("click", () => {
|
|
2841
|
+
const form = frpForm();
|
|
2842
|
+
if (!validFrpForm(form)) {
|
|
2843
|
+
remoteStatus.textContent = t("frpInputInvalid");
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
navigator.clipboard.writeText(createFrpServerTemplateForClipboard(form.serverPort, form.token, form.publicOrigin)).then(() => {
|
|
2847
|
+
remoteStatus.textContent = t("templateCopied");
|
|
2848
|
+
}, () => {
|
|
2849
|
+
remoteStatus.textContent = t("templateCopyFailed");
|
|
2850
|
+
});
|
|
2851
|
+
});
|
|
2852
|
+
frpInstall.addEventListener("click", () => {
|
|
2853
|
+
if (remoteProviderBusy) return;
|
|
2854
|
+
if (!window.confirm(t("frpInstallConfirm", { size: frpDownloadSize }))) return;
|
|
2855
|
+
remoteProviderBusy = true;
|
|
2856
|
+
frpInstall.disabled = true;
|
|
2857
|
+
frpInstall.textContent = t("downloading");
|
|
2858
|
+
remoteStatus.textContent = t("installingFrp");
|
|
2859
|
+
controlRequestJson("/api/mobile-access/remote/frp/component/install", {
|
|
2860
|
+
method: "POST",
|
|
2861
|
+
body: JSON.stringify({ confirm: true })
|
|
2862
|
+
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderRemote, (error) => {
|
|
2863
|
+
remoteStatus.textContent = t("installFailed", { error: String(error) });
|
|
2864
|
+
}).finally(() => {
|
|
2865
|
+
remoteProviderBusy = false;
|
|
2866
|
+
loadRemote();
|
|
2867
|
+
});
|
|
2868
|
+
});
|
|
2869
|
+
frpConfigure.addEventListener("click", () => {
|
|
2870
|
+
if (remoteProviderBusy || !frpInstalled) return;
|
|
2871
|
+
const form = frpForm();
|
|
2872
|
+
const unchanged = frpConfigured && form.token === "" && form.serverAddress === configuredFrpServer && form.serverPort === configuredFrpPort && form.publicOrigin === configuredFrpOrigin;
|
|
2873
|
+
if (!unchanged && !validFrpForm(form)) {
|
|
2874
|
+
remoteStatus.textContent = t("frpInputInvalid");
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
remoteProviderBusy = true;
|
|
2878
|
+
frpConfigure.disabled = true;
|
|
2879
|
+
frpConfigure.setAttribute("aria-busy", "true");
|
|
2880
|
+
frpConfigure.textContent = t("saving");
|
|
2881
|
+
(unchanged ? Promise.resolve({}) : controlRequestJson("/api/mobile-access/remote/frp/configure", {
|
|
2882
|
+
method: "POST",
|
|
2883
|
+
body: JSON.stringify(form)
|
|
2884
|
+
})).then(() => {
|
|
2885
|
+
frpToken.value = "";
|
|
2886
|
+
remoteStatus.textContent = t("frpSavingConnecting");
|
|
2887
|
+
return controlRequestJson("/api/mobile-access/remote/control", {
|
|
2888
|
+
method: "POST",
|
|
2889
|
+
body: JSON.stringify({ running: true })
|
|
2890
|
+
});
|
|
2891
|
+
}).then(renderRemote, (error) => {
|
|
2892
|
+
remoteStatus.textContent = t("configureFailed", { error: String(error) });
|
|
2893
|
+
}).finally(() => {
|
|
2894
|
+
remoteProviderBusy = false;
|
|
2895
|
+
frpConfigure.setAttribute("aria-busy", "false");
|
|
2896
|
+
frpConfigure.textContent = t("frpSaveConnect");
|
|
2897
|
+
loadRemote();
|
|
2898
|
+
});
|
|
2899
|
+
});
|
|
2900
|
+
frpPurge.addEventListener("click", () => {
|
|
2901
|
+
if (remoteProviderBusy || !window.confirm(t("purgeFrpConfirm"))) return;
|
|
2902
|
+
remoteProviderBusy = true;
|
|
2903
|
+
frpPurge.disabled = true;
|
|
2904
|
+
remoteStatus.textContent = t("purgingFrp");
|
|
2905
|
+
controlRequestJson("/api/mobile-access/remote/frp/component/purge", {
|
|
2906
|
+
method: "POST",
|
|
2907
|
+
body: JSON.stringify({ confirm: true })
|
|
2908
|
+
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderRemote, (error) => {
|
|
2909
|
+
remoteStatus.textContent = t("purgeFailed", { error: String(error) });
|
|
2910
|
+
}).finally(() => {
|
|
2911
|
+
remoteProviderBusy = false;
|
|
2912
|
+
frpPurge.disabled = false;
|
|
2913
|
+
loadRemote();
|
|
2914
|
+
});
|
|
2915
|
+
});
|
|
1201
2916
|
remoteToggle.addEventListener("click", () => {
|
|
1202
2917
|
remoteToggle.disabled = true;
|
|
1203
|
-
|
|
2918
|
+
controlRequestJson("/api/mobile-access/remote/control", {
|
|
1204
2919
|
method: "POST",
|
|
1205
2920
|
body: JSON.stringify({ running: !remoteRunning })
|
|
1206
2921
|
}).then(renderRemote, (error) => {
|
|
1207
|
-
remoteStatus.textContent = String(error);
|
|
2922
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1208
2923
|
}).finally(loadRemote);
|
|
1209
2924
|
});
|
|
1210
2925
|
remoteLogin.addEventListener("click", () => {
|
|
@@ -1216,12 +2931,12 @@ window.__ModuleLoader__.load({
|
|
|
1216
2931
|
remoteReconnect.disabled = true;
|
|
1217
2932
|
remoteSetup.disabled = true;
|
|
1218
2933
|
remoteSetupRetry.disabled = true;
|
|
1219
|
-
remoteStatus.textContent = remoteProvider === "cpolar" ? "
|
|
1220
|
-
|
|
2934
|
+
remoteStatus.textContent = remoteProvider === "cpolar" ? t("reconnectingCpolar") : remoteProvider === "frp" ? t("reconnectingFrp") : t("reconnectingTailscale");
|
|
2935
|
+
controlRequestJson("/api/mobile-access/remote/reconnect", {
|
|
1221
2936
|
method: "POST",
|
|
1222
2937
|
body: "{}"
|
|
1223
2938
|
}).then(renderRemote, (error) => {
|
|
1224
|
-
remoteStatus.textContent = String(error);
|
|
2939
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1225
2940
|
}).finally(() => {
|
|
1226
2941
|
remoteReconnectBusy = false;
|
|
1227
2942
|
remoteReconnect.disabled = false;
|
|
@@ -1238,7 +2953,7 @@ window.__ModuleLoader__.load({
|
|
|
1238
2953
|
if (remoteSetupUrl === "") return;
|
|
1239
2954
|
remoteSetupPending = true;
|
|
1240
2955
|
remoteSetupOpenedAt = Date.now();
|
|
1241
|
-
remoteStatus.textContent = "
|
|
2956
|
+
remoteStatus.textContent = t("setupOpened");
|
|
1242
2957
|
window.open(remoteSetupUrl, "_blank", "noopener,noreferrer");
|
|
1243
2958
|
});
|
|
1244
2959
|
const retryAfterSetup = () => {
|
|
@@ -1250,7 +2965,7 @@ window.__ModuleLoader__.load({
|
|
|
1250
2965
|
document.addEventListener("visibilitychange", retryAfterSetup);
|
|
1251
2966
|
remotePair.addEventListener("click", () => {
|
|
1252
2967
|
remotePair.disabled = true;
|
|
1253
|
-
|
|
2968
|
+
controlRequestJson("/api/mobile-access/remote/pairing/open", {
|
|
1254
2969
|
method: "POST",
|
|
1255
2970
|
body: "{}"
|
|
1256
2971
|
}).then(async (data) => {
|
|
@@ -1259,9 +2974,9 @@ window.__ModuleLoader__.load({
|
|
|
1259
2974
|
if (pairUrl !== "") try {
|
|
1260
2975
|
await navigator.clipboard.writeText(pairUrl);
|
|
1261
2976
|
} catch {}
|
|
1262
|
-
remoteStatus.textContent = "
|
|
2977
|
+
remoteStatus.textContent = t("remoteQrReady");
|
|
1263
2978
|
}, (error) => {
|
|
1264
|
-
remoteStatus.textContent = String(error);
|
|
2979
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1265
2980
|
}).finally(() => {
|
|
1266
2981
|
remotePair.disabled = !remoteReady;
|
|
1267
2982
|
});
|
|
@@ -1271,26 +2986,26 @@ window.__ModuleLoader__.load({
|
|
|
1271
2986
|
remoteDevicePanel.replaceChildren();
|
|
1272
2987
|
if (devices.length === 0) {
|
|
1273
2988
|
const empty = element("p", "dsh-mobile-control__device-empty");
|
|
1274
|
-
empty.textContent = "
|
|
2989
|
+
empty.textContent = t("noRemoteDevices");
|
|
1275
2990
|
remoteDevicePanel.append(empty);
|
|
1276
2991
|
return;
|
|
1277
2992
|
}
|
|
1278
2993
|
for (const device of devices) {
|
|
1279
2994
|
const row = element("div", "dsh-mobile-control__device");
|
|
1280
2995
|
const label = element("span", "dsh-mobile-control__device-label");
|
|
1281
|
-
label.textContent = typeof device.label === "string" ? device.label : "
|
|
2996
|
+
label.textContent = typeof device.label === "string" ? device.label : t("device");
|
|
1282
2997
|
const meta = element("span", "dsh-mobile-control__device-meta");
|
|
1283
|
-
meta.textContent =
|
|
2998
|
+
meta.textContent = t("expires", { time: formatTime(device.expiresAt) });
|
|
1284
2999
|
const revoke = element("button", "dsh-mobile-control__device-revoke");
|
|
1285
3000
|
revoke.type = "button";
|
|
1286
|
-
revoke.textContent = "
|
|
3001
|
+
revoke.textContent = t("revoke");
|
|
1287
3002
|
const id = typeof device.id === "string" ? device.id : "";
|
|
1288
3003
|
revoke.addEventListener("click", () => {
|
|
1289
|
-
|
|
3004
|
+
controlRequestJson("/api/mobile-access/remote/devices/revoke", {
|
|
1290
3005
|
method: "POST",
|
|
1291
3006
|
body: JSON.stringify({ deviceId: id })
|
|
1292
3007
|
}).then(loadRemoteDevices, (error) => {
|
|
1293
|
-
remoteStatus.textContent = String(error);
|
|
3008
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1294
3009
|
});
|
|
1295
3010
|
});
|
|
1296
3011
|
row.append(label, meta, revoke);
|
|
@@ -1298,8 +3013,8 @@ window.__ModuleLoader__.load({
|
|
|
1298
3013
|
}
|
|
1299
3014
|
};
|
|
1300
3015
|
const loadRemoteDevices = () => {
|
|
1301
|
-
|
|
1302
|
-
remoteStatus.textContent = String(error);
|
|
3016
|
+
controlRequestJson("/api/mobile-access/remote/devices").then(renderRemoteDevices, (error) => {
|
|
3017
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1303
3018
|
});
|
|
1304
3019
|
};
|
|
1305
3020
|
remoteDevices.addEventListener("click", () => {
|
|
@@ -1308,29 +3023,101 @@ window.__ModuleLoader__.load({
|
|
|
1308
3023
|
if (show) loadRemoteDevices();
|
|
1309
3024
|
});
|
|
1310
3025
|
remoteReset.addEventListener("click", () => {
|
|
1311
|
-
const prompt = remoteProvider === "cpolar" ? "
|
|
3026
|
+
const prompt = remoteProvider === "cpolar" ? t("resetCpolarConfirm") : remoteProvider === "frp" ? t("resetFrpConfirm") : t("resetTailscaleConfirm");
|
|
1312
3027
|
if (!window.confirm(prompt)) return;
|
|
1313
|
-
|
|
3028
|
+
controlRequestJson("/api/mobile-access/remote/reset", {
|
|
1314
3029
|
method: "POST",
|
|
1315
3030
|
body: JSON.stringify({ confirm: true })
|
|
1316
3031
|
}).then(renderRemote, (error) => {
|
|
1317
|
-
remoteStatus.textContent = String(error);
|
|
3032
|
+
remoteStatus.textContent = t("requestFailed", { error: String(error) });
|
|
1318
3033
|
});
|
|
1319
3034
|
});
|
|
1320
3035
|
const renderDiagnostics = (data) => {
|
|
1321
|
-
const
|
|
3036
|
+
const entries = [...diagnosticEntriesForRender(data)];
|
|
3037
|
+
const overall = diagnosticOverallForChecks(data.overall, entries.map((entry) => entry.status));
|
|
1322
3038
|
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 : [];
|
|
3039
|
+
diagnosticsSummaryTitle.textContent = overall === "ok" ? t("diagnosticsComplete") : overall === "attention" ? t("diagnosticsAttention") : t("diagnosticsProblem");
|
|
3040
|
+
diagnosticsSummaryText.textContent = locale === "zh" && data.overall === overall && typeof data.summary === "string" ? data.summary : t("diagnosticsCompleteFallback");
|
|
1326
3041
|
diagnosticsChecks.replaceChildren();
|
|
1327
3042
|
const statusLabels = {
|
|
1328
|
-
ok: "
|
|
1329
|
-
warning: "
|
|
1330
|
-
error: "
|
|
1331
|
-
info: "
|
|
3043
|
+
ok: t("diagnosticStatusOk"),
|
|
3044
|
+
warning: t("diagnosticStatusWarning"),
|
|
3045
|
+
error: t("diagnosticStatusError"),
|
|
3046
|
+
info: t("diagnosticStatusInfo")
|
|
3047
|
+
};
|
|
3048
|
+
const diagnosticLabelKeys = {
|
|
3049
|
+
versions: "diagnosticLabelVersions",
|
|
3050
|
+
network: "diagnosticLabelNetwork",
|
|
3051
|
+
lan: "diagnosticLabelLan",
|
|
3052
|
+
firewall: "diagnosticLabelFirewall",
|
|
3053
|
+
remote: "diagnosticLabelRemote",
|
|
3054
|
+
"phone-network": "diagnosticLabelPhone"
|
|
3055
|
+
};
|
|
3056
|
+
const statusOf = (entry) => normalizeDiagnosticStatus(entry.status);
|
|
3057
|
+
const localizedEntryCopy = (entry) => {
|
|
3058
|
+
const serverCopy = diagnosticServerCopy(entry);
|
|
3059
|
+
const reason = typeof entry.reason === "string" ? entry.reason : "";
|
|
3060
|
+
const templates = DIAGNOSTIC_REASON_MESSAGES[locale][reason];
|
|
3061
|
+
if (templates === void 0) return serverCopy;
|
|
3062
|
+
const facts = entry.facts !== null && typeof entry.facts === "object" ? entry.facts : {};
|
|
3063
|
+
const values = {
|
|
3064
|
+
provider: facts.provider === "tailscale" || facts.provider === "cpolar" || facts.provider === "frp" ? facts.provider : "",
|
|
3065
|
+
latencyMs: typeof facts.latencyMs === "number" && Number.isFinite(facts.latencyMs) ? new Intl.NumberFormat(localeTag).format(facts.latencyMs) : "",
|
|
3066
|
+
interfaceName: typeof facts.interfaceName === "string" ? facts.interfaceName : "",
|
|
3067
|
+
endpointSuffix: typeof facts.endpointSuffix === "string" ? facts.endpointSuffix : "",
|
|
3068
|
+
controllerCode: typeof facts.controllerCode === "string" ? facts.controllerCode : ""
|
|
3069
|
+
};
|
|
3070
|
+
const interpolate = (template) => template.replace(/\{(\w+)\}/gu, (_match, key) => values[key] ?? "");
|
|
3071
|
+
let detail = interpolate(templates[0]);
|
|
3072
|
+
let action = interpolate(templates[1]);
|
|
3073
|
+
if (reason === "versions-current") {
|
|
3074
|
+
const versions = data.versions !== null && typeof data.versions === "object" ? data.versions : {};
|
|
3075
|
+
if ([
|
|
3076
|
+
versions.plugin,
|
|
3077
|
+
versions.dsh,
|
|
3078
|
+
versions.minimumAndroidApp
|
|
3079
|
+
].every((value) => typeof value === "string")) detail += ` plugin ${String(versions.plugin)}, DSH ${String(versions.dsh)}, Android ${String(versions.minimumAndroidApp)}.`;
|
|
3080
|
+
}
|
|
3081
|
+
if (reason === "remote-controller-error") {
|
|
3082
|
+
const actionKey = {
|
|
3083
|
+
component_missing: "remoteUnavailableTailscale",
|
|
3084
|
+
funnel_permission_required: "funnelPermission",
|
|
3085
|
+
funnel_https_required: "funnelHttps",
|
|
3086
|
+
funnel_start_failed: "funnelStart",
|
|
3087
|
+
funnel_start_timeout: "funnelTimeout",
|
|
3088
|
+
tailscale_dns_missing: "tailscaleDnsMissing",
|
|
3089
|
+
sidecar_launch_failed: "remoteUnavailableTailscale",
|
|
3090
|
+
sidecar_stopped: "controlChannelFailed",
|
|
3091
|
+
sidecar_exited: "controlChannelFailed",
|
|
3092
|
+
control_channel_failed: "controlChannelFailed",
|
|
3093
|
+
cpolar_component_missing: "cpolarMissing",
|
|
3094
|
+
cpolar_component_invalid: "cpolarInvalid",
|
|
3095
|
+
cpolar_config_missing: "cpolarConfigMissing",
|
|
3096
|
+
cpolar_config_invalid: "cpolarConfigInvalid",
|
|
3097
|
+
cpolar_start_timeout: "cpolarTimeout",
|
|
3098
|
+
cpolar_stopped: "cpolarStopped",
|
|
3099
|
+
cpolar_exited: "cpolarExited",
|
|
3100
|
+
frp_component_missing: "frpMissing",
|
|
3101
|
+
frp_component_invalid: "frpInvalid",
|
|
3102
|
+
frp_config_missing: "frpConfigMissing",
|
|
3103
|
+
frp_config_verify_failed: "frpConfigVerifyFailed",
|
|
3104
|
+
frp_vhost_publicly_reachable: "frpVhostPublic",
|
|
3105
|
+
frp_vhost_probe_failed: "frpVhostProbeFailed",
|
|
3106
|
+
frp_launch_failed: "frpLaunchFailed",
|
|
3107
|
+
frp_start_timeout: "frpTimeout",
|
|
3108
|
+
frp_discovery_mismatch: "frpDiscoveryMismatch",
|
|
3109
|
+
frp_discovery_invalid: "frpDiscoveryInvalid",
|
|
3110
|
+
frp_stopped: "frpStopped",
|
|
3111
|
+
frp_exited: "frpExited",
|
|
3112
|
+
gateway_start_failed: "gatewayStartFailed"
|
|
3113
|
+
}[values.controllerCode ?? ""];
|
|
3114
|
+
if (actionKey !== void 0) action = t(actionKey);
|
|
3115
|
+
}
|
|
3116
|
+
return {
|
|
3117
|
+
detail,
|
|
3118
|
+
action
|
|
3119
|
+
};
|
|
1332
3120
|
};
|
|
1333
|
-
const statusOf = (entry) => entry.status === "ok" || entry.status === "warning" || entry.status === "error" ? entry.status : "info";
|
|
1334
3121
|
const appendGroup = (label, groupEntries) => {
|
|
1335
3122
|
if (groupEntries.length === 0) return;
|
|
1336
3123
|
const group = element("section", "dsh-mobile-control__diagnostic-group");
|
|
@@ -1338,7 +3125,7 @@ window.__ModuleLoader__.load({
|
|
|
1338
3125
|
const groupTitle = element("h3");
|
|
1339
3126
|
groupTitle.textContent = label;
|
|
1340
3127
|
const groupCount = element("span");
|
|
1341
|
-
groupCount.textContent =
|
|
3128
|
+
groupCount.textContent = t("diagnosticItems", { count: groupEntries.length });
|
|
1342
3129
|
const list = element("div", "dsh-mobile-control__diagnostic-list");
|
|
1343
3130
|
list.setAttribute("role", "list");
|
|
1344
3131
|
groupHeader.append(groupTitle, groupCount);
|
|
@@ -1350,19 +3137,21 @@ window.__ModuleLoader__.load({
|
|
|
1350
3137
|
marker.setAttribute("aria-hidden", "true");
|
|
1351
3138
|
const rowBody = element("div", "dsh-mobile-control__diagnostic-check-body");
|
|
1352
3139
|
const rowHeader = element("div", "dsh-mobile-control__diagnostic-check-header");
|
|
3140
|
+
const labelKey = typeof entry.id === "string" ? diagnosticLabelKeys[entry.id] : void 0;
|
|
1353
3141
|
const rowTitle = element("strong");
|
|
1354
|
-
rowTitle.textContent = typeof entry.label === "string" ? entry.label : "
|
|
3142
|
+
rowTitle.textContent = locale !== "zh" && labelKey !== void 0 ? t(labelKey) : typeof entry.label === "string" ? entry.label : t("diagnosticCheck");
|
|
1355
3143
|
const badge = element("span", "dsh-mobile-control__diagnostic-badge");
|
|
1356
3144
|
badge.textContent = statusLabels[state];
|
|
3145
|
+
const localizedCopy = localizedEntryCopy(entry);
|
|
1357
3146
|
const detail = element("p");
|
|
1358
|
-
detail.textContent =
|
|
3147
|
+
detail.textContent = localizedCopy.detail;
|
|
1359
3148
|
rowHeader.append(rowTitle, badge);
|
|
1360
3149
|
rowBody.append(rowHeader, detail);
|
|
1361
|
-
if (
|
|
3150
|
+
if (localizedCopy.action !== "") {
|
|
1362
3151
|
const action = element("p", "dsh-mobile-control__diagnostic-action");
|
|
1363
3152
|
const actionLabel = element("span");
|
|
1364
|
-
actionLabel.textContent = "
|
|
1365
|
-
action.append(actionLabel, document.createTextNode(
|
|
3153
|
+
actionLabel.textContent = t("diagnosticAction");
|
|
3154
|
+
action.append(actionLabel, document.createTextNode(localizedCopy.action));
|
|
1366
3155
|
rowBody.append(action);
|
|
1367
3156
|
}
|
|
1368
3157
|
row.append(marker, rowBody);
|
|
@@ -1373,40 +3162,67 @@ window.__ModuleLoader__.load({
|
|
|
1373
3162
|
};
|
|
1374
3163
|
const issues = entries.filter((entry) => statusOf(entry) === "error" || statusOf(entry) === "warning");
|
|
1375
3164
|
const otherChecks = entries.filter((entry) => statusOf(entry) !== "error" && statusOf(entry) !== "warning");
|
|
1376
|
-
appendGroup("
|
|
1377
|
-
appendGroup(issues.length === 0 ? "
|
|
1378
|
-
diagnosticsSummaryMeta.textContent = issues.length === 0 ?
|
|
3165
|
+
appendGroup(t("diagnosticNeedsAction"), issues);
|
|
3166
|
+
appendGroup(issues.length === 0 ? t("diagnosticDetails") : t("diagnosticOther"), otherChecks);
|
|
3167
|
+
diagnosticsSummaryMeta.textContent = issues.length === 0 ? t("diagnosticNoBlockers", { count: entries.length }) : t("diagnosticNeedsCount", {
|
|
3168
|
+
count: entries.length,
|
|
3169
|
+
issues: issues.length
|
|
3170
|
+
});
|
|
1379
3171
|
diagnosticsChecks.hidden = entries.length === 0;
|
|
1380
|
-
|
|
3172
|
+
const reportCopy = LOCALIZED_DIAGNOSTIC_COPY[locale];
|
|
3173
|
+
const generatedAt = typeof data.generatedAt === "number" ? new Date(data.generatedAt).toLocaleString(localeTag) : (/* @__PURE__ */ new Date()).toLocaleString(localeTag);
|
|
3174
|
+
const lines = entries.map((entry) => {
|
|
3175
|
+
const state = statusOf(entry);
|
|
3176
|
+
const labelKey = typeof entry.id === "string" ? diagnosticLabelKeys[entry.id] : void 0;
|
|
3177
|
+
const label = labelKey === void 0 && typeof entry.label === "string" ? entry.label : labelKey === void 0 ? t("diagnosticCheck") : t(labelKey);
|
|
3178
|
+
const localizedCopy = localizedEntryCopy(entry);
|
|
3179
|
+
return `[${statusLabels[state]}] ${label}: ${localizedCopy.detail}${localizedCopy.action === "" ? "" : ` ${localizedCopy.action}`}`;
|
|
3180
|
+
});
|
|
3181
|
+
copiedDiagnosticReport = [
|
|
3182
|
+
reportCopy.reportTitle,
|
|
3183
|
+
`${reportCopy.generated}: ${generatedAt}`,
|
|
3184
|
+
...lines
|
|
3185
|
+
].join("\n");
|
|
1381
3186
|
diagnosticsReport.textContent = copiedDiagnosticReport;
|
|
1382
3187
|
diagnosticsDetails.hidden = copiedDiagnosticReport === "";
|
|
1383
3188
|
diagnosticsCopy.disabled = copiedDiagnosticReport === "";
|
|
1384
3189
|
diagnosticsCopy.hidden = copiedDiagnosticReport === "";
|
|
1385
3190
|
diagnosticsToolbar.classList.toggle("has-report", copiedDiagnosticReport !== "");
|
|
1386
3191
|
};
|
|
3192
|
+
const showDiagnosticsFailure = (error) => {
|
|
3193
|
+
diagnosticsSummary.className = "dsh-mobile-control__diagnostic-summary is-error";
|
|
3194
|
+
diagnosticsSummaryTitle.textContent = t("diagnosticsIncomplete");
|
|
3195
|
+
diagnosticsSummaryText.textContent = t("diagnosticsReadFailed", { error: String(error) });
|
|
3196
|
+
diagnosticsSummaryMeta.textContent = t("diagnosticsUnavailable");
|
|
3197
|
+
diagnosticsChecks.replaceChildren();
|
|
3198
|
+
diagnosticsChecks.hidden = true;
|
|
3199
|
+
copiedDiagnosticReport = "";
|
|
3200
|
+
diagnosticsReport.textContent = "";
|
|
3201
|
+
diagnosticsDetails.hidden = true;
|
|
3202
|
+
diagnosticsCopy.disabled = true;
|
|
3203
|
+
diagnosticsCopy.hidden = true;
|
|
3204
|
+
diagnosticsToolbar.classList.remove("has-report");
|
|
3205
|
+
};
|
|
1387
3206
|
const loadDiagnostics = () => {
|
|
1388
3207
|
if (diagnosticsBusy) return;
|
|
1389
3208
|
diagnosticsBusy = true;
|
|
1390
3209
|
diagnosticsRun.disabled = true;
|
|
1391
3210
|
diagnosticsRun.setAttribute("aria-busy", "true");
|
|
1392
|
-
diagnosticsRun.textContent = "
|
|
3211
|
+
diagnosticsRun.textContent = t("diagnosticsChecking");
|
|
1393
3212
|
diagnosticsSummary.className = "dsh-mobile-control__diagnostic-summary is-running";
|
|
1394
|
-
diagnosticsSummaryTitle.textContent = "
|
|
1395
|
-
diagnosticsSummaryText.textContent = "
|
|
1396
|
-
diagnosticsSummaryMeta.textContent = "
|
|
3213
|
+
diagnosticsSummaryTitle.textContent = t("diagnosticsCheckingTitle");
|
|
3214
|
+
diagnosticsSummaryText.textContent = t("diagnosticsCheckingText");
|
|
3215
|
+
diagnosticsSummaryMeta.textContent = t("diagnosticsRunningMeta");
|
|
1397
3216
|
diagnosticsFeedback.hidden = true;
|
|
1398
3217
|
diagnosticsChecks.classList.add("is-refreshing");
|
|
1399
3218
|
diagnosticsChecks.setAttribute("aria-busy", "true");
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
diagnosticsSummaryText.textContent = `无法读取诊断结果:${String(error)}`;
|
|
1404
|
-
diagnosticsSummaryMeta.textContent = "诊断服务暂不可用 · 请稍后重试";
|
|
1405
|
-
}).finally(() => {
|
|
3219
|
+
controlRequestJson("/api/mobile-access/diagnostics").then((data) => {
|
|
3220
|
+
renderDiagnosticPayloadSafely(data, renderDiagnostics, showDiagnosticsFailure);
|
|
3221
|
+
}, showDiagnosticsFailure).finally(() => {
|
|
1406
3222
|
diagnosticsBusy = false;
|
|
1407
3223
|
diagnosticsRun.disabled = false;
|
|
1408
3224
|
diagnosticsRun.setAttribute("aria-busy", "false");
|
|
1409
|
-
diagnosticsRun.textContent = "
|
|
3225
|
+
diagnosticsRun.textContent = t("diagnosticsRetry");
|
|
1410
3226
|
diagnosticsChecks.classList.remove("is-refreshing");
|
|
1411
3227
|
diagnosticsChecks.setAttribute("aria-busy", "false");
|
|
1412
3228
|
});
|
|
@@ -1420,14 +3236,38 @@ window.__ModuleLoader__.load({
|
|
|
1420
3236
|
loadDiagnostics();
|
|
1421
3237
|
});
|
|
1422
3238
|
diagnosticsRun.addEventListener("click", loadDiagnostics);
|
|
3239
|
+
updatePlugin.addEventListener("click", () => {
|
|
3240
|
+
if (!pluginUpdateAvailable || pluginLatestVersion === "") return;
|
|
3241
|
+
updatePlugin.disabled = true;
|
|
3242
|
+
updatePlugin.textContent = t("updatingPlugin");
|
|
3243
|
+
releaseNotice.hidden = true;
|
|
3244
|
+
releaseNotice.classList.remove("is-error");
|
|
3245
|
+
controlRequestJson("/api/mobile-access/release/update", {
|
|
3246
|
+
method: "POST",
|
|
3247
|
+
body: "{}"
|
|
3248
|
+
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then((data) => {
|
|
3249
|
+
const installedVersion = releaseVersion(data.installedVersion) ?? pluginLatestVersion;
|
|
3250
|
+
pluginUpdateAvailable = false;
|
|
3251
|
+
updatePlugin.hidden = true;
|
|
3252
|
+
releaseNotice.textContent = t("pluginUpdatedRestart", { version: installedVersion });
|
|
3253
|
+
releaseNotice.hidden = false;
|
|
3254
|
+
}, (error) => {
|
|
3255
|
+
updatePlugin.textContent = t("updatePlugin");
|
|
3256
|
+
releaseNotice.textContent = t("pluginUpdateFailed", { error: String(error) });
|
|
3257
|
+
releaseNotice.classList.add("is-error");
|
|
3258
|
+
releaseNotice.hidden = false;
|
|
3259
|
+
}).finally(() => {
|
|
3260
|
+
updatePlugin.disabled = false;
|
|
3261
|
+
});
|
|
3262
|
+
});
|
|
1423
3263
|
diagnosticsCopy.addEventListener("click", () => {
|
|
1424
3264
|
if (copiedDiagnosticReport === "") return;
|
|
1425
3265
|
navigator.clipboard.writeText(copiedDiagnosticReport).then(() => {
|
|
1426
|
-
diagnosticsFeedback.textContent = "
|
|
3266
|
+
diagnosticsFeedback.textContent = t("diagnosticsCopied");
|
|
1427
3267
|
diagnosticsFeedback.hidden = false;
|
|
1428
3268
|
}, () => {
|
|
1429
3269
|
diagnosticsDetails.open = true;
|
|
1430
|
-
diagnosticsFeedback.textContent = "
|
|
3270
|
+
diagnosticsFeedback.textContent = t("diagnosticsCopyManual");
|
|
1431
3271
|
diagnosticsFeedback.hidden = false;
|
|
1432
3272
|
});
|
|
1433
3273
|
});
|
|
@@ -1452,8 +3292,9 @@ window.__ModuleLoader__.load({
|
|
|
1452
3292
|
if (!panel.contains(event.target) && !document.querySelector(".dsh-mobile-control__trigger")?.contains(event.target)) setOpen(false);
|
|
1453
3293
|
};
|
|
1454
3294
|
document.addEventListener("pointerdown", dismiss);
|
|
1455
|
-
|
|
1456
|
-
|
|
3295
|
+
controlRequestJson("/api/mobile-access/release").then(renderRelease, () => {});
|
|
3296
|
+
controlRequestJson("/api/mobile-access/lan/control").then(render, (error) => {
|
|
3297
|
+
status.textContent = t("requestFailed", { error: String(error) });
|
|
1457
3298
|
});
|
|
1458
3299
|
loadRemote();
|
|
1459
3300
|
const remotePoll = window.setInterval(() => {
|
|
@@ -1461,6 +3302,7 @@ window.__ModuleLoader__.load({
|
|
|
1461
3302
|
}, 1500);
|
|
1462
3303
|
return {
|
|
1463
3304
|
remove: () => {
|
|
3305
|
+
lifecycle.abort();
|
|
1464
3306
|
window.clearInterval(remotePoll);
|
|
1465
3307
|
window.removeEventListener("focus", retryAfterSetup);
|
|
1466
3308
|
document.removeEventListener("visibilitychange", retryAfterSetup);
|
|
@@ -1489,24 +3331,512 @@ window.__ModuleLoader__.load({
|
|
|
1489
3331
|
redirect: "error"
|
|
1490
3332
|
});
|
|
1491
3333
|
}
|
|
3334
|
+
/** Combine extension and caller abort lifetimes and expose deterministic listener cleanup. */
|
|
3335
|
+
function combineClientSignalLifetime(first, second) {
|
|
3336
|
+
if (first.aborted || second.aborted) {
|
|
3337
|
+
const aborted = new AbortController();
|
|
3338
|
+
aborted.abort(first.aborted ? first.reason : second.reason);
|
|
3339
|
+
return {
|
|
3340
|
+
signal: aborted.signal,
|
|
3341
|
+
cleanup: () => void 0
|
|
3342
|
+
};
|
|
3343
|
+
}
|
|
3344
|
+
const controller = new AbortController();
|
|
3345
|
+
const cleanup = () => {
|
|
3346
|
+
first.removeEventListener("abort", abortFirst);
|
|
3347
|
+
second.removeEventListener("abort", abortSecond);
|
|
3348
|
+
};
|
|
3349
|
+
const abortFirst = () => {
|
|
3350
|
+
cleanup();
|
|
3351
|
+
controller.abort(first.reason);
|
|
3352
|
+
};
|
|
3353
|
+
const abortSecond = () => {
|
|
3354
|
+
cleanup();
|
|
3355
|
+
controller.abort(second.reason);
|
|
3356
|
+
};
|
|
3357
|
+
first.addEventListener("abort", abortFirst, { once: true });
|
|
3358
|
+
second.addEventListener("abort", abortSecond, { once: true });
|
|
3359
|
+
return {
|
|
3360
|
+
signal: controller.signal,
|
|
3361
|
+
cleanup
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
/** Combine extension and caller abort lifetimes on WebViews without AbortSignal.any. */
|
|
3365
|
+
function combineClientSignals(first, second) {
|
|
3366
|
+
return combineClientSignalLifetime(first, second).signal;
|
|
3367
|
+
}
|
|
3368
|
+
/** Keep a combined request lifetime until a streamed response is consumed or cancelled. */
|
|
3369
|
+
function bindClientResponseLifetime(response, cleanup) {
|
|
3370
|
+
if (response.body === null) {
|
|
3371
|
+
cleanup();
|
|
3372
|
+
return response;
|
|
3373
|
+
}
|
|
3374
|
+
const reader = response.body.getReader();
|
|
3375
|
+
let released = false;
|
|
3376
|
+
const release = () => {
|
|
3377
|
+
if (released) return;
|
|
3378
|
+
released = true;
|
|
3379
|
+
cleanup();
|
|
3380
|
+
};
|
|
3381
|
+
const body = new ReadableStream({
|
|
3382
|
+
async pull(controller) {
|
|
3383
|
+
try {
|
|
3384
|
+
const result = await reader.read();
|
|
3385
|
+
if (result.done) {
|
|
3386
|
+
release();
|
|
3387
|
+
controller.close();
|
|
3388
|
+
} else controller.enqueue(result.value);
|
|
3389
|
+
} catch (error) {
|
|
3390
|
+
release();
|
|
3391
|
+
controller.error(error);
|
|
3392
|
+
}
|
|
3393
|
+
},
|
|
3394
|
+
async cancel(reason) {
|
|
3395
|
+
try {
|
|
3396
|
+
await reader.cancel(reason);
|
|
3397
|
+
} finally {
|
|
3398
|
+
release();
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
});
|
|
3402
|
+
try {
|
|
3403
|
+
const retained = new Response(body, {
|
|
3404
|
+
headers: response.headers,
|
|
3405
|
+
status: response.status,
|
|
3406
|
+
statusText: response.statusText
|
|
3407
|
+
});
|
|
3408
|
+
Object.defineProperties(retained, {
|
|
3409
|
+
redirected: {
|
|
3410
|
+
configurable: true,
|
|
3411
|
+
value: response.redirected
|
|
3412
|
+
},
|
|
3413
|
+
type: {
|
|
3414
|
+
configurable: true,
|
|
3415
|
+
value: response.type
|
|
3416
|
+
},
|
|
3417
|
+
url: {
|
|
3418
|
+
configurable: true,
|
|
3419
|
+
value: response.url
|
|
3420
|
+
}
|
|
3421
|
+
});
|
|
3422
|
+
return retained;
|
|
3423
|
+
} catch (error) {
|
|
3424
|
+
reader.cancel(error);
|
|
3425
|
+
release();
|
|
3426
|
+
throw error;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
/** Dispose an old UI when its replacement Host generation cannot be activated. */
|
|
3430
|
+
function failClosedExtensionGenerationReplacement(hasActive, activeGeneration, replacementGeneration, dispose) {
|
|
3431
|
+
if (!hasActive || activeGeneration === replacementGeneration) return false;
|
|
3432
|
+
dispose();
|
|
3433
|
+
return true;
|
|
3434
|
+
}
|
|
3435
|
+
/** Resolve one SDK route and prove its normalized path remains in the current extension namespace. */
|
|
3436
|
+
function extensionRouteUrl(id, path, baseUrl) {
|
|
3437
|
+
const rawPathname = path.split(/[?#]/u, 1)[0] ?? "";
|
|
3438
|
+
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");
|
|
3439
|
+
const origin = new URL(baseUrl).origin;
|
|
3440
|
+
const prefix = `/mobile-access/extensions/${id}/routes`;
|
|
3441
|
+
let target;
|
|
3442
|
+
try {
|
|
3443
|
+
target = new URL(`${prefix}${path}`, origin);
|
|
3444
|
+
} catch {
|
|
3445
|
+
throw new TypeError("extension routes must be relative");
|
|
3446
|
+
}
|
|
3447
|
+
let decodedPathname;
|
|
3448
|
+
try {
|
|
3449
|
+
decodedPathname = decodeURIComponent(target.pathname);
|
|
3450
|
+
} catch {
|
|
3451
|
+
throw new TypeError("extension routes must be relative");
|
|
3452
|
+
}
|
|
3453
|
+
if (target.origin !== origin || target.hash !== "" || decodedPathname.includes("\\") || decodedPathname !== prefix && !decodedPathname.startsWith(`${prefix}/`)) throw new TypeError("extension routes must be relative");
|
|
3454
|
+
if (decodedPathname.slice(prefix.length).split("/").some((part) => part === "." || part === "..")) throw new TypeError("extension routes must be relative");
|
|
3455
|
+
return target;
|
|
3456
|
+
}
|
|
3457
|
+
/** Resolve one generation-pinned static asset URL in the current extension namespace. */
|
|
3458
|
+
function extensionAssetUrl(id, generation, path, baseUrl) {
|
|
3459
|
+
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");
|
|
3460
|
+
const normalized = path.replaceAll("\\", "/");
|
|
3461
|
+
if (normalized.length === 0 || normalized.startsWith("/") || normalized.split("/").some((part) => part === "" || part === "." || part === "..")) throw new TypeError("extension asset path is invalid");
|
|
3462
|
+
const target = new URL(`/mobile-access/extensions/${id}/assets/${normalized.split("/").map(encodeURIComponent).join("/")}`, new URL(baseUrl).origin);
|
|
3463
|
+
if (generation !== void 0) target.searchParams.set("generation", generation);
|
|
3464
|
+
return target;
|
|
3465
|
+
}
|
|
3466
|
+
/** Add the immutable Host generation selected for one activated mobile UI. */
|
|
3467
|
+
function extensionGenerationHeaders(generation, headers) {
|
|
3468
|
+
const result = new Headers(headers);
|
|
3469
|
+
if (generation !== void 0) result.set("x-dsh-mobile-extension-generation", generation);
|
|
3470
|
+
return result;
|
|
3471
|
+
}
|
|
3472
|
+
function registerUniqueDisposable(entries, claimedIds, id, mount) {
|
|
3473
|
+
if (claimedIds.has(id)) throw new Error(`duplicate lifecycle id: ${id}`);
|
|
3474
|
+
claimedIds.add(id);
|
|
3475
|
+
let mounted;
|
|
3476
|
+
try {
|
|
3477
|
+
mounted = mount();
|
|
3478
|
+
} catch (error) {
|
|
3479
|
+
claimedIds.delete(id);
|
|
3480
|
+
throw error;
|
|
3481
|
+
}
|
|
3482
|
+
let disposed = false;
|
|
3483
|
+
const dispose = () => {
|
|
3484
|
+
if (disposed) return;
|
|
3485
|
+
disposed = true;
|
|
3486
|
+
mounted.dispose();
|
|
3487
|
+
};
|
|
3488
|
+
const entry = {
|
|
3489
|
+
...mounted,
|
|
3490
|
+
dispose
|
|
3491
|
+
};
|
|
3492
|
+
entries.set(id, entry);
|
|
3493
|
+
return () => {
|
|
3494
|
+
if (entries.get(id) === entry) {
|
|
3495
|
+
entries.delete(id);
|
|
3496
|
+
claimedIds.delete(id);
|
|
3497
|
+
}
|
|
3498
|
+
dispose();
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
/** Dispose resources omitted by a successfully fetched authoritative manifest. */
|
|
3502
|
+
function reconcileRemovedExtensions(currentIds, seen, dispose) {
|
|
3503
|
+
for (const id of new Set(currentIds)) if (!seen.has(id)) dispose(id);
|
|
3504
|
+
}
|
|
3505
|
+
/** Reconcile managed resources against one validated authoritative id set. */
|
|
3506
|
+
function publishAuthoritativeExtensionIds(authoritativeIds, seen, managedIdSources, dispose) {
|
|
3507
|
+
const current = new Set(authoritativeIds);
|
|
3508
|
+
for (const source of managedIdSources) for (const id of source) current.add(id);
|
|
3509
|
+
reconcileRemovedExtensions(current, seen, dispose);
|
|
3510
|
+
authoritativeIds.clear();
|
|
3511
|
+
for (const id of seen) authoritativeIds.add(id);
|
|
3512
|
+
}
|
|
3513
|
+
function validManifestResourceUrl(value) {
|
|
3514
|
+
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") && !value.includes("://") && !value.split("/").some((part) => part === ".." || part === ".");
|
|
3515
|
+
}
|
|
3516
|
+
/** Validate the manifest before treating it as authoritative state. */
|
|
3517
|
+
function parseMobileExtensionManifest(payload) {
|
|
3518
|
+
if (typeof payload !== "object" || payload === null) return void 0;
|
|
3519
|
+
const candidate = payload;
|
|
3520
|
+
if (candidate.protocol !== 1 || !Array.isArray(candidate.extensions) || typeof candidate.legacy !== "object" || candidate.legacy === null) return void 0;
|
|
3521
|
+
const legacy = candidate.legacy;
|
|
3522
|
+
if (typeof legacy.scriptRevision !== "string" || typeof legacy.styleRevision !== "string") return void 0;
|
|
3523
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3524
|
+
const extensions = [];
|
|
3525
|
+
for (const value of candidate.extensions) {
|
|
3526
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
3527
|
+
const entry = value;
|
|
3528
|
+
if (typeof entry.id !== "string" || !/^[a-z][a-z0-9-]{0,63}$/u.test(entry.id) || ids.has(entry.id)) return void 0;
|
|
3529
|
+
if (entry.generation !== void 0 && (typeof entry.generation !== "string" || !/^[a-f\d]{64}$/u.test(entry.generation))) return void 0;
|
|
3530
|
+
if (entry.scriptUrl !== void 0 && !validManifestResourceUrl(entry.scriptUrl)) return void 0;
|
|
3531
|
+
if (entry.styleUrl !== void 0 && !validManifestResourceUrl(entry.styleUrl)) return void 0;
|
|
3532
|
+
if (entry.assetsUrl !== void 0 && !validManifestResourceUrl(entry.assetsUrl)) return void 0;
|
|
3533
|
+
ids.add(entry.id);
|
|
3534
|
+
extensions.push({
|
|
3535
|
+
id: entry.id,
|
|
3536
|
+
...entry.generation === void 0 ? {} : { generation: entry.generation },
|
|
3537
|
+
...entry.scriptUrl === void 0 ? {} : { scriptUrl: entry.scriptUrl },
|
|
3538
|
+
...entry.styleUrl === void 0 ? {} : { styleUrl: entry.styleUrl },
|
|
3539
|
+
...entry.assetsUrl === void 0 ? {} : { assetsUrl: entry.assetsUrl }
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
return {
|
|
3543
|
+
extensions,
|
|
3544
|
+
legacy: {
|
|
3545
|
+
scriptRevision: legacy.scriptRevision,
|
|
3546
|
+
styleRevision: legacy.styleRevision
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
/** A missing manifest is authoritative before optional legacy resources finish refreshing. */
|
|
3551
|
+
async function handleMissingExtensionManifest(clearManifestResources, refreshLegacyResources, signal) {
|
|
3552
|
+
clearManifestResources();
|
|
3553
|
+
const results = await refreshLegacyResources();
|
|
3554
|
+
return !signal.aborted && results.every(Boolean);
|
|
3555
|
+
}
|
|
3556
|
+
function refreshAborted(signal) {
|
|
3557
|
+
return signal.aborted;
|
|
3558
|
+
}
|
|
3559
|
+
/** Run one coalesced refresh cycle, slowing down when the page is hidden. */
|
|
3560
|
+
function startLifecycleRefreshScheduler(refresh, options = {}, runtime = {
|
|
3561
|
+
document,
|
|
3562
|
+
window
|
|
3563
|
+
}) {
|
|
3564
|
+
const visibleIntervalMs = options.visibleIntervalMs ?? 45e3;
|
|
3565
|
+
const hiddenIntervalMs = options.hiddenIntervalMs ?? 3e5;
|
|
3566
|
+
const cycleTimeoutMs = options.cycleTimeoutMs ?? 3e4;
|
|
3567
|
+
let timer;
|
|
3568
|
+
let cycleTimer;
|
|
3569
|
+
let running = false;
|
|
3570
|
+
let queued = false;
|
|
3571
|
+
let disposed = false;
|
|
3572
|
+
let controller;
|
|
3573
|
+
const clearTimer = () => {
|
|
3574
|
+
if (timer === void 0) return;
|
|
3575
|
+
runtime.window.clearTimeout(timer);
|
|
3576
|
+
timer = void 0;
|
|
3577
|
+
};
|
|
3578
|
+
const clearCycleTimer = () => {
|
|
3579
|
+
if (cycleTimer === void 0) return;
|
|
3580
|
+
runtime.window.clearTimeout(cycleTimer);
|
|
3581
|
+
cycleTimer = void 0;
|
|
3582
|
+
};
|
|
3583
|
+
const schedule = () => {
|
|
3584
|
+
if (disposed) return;
|
|
3585
|
+
clearTimer();
|
|
3586
|
+
const delay = runtime.document.visibilityState === "hidden" ? hiddenIntervalMs : visibleIntervalMs;
|
|
3587
|
+
timer = runtime.window.setTimeout(run, delay);
|
|
3588
|
+
};
|
|
3589
|
+
const run = () => {
|
|
3590
|
+
if (disposed) return;
|
|
3591
|
+
clearTimer();
|
|
3592
|
+
if (running) {
|
|
3593
|
+
queued = true;
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3596
|
+
running = true;
|
|
3597
|
+
const current = new AbortController();
|
|
3598
|
+
controller = current;
|
|
3599
|
+
const refreshPromise = Promise.resolve().then(() => refresh(current.signal));
|
|
3600
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
3601
|
+
cycleTimer = runtime.window.setTimeout(() => {
|
|
3602
|
+
cycleTimer = void 0;
|
|
3603
|
+
current.abort(new DOMException("mobile extension refresh timed out", "TimeoutError"));
|
|
3604
|
+
resolve();
|
|
3605
|
+
}, cycleTimeoutMs);
|
|
3606
|
+
});
|
|
3607
|
+
Promise.race([refreshPromise, timeoutPromise]).catch(() => {}).finally(() => {
|
|
3608
|
+
clearCycleTimer();
|
|
3609
|
+
if (controller === current) controller = void 0;
|
|
3610
|
+
running = false;
|
|
3611
|
+
if (disposed) return;
|
|
3612
|
+
if (queued) {
|
|
3613
|
+
queued = false;
|
|
3614
|
+
run();
|
|
3615
|
+
} else schedule();
|
|
3616
|
+
});
|
|
3617
|
+
refreshPromise.catch(() => {});
|
|
3618
|
+
};
|
|
3619
|
+
const onVisibilityChange = () => {
|
|
3620
|
+
if (runtime.document.visibilityState === "hidden") schedule();
|
|
3621
|
+
else run();
|
|
3622
|
+
};
|
|
3623
|
+
runtime.document.addEventListener("visibilitychange", onVisibilityChange);
|
|
3624
|
+
runtime.window.addEventListener("focus", run);
|
|
3625
|
+
runtime.window.addEventListener("online", run);
|
|
3626
|
+
run();
|
|
3627
|
+
const stop = () => {
|
|
3628
|
+
disposed = true;
|
|
3629
|
+
queued = false;
|
|
3630
|
+
controller?.abort();
|
|
3631
|
+
controller = void 0;
|
|
3632
|
+
clearCycleTimer();
|
|
3633
|
+
clearTimer();
|
|
3634
|
+
runtime.document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
3635
|
+
runtime.window.removeEventListener("focus", run);
|
|
3636
|
+
runtime.window.removeEventListener("online", run);
|
|
3637
|
+
};
|
|
3638
|
+
return Object.assign(stop, { refresh: run });
|
|
3639
|
+
}
|
|
3640
|
+
/** Maintain one authenticated same-origin extension event stream with bounded reconnect backoff. */
|
|
3641
|
+
function startExtensionChangeStream(changed, runtime = {
|
|
3642
|
+
window,
|
|
3643
|
+
create: (url) => new EventSource(url, { withCredentials: true })
|
|
3644
|
+
}) {
|
|
3645
|
+
let source;
|
|
3646
|
+
let timer;
|
|
3647
|
+
let disposed = false;
|
|
3648
|
+
let retryMs = 1e3;
|
|
3649
|
+
const clearTimer = () => {
|
|
3650
|
+
if (timer === void 0) return;
|
|
3651
|
+
runtime.window.clearTimeout(timer);
|
|
3652
|
+
timer = void 0;
|
|
3653
|
+
};
|
|
3654
|
+
const connect = () => {
|
|
3655
|
+
if (disposed || source !== void 0) return;
|
|
3656
|
+
clearTimer();
|
|
3657
|
+
const next = runtime.create("/mobile-access/extensions/events");
|
|
3658
|
+
source = next;
|
|
3659
|
+
next.onopen = () => {
|
|
3660
|
+
retryMs = 1e3;
|
|
3661
|
+
};
|
|
3662
|
+
next.addEventListener("extensions-changed", changed);
|
|
3663
|
+
next.onerror = () => {
|
|
3664
|
+
if (source !== next) return;
|
|
3665
|
+
next.close();
|
|
3666
|
+
source = void 0;
|
|
3667
|
+
if (disposed) return;
|
|
3668
|
+
const delay = retryMs;
|
|
3669
|
+
retryMs = Math.min(3e4, retryMs * 2);
|
|
3670
|
+
timer = runtime.window.setTimeout(connect, delay);
|
|
3671
|
+
};
|
|
3672
|
+
};
|
|
3673
|
+
const reconnectNow = () => {
|
|
3674
|
+
if (disposed) return;
|
|
3675
|
+
source?.close();
|
|
3676
|
+
source = void 0;
|
|
3677
|
+
retryMs = 1e3;
|
|
3678
|
+
connect();
|
|
3679
|
+
};
|
|
3680
|
+
runtime.window.addEventListener("online", reconnectNow);
|
|
3681
|
+
connect();
|
|
3682
|
+
return () => {
|
|
3683
|
+
disposed = true;
|
|
3684
|
+
clearTimer();
|
|
3685
|
+
source?.close();
|
|
3686
|
+
source = void 0;
|
|
3687
|
+
runtime.window.removeEventListener("online", reconnectNow);
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
/** Keep at most one activation in flight per id and commit only its latest generation. */
|
|
3691
|
+
var PerIdActivationLifecycle = class {
|
|
3692
|
+
active = /* @__PURE__ */ new Map();
|
|
3693
|
+
pending = /* @__PURE__ */ new Map();
|
|
3694
|
+
generations = /* @__PURE__ */ new Map();
|
|
3695
|
+
disposed = false;
|
|
3696
|
+
hasActive(id) {
|
|
3697
|
+
return this.active.has(id);
|
|
3698
|
+
}
|
|
3699
|
+
getActive(id) {
|
|
3700
|
+
return this.active.get(id)?.value;
|
|
3701
|
+
}
|
|
3702
|
+
pendingCount() {
|
|
3703
|
+
return this.pending.size;
|
|
3704
|
+
}
|
|
3705
|
+
async activate(id, key, cycleSignal, create) {
|
|
3706
|
+
if (this.disposed || cycleSignal?.aborted === true) return false;
|
|
3707
|
+
const existing = this.pending.get(id);
|
|
3708
|
+
if (existing !== void 0) {
|
|
3709
|
+
if (existing.key === key && !existing.controller.signal.aborted) return this.waitFor(existing, cycleSignal);
|
|
3710
|
+
existing.cancel();
|
|
3711
|
+
}
|
|
3712
|
+
const generation = (this.generations.get(id) ?? 0) + 1;
|
|
3713
|
+
this.generations.set(id, generation);
|
|
3714
|
+
const controller = new AbortController();
|
|
3715
|
+
let work;
|
|
3716
|
+
try {
|
|
3717
|
+
work = create(controller, generation);
|
|
3718
|
+
} catch {
|
|
3719
|
+
controller.abort(new DOMException("mobile extension activation failed to start", "AbortError"));
|
|
3720
|
+
return false;
|
|
3721
|
+
}
|
|
3722
|
+
let cancelled = false;
|
|
3723
|
+
let pending = {};
|
|
3724
|
+
const cancel = () => {
|
|
3725
|
+
if (cancelled) return;
|
|
3726
|
+
cancelled = true;
|
|
3727
|
+
if (this.pending.get(id) === pending) this.pending.delete(id);
|
|
3728
|
+
if (this.generations.get(id) === generation) this.generations.set(id, generation + 1);
|
|
3729
|
+
controller.abort(new DOMException("mobile extension activation cancelled", "AbortError"));
|
|
3730
|
+
work.cancel();
|
|
3731
|
+
};
|
|
3732
|
+
const completion = Promise.resolve(work.result).then((value) => {
|
|
3733
|
+
const current = this.pending.get(id);
|
|
3734
|
+
if (!(!this.disposed && current === pending && current.generation === this.generations.get(id) && !controller.signal.aborted)) {
|
|
3735
|
+
try {
|
|
3736
|
+
work.dispose(value);
|
|
3737
|
+
} catch {}
|
|
3738
|
+
return false;
|
|
3739
|
+
}
|
|
3740
|
+
try {
|
|
3741
|
+
work.commit?.(value);
|
|
3742
|
+
} catch {
|
|
3743
|
+
try {
|
|
3744
|
+
work.dispose(value);
|
|
3745
|
+
} catch {}
|
|
3746
|
+
return false;
|
|
3747
|
+
}
|
|
3748
|
+
const previous = this.active.get(id);
|
|
3749
|
+
let valueDisposed = false;
|
|
3750
|
+
const dispose = () => {
|
|
3751
|
+
if (valueDisposed) return;
|
|
3752
|
+
valueDisposed = true;
|
|
3753
|
+
controller.abort(new DOMException("mobile extension deactivated", "AbortError"));
|
|
3754
|
+
try {
|
|
3755
|
+
work.dispose(value);
|
|
3756
|
+
} catch {}
|
|
3757
|
+
};
|
|
3758
|
+
this.active.set(id, {
|
|
3759
|
+
value,
|
|
3760
|
+
dispose
|
|
3761
|
+
});
|
|
3762
|
+
previous?.dispose();
|
|
3763
|
+
return true;
|
|
3764
|
+
}, () => {
|
|
3765
|
+
cancel();
|
|
3766
|
+
return false;
|
|
3767
|
+
}).finally(() => {
|
|
3768
|
+
if (this.pending.get(id) === pending) this.pending.delete(id);
|
|
3769
|
+
});
|
|
3770
|
+
Object.assign(pending, {
|
|
3771
|
+
key,
|
|
3772
|
+
generation,
|
|
3773
|
+
controller,
|
|
3774
|
+
cancel,
|
|
3775
|
+
completion
|
|
3776
|
+
});
|
|
3777
|
+
this.pending.set(id, pending);
|
|
3778
|
+
return this.waitFor(pending, cycleSignal);
|
|
3779
|
+
}
|
|
3780
|
+
remove(id) {
|
|
3781
|
+
this.generations.set(id, (this.generations.get(id) ?? 0) + 1);
|
|
3782
|
+
this.active.get(id)?.dispose();
|
|
3783
|
+
this.active.delete(id);
|
|
3784
|
+
this.pending.get(id)?.cancel();
|
|
3785
|
+
}
|
|
3786
|
+
dispose() {
|
|
3787
|
+
if (this.disposed) return;
|
|
3788
|
+
this.disposed = true;
|
|
3789
|
+
for (const current of this.active.values()) current.dispose();
|
|
3790
|
+
this.active.clear();
|
|
3791
|
+
for (const current of this.pending.values()) current.cancel();
|
|
3792
|
+
}
|
|
3793
|
+
waitFor(pending, signal) {
|
|
3794
|
+
if (signal === void 0) return pending.completion;
|
|
3795
|
+
if (signal.aborted) {
|
|
3796
|
+
pending.cancel();
|
|
3797
|
+
return Promise.resolve(false);
|
|
3798
|
+
}
|
|
3799
|
+
return new Promise((resolve) => {
|
|
3800
|
+
let settled = false;
|
|
3801
|
+
const finish = (value) => {
|
|
3802
|
+
if (settled) return;
|
|
3803
|
+
settled = true;
|
|
3804
|
+
signal.removeEventListener("abort", onAbort);
|
|
3805
|
+
resolve(value);
|
|
3806
|
+
};
|
|
3807
|
+
const onAbort = () => {
|
|
3808
|
+
pending.cancel();
|
|
3809
|
+
finish(false);
|
|
3810
|
+
};
|
|
3811
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3812
|
+
pending.completion.then(finish);
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
};
|
|
1492
3816
|
function installCustomAssets() {
|
|
1493
3817
|
const legacyStyle = element("style");
|
|
1494
3818
|
legacyStyle.dataset.plugin = "dsh-mobile-custom";
|
|
1495
3819
|
document.head.append(legacyStyle);
|
|
3820
|
+
const legacyCssState = {
|
|
3821
|
+
etag: "",
|
|
3822
|
+
modified: ""
|
|
3823
|
+
};
|
|
1496
3824
|
const previous = window.dshMobile;
|
|
1497
3825
|
let legacyMount = queuedLegacyMount;
|
|
1498
3826
|
let legacySource = "";
|
|
1499
3827
|
let legacyRoot;
|
|
1500
3828
|
let legacyDispose;
|
|
1501
3829
|
const definitions = /* @__PURE__ */ new Map();
|
|
1502
|
-
const
|
|
3830
|
+
const activations = new PerIdActivationLifecycle();
|
|
1503
3831
|
const styleNodes = /* @__PURE__ */ new Map();
|
|
1504
3832
|
const styleEtags = /* @__PURE__ */ new Map();
|
|
1505
3833
|
const scriptDigests = /* @__PURE__ */ new Map();
|
|
3834
|
+
const activeHostGenerations = /* @__PURE__ */ new Map();
|
|
3835
|
+
const activationKeys = /* @__PURE__ */ new Map();
|
|
1506
3836
|
const manifestExtensionIds = /* @__PURE__ */ new Set();
|
|
1507
3837
|
const managedDefinitionIds = /* @__PURE__ */ new Set();
|
|
1508
|
-
const activationQueues = /* @__PURE__ */ new Map();
|
|
1509
3838
|
let manifestEtag = "";
|
|
3839
|
+
let disposed = false;
|
|
1510
3840
|
let expectedDefinitionId;
|
|
1511
3841
|
const SURFACE_HOST_STYLES = {
|
|
1512
3842
|
"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 +3883,34 @@ window.__ModuleLoader__.load({
|
|
|
1553
3883
|
return value;
|
|
1554
3884
|
}
|
|
1555
3885
|
};
|
|
1556
|
-
const invokeNative = async (action, input) => {
|
|
3886
|
+
const invokeNative = async (action, input, signal) => {
|
|
3887
|
+
const abortReason = () => signal.reason ?? new DOMException("mobile extension disposed", "AbortError");
|
|
3888
|
+
if (signal.aborted) throw abortReason();
|
|
1557
3889
|
const bridge = window.__DSH_MOBILE_NATIVE__;
|
|
1558
|
-
if (bridge !== void 0) return
|
|
3890
|
+
if (bridge !== void 0) return new Promise((resolve, reject) => {
|
|
3891
|
+
let settled = false;
|
|
3892
|
+
const finish = (callback) => {
|
|
3893
|
+
if (settled) return;
|
|
3894
|
+
settled = true;
|
|
3895
|
+
signal.removeEventListener("abort", onAbort);
|
|
3896
|
+
callback();
|
|
3897
|
+
};
|
|
3898
|
+
const onAbort = () => {
|
|
3899
|
+
finish(() => {
|
|
3900
|
+
reject(abortReason());
|
|
3901
|
+
});
|
|
3902
|
+
};
|
|
3903
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3904
|
+
Promise.resolve().then(() => bridge.invoke(action, input)).then((value) => {
|
|
3905
|
+
finish(() => {
|
|
3906
|
+
resolve(materializeNativeFile(value));
|
|
3907
|
+
});
|
|
3908
|
+
}, (error) => {
|
|
3909
|
+
finish(() => {
|
|
3910
|
+
reject(error);
|
|
3911
|
+
});
|
|
3912
|
+
});
|
|
3913
|
+
});
|
|
1559
3914
|
if (action === "share" && typeof navigator.share === "function") {
|
|
1560
3915
|
await navigator.share(input ?? {});
|
|
1561
3916
|
return { ok: true };
|
|
@@ -1570,65 +3925,156 @@ window.__ModuleLoader__.load({
|
|
|
1570
3925
|
inputElement.type = "file";
|
|
1571
3926
|
inputElement.accept = action === "camera.capture" ? "image/*" : "*/*";
|
|
1572
3927
|
if (action === "camera.capture") inputElement.capture = "environment";
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
3928
|
+
inputElement.hidden = true;
|
|
3929
|
+
return new Promise((resolve, reject) => {
|
|
3930
|
+
let settled = false;
|
|
3931
|
+
let cleaned = false;
|
|
3932
|
+
let cleanupTimer = 0;
|
|
3933
|
+
let watchdogTimer = 0;
|
|
3934
|
+
const cleanup = () => {
|
|
3935
|
+
if (cleaned) return;
|
|
3936
|
+
cleaned = true;
|
|
3937
|
+
if (cleanupTimer !== 0) window.clearTimeout(cleanupTimer);
|
|
3938
|
+
if (watchdogTimer !== 0) window.clearTimeout(watchdogTimer);
|
|
3939
|
+
window.removeEventListener("focus", scheduleCancel);
|
|
3940
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
3941
|
+
signal.removeEventListener("abort", onAbort);
|
|
3942
|
+
inputElement.removeEventListener("change", onChange);
|
|
3943
|
+
inputElement.removeEventListener("cancel", onCancel);
|
|
3944
|
+
inputElement.remove();
|
|
3945
|
+
};
|
|
3946
|
+
const finish = (callback) => {
|
|
3947
|
+
if (settled) return;
|
|
3948
|
+
settled = true;
|
|
3949
|
+
cleanup();
|
|
3950
|
+
callback();
|
|
3951
|
+
};
|
|
3952
|
+
const onChange = () => {
|
|
3953
|
+
const file = inputElement.files?.[0];
|
|
3954
|
+
finish(() => {
|
|
3955
|
+
resolve(file);
|
|
3956
|
+
});
|
|
3957
|
+
};
|
|
3958
|
+
const onCancel = () => {
|
|
3959
|
+
finish(() => {
|
|
3960
|
+
resolve(void 0);
|
|
3961
|
+
});
|
|
3962
|
+
};
|
|
3963
|
+
const onAbort = () => {
|
|
3964
|
+
finish(() => {
|
|
3965
|
+
reject(abortReason());
|
|
3966
|
+
});
|
|
3967
|
+
};
|
|
3968
|
+
const scheduleCancel = () => {
|
|
3969
|
+
if (!settled && cleanupTimer === 0) cleanupTimer = window.setTimeout(onCancel, 1e3);
|
|
3970
|
+
};
|
|
3971
|
+
const onVisibilityChange = () => {
|
|
3972
|
+
if (document.visibilityState === "visible") scheduleCancel();
|
|
3973
|
+
};
|
|
3974
|
+
inputElement.addEventListener("change", onChange, { once: true });
|
|
3975
|
+
inputElement.addEventListener("cancel", onCancel, { once: true });
|
|
3976
|
+
window.addEventListener("focus", scheduleCancel);
|
|
3977
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
3978
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3979
|
+
watchdogTimer = window.setTimeout(onCancel, 3e5);
|
|
3980
|
+
try {
|
|
3981
|
+
document.body.append(inputElement);
|
|
3982
|
+
inputElement.click();
|
|
3983
|
+
} catch (error) {
|
|
3984
|
+
finish(() => {
|
|
3985
|
+
reject(error);
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
1576
3988
|
});
|
|
1577
3989
|
}
|
|
1578
3990
|
throw new Error("native capability is unavailable");
|
|
1579
3991
|
};
|
|
1580
|
-
const makeApi = (id, controller) => {
|
|
1581
|
-
const
|
|
3992
|
+
const makeApi = (id, hostGeneration, controller, surfaces, surfaceIds) => {
|
|
3993
|
+
const ensureCurrent = () => {
|
|
3994
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
3995
|
+
};
|
|
3996
|
+
const requestSignal = (signal) => signal === void 0 || signal === null ? {
|
|
3997
|
+
signal: controller.signal,
|
|
3998
|
+
cleanup: () => void 0
|
|
3999
|
+
} : combineClientSignalLifetime(controller.signal, signal);
|
|
1582
4000
|
const mountSurface = (surface) => {
|
|
4001
|
+
ensureCurrent();
|
|
1583
4002
|
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
|
-
|
|
4003
|
+
return registerUniqueDisposable(surfaces, surfaceIds, surface.id, () => {
|
|
4004
|
+
const container = element("section");
|
|
4005
|
+
container.dataset.dshMobileSurface = surface.id;
|
|
4006
|
+
container.hidden = surface.placement === "page" || surface.placement === "overlay";
|
|
4007
|
+
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";
|
|
4008
|
+
const host = () => surface.placement === "page" || surface.placement === "overlay" ? shellLayer() : surfaceHost(surface.placement) ?? shellLayer();
|
|
4009
|
+
const mounted = surface.mount(container);
|
|
4010
|
+
const dispose = () => {
|
|
4011
|
+
try {
|
|
4012
|
+
if (typeof mounted === "function") mounted();
|
|
4013
|
+
} finally {
|
|
4014
|
+
container.remove();
|
|
4015
|
+
}
|
|
4016
|
+
};
|
|
4017
|
+
return {
|
|
4018
|
+
dispose,
|
|
4019
|
+
container,
|
|
4020
|
+
host
|
|
4021
|
+
};
|
|
1597
4022
|
});
|
|
1598
|
-
return () => {
|
|
1599
|
-
if (surfaces.get(surface.id)?.dispose === dispose) surfaces.delete(surface.id);
|
|
1600
|
-
dispose();
|
|
1601
|
-
};
|
|
1602
4023
|
};
|
|
1603
4024
|
return {
|
|
1604
4025
|
host: {
|
|
1605
|
-
invoke: (action, input) =>
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
4026
|
+
invoke: (action, input) => {
|
|
4027
|
+
ensureCurrent();
|
|
4028
|
+
return mobileRequest(`/mobile-access/extensions/${encodeURIComponent(id)}/actions/${encodeURIComponent(action)}`, {
|
|
4029
|
+
method: "POST",
|
|
4030
|
+
headers: extensionGenerationHeaders(hostGeneration),
|
|
4031
|
+
body: JSON.stringify(input ?? {}),
|
|
4032
|
+
signal: controller.signal
|
|
4033
|
+
}).then(async (response) => {
|
|
4034
|
+
const value = await response.json();
|
|
4035
|
+
if (!response.ok) throw new Error(typeof value === "object" && value !== null && "error" in value ? String(value.error) : `HTTP ${String(response.status)}`);
|
|
4036
|
+
return value;
|
|
4037
|
+
});
|
|
4038
|
+
},
|
|
1613
4039
|
fetch: (path, init) => {
|
|
1614
|
-
|
|
1615
|
-
|
|
4040
|
+
ensureCurrent();
|
|
4041
|
+
const target = extensionRouteUrl(id, path, location.href);
|
|
4042
|
+
const headers = extensionGenerationHeaders(hostGeneration, init?.headers);
|
|
4043
|
+
const lifetime = requestSignal(init?.signal);
|
|
4044
|
+
return mobileRequest(target.href, {
|
|
4045
|
+
...init,
|
|
4046
|
+
headers,
|
|
4047
|
+
signal: lifetime.signal
|
|
4048
|
+
}).then((response) => bindClientResponseLifetime(response, lifetime.cleanup), (error) => {
|
|
4049
|
+
lifetime.cleanup();
|
|
4050
|
+
throw error;
|
|
4051
|
+
});
|
|
4052
|
+
},
|
|
4053
|
+
assetUrl: (path) => {
|
|
4054
|
+
ensureCurrent();
|
|
4055
|
+
return extensionAssetUrl(id, hostGeneration, path, location.href).href;
|
|
1616
4056
|
}
|
|
1617
4057
|
},
|
|
1618
4058
|
ui: {
|
|
1619
4059
|
registerSurface: mountSurface,
|
|
1620
|
-
open: (
|
|
1621
|
-
|
|
4060
|
+
open: (surfaceId) => {
|
|
4061
|
+
ensureCurrent();
|
|
4062
|
+
const entry = surfaces.get(surfaceId);
|
|
1622
4063
|
if (entry !== void 0) entry.container.hidden = false;
|
|
1623
4064
|
},
|
|
1624
|
-
close: (
|
|
1625
|
-
|
|
4065
|
+
close: (surfaceId) => {
|
|
4066
|
+
ensureCurrent();
|
|
4067
|
+
const entry = surfaces.get(surfaceId);
|
|
1626
4068
|
if (entry !== void 0) entry.container.hidden = true;
|
|
1627
4069
|
},
|
|
1628
|
-
toast
|
|
4070
|
+
toast: (message) => {
|
|
4071
|
+
ensureCurrent();
|
|
4072
|
+
toast(message);
|
|
4073
|
+
}
|
|
1629
4074
|
},
|
|
1630
4075
|
native: {
|
|
1631
4076
|
capabilities: async () => {
|
|
4077
|
+
ensureCurrent();
|
|
1632
4078
|
const bridge = window.__DSH_MOBILE_NATIVE__;
|
|
1633
4079
|
return bridge === void 0 ? [
|
|
1634
4080
|
"files.pick",
|
|
@@ -1638,63 +4084,59 @@ window.__ModuleLoader__.load({
|
|
|
1638
4084
|
"clipboard.write"
|
|
1639
4085
|
] : bridge.capabilities();
|
|
1640
4086
|
},
|
|
1641
|
-
invoke: invokeNative
|
|
4087
|
+
invoke: (action, input) => invokeNative(action, input, controller.signal)
|
|
1642
4088
|
},
|
|
1643
4089
|
signal: controller.signal,
|
|
1644
4090
|
document,
|
|
1645
4091
|
window
|
|
1646
4092
|
};
|
|
1647
4093
|
};
|
|
1648
|
-
const
|
|
1649
|
-
const
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
4094
|
+
const activateDefinition = (definition, cycleSignal, commitGeneration, hostGeneration) => {
|
|
4095
|
+
const previousKey = activationKeys.get(definition.id);
|
|
4096
|
+
const key = previousKey?.definition === definition && previousKey.generation === hostGeneration ? previousKey : {
|
|
4097
|
+
definition,
|
|
4098
|
+
...hostGeneration === void 0 ? {} : { generation: hostGeneration }
|
|
4099
|
+
};
|
|
4100
|
+
activationKeys.set(definition.id, key);
|
|
4101
|
+
return activations.activate(definition.id, key, cycleSignal, (controller) => {
|
|
4102
|
+
const surfaces = /* @__PURE__ */ new Map();
|
|
4103
|
+
const surfaceIds = /* @__PURE__ */ new Set();
|
|
4104
|
+
let pendingDisposed = false;
|
|
4105
|
+
const disposePending = () => {
|
|
4106
|
+
if (pendingDisposed) return;
|
|
4107
|
+
pendingDisposed = true;
|
|
4108
|
+
for (const surface of surfaces.values()) try {
|
|
4109
|
+
surface.dispose();
|
|
4110
|
+
} catch {}
|
|
1658
4111
|
surfaces.clear();
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
4112
|
+
surfaceIds.clear();
|
|
4113
|
+
};
|
|
4114
|
+
return {
|
|
4115
|
+
result: Promise.resolve().then(() => definition.activate(makeApi(definition.id, hostGeneration, controller, surfaces, surfaceIds))).then((cleanup) => ({
|
|
4116
|
+
controller,
|
|
4117
|
+
surfaces,
|
|
4118
|
+
...typeof cleanup === "function" ? { cleanup } : {}
|
|
4119
|
+
})),
|
|
4120
|
+
cancel: disposePending,
|
|
4121
|
+
commit: (value) => {
|
|
4122
|
+
if (definitions.get(definition.id) !== definition || controller.signal.aborted) throw new Error("stale mobile extension activation");
|
|
4123
|
+
for (const surface of value.surfaces.values()) surface.host().append(surface.container);
|
|
4124
|
+
activeHostGenerations.set(definition.id, hostGeneration);
|
|
4125
|
+
commitGeneration?.();
|
|
4126
|
+
},
|
|
4127
|
+
dispose: (value) => {
|
|
4128
|
+
controller.abort(new DOMException("mobile extension disposed", "AbortError"));
|
|
4129
|
+
try {
|
|
4130
|
+
value.cleanup?.();
|
|
4131
|
+
} finally {
|
|
4132
|
+
disposePending();
|
|
4133
|
+
}
|
|
1674
4134
|
}
|
|
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);
|
|
4135
|
+
};
|
|
1693
4136
|
});
|
|
1694
|
-
return task;
|
|
1695
4137
|
};
|
|
1696
4138
|
const define = (definition) => {
|
|
1697
|
-
if (definition.apiVersion !== 1 || !/^[a-z][a-z0-9-]{0,63}$/u.test(definition.id) || typeof definition.activate !== "function") return;
|
|
4139
|
+
if (disposed || definition.apiVersion !== 1 || !/^[a-z][a-z0-9-]{0,63}$/u.test(definition.id) || typeof definition.activate !== "function") return;
|
|
1698
4140
|
if (expectedDefinitionId !== void 0 && definition.id !== expectedDefinitionId) return;
|
|
1699
4141
|
definitions.set(definition.id, definition);
|
|
1700
4142
|
if (started && expectedDefinitionId === void 0) activateDefinition(definition);
|
|
@@ -1720,22 +4162,37 @@ window.__ModuleLoader__.load({
|
|
|
1720
4162
|
credentials: "same-origin",
|
|
1721
4163
|
cache: "no-store",
|
|
1722
4164
|
headers,
|
|
1723
|
-
|
|
4165
|
+
signal
|
|
1724
4166
|
});
|
|
1725
4167
|
if (response.status === 304) return true;
|
|
1726
4168
|
if (!response.ok) return false;
|
|
1727
|
-
|
|
1728
|
-
|
|
4169
|
+
const nextEtag = response.headers.get("etag") ?? "";
|
|
4170
|
+
const nextModified = response.headers.get("last-modified") ?? "";
|
|
1729
4171
|
const next = await response.text();
|
|
1730
|
-
if (
|
|
4172
|
+
if (refreshAborted(signal)) return false;
|
|
4173
|
+
if (next === legacySource) {
|
|
4174
|
+
legacyJsEtag = nextEtag;
|
|
4175
|
+
legacyJsModified = nextModified;
|
|
4176
|
+
return true;
|
|
4177
|
+
}
|
|
1731
4178
|
legacyMount = void 0;
|
|
1732
4179
|
const script = element("script");
|
|
1733
4180
|
script.textContent = `${next}\n//# sourceURL=dsh-mobile-custom.js`;
|
|
1734
4181
|
document.head.append(script);
|
|
1735
4182
|
script.remove();
|
|
4183
|
+
if (refreshAborted(signal)) {
|
|
4184
|
+
legacyMount = previousMount;
|
|
4185
|
+
return false;
|
|
4186
|
+
}
|
|
1736
4187
|
const mount = legacyMount;
|
|
1737
4188
|
if (mount === void 0) {
|
|
4189
|
+
legacyDispose?.();
|
|
4190
|
+
legacyDispose = void 0;
|
|
4191
|
+
legacyRoot?.remove();
|
|
4192
|
+
legacyRoot = void 0;
|
|
1738
4193
|
legacySource = next;
|
|
4194
|
+
legacyJsEtag = nextEtag;
|
|
4195
|
+
legacyJsModified = nextModified;
|
|
1739
4196
|
return true;
|
|
1740
4197
|
}
|
|
1741
4198
|
const nextRoot = element("div");
|
|
@@ -1748,12 +4205,21 @@ window.__ModuleLoader__.load({
|
|
|
1748
4205
|
root: nextRoot,
|
|
1749
4206
|
window
|
|
1750
4207
|
});
|
|
4208
|
+
if (refreshAborted(signal)) {
|
|
4209
|
+
if (typeof nextDispose === "function") nextDispose();
|
|
4210
|
+
pendingRoot.remove();
|
|
4211
|
+
pendingRoot = void 0;
|
|
4212
|
+
legacyMount = previousMount;
|
|
4213
|
+
return false;
|
|
4214
|
+
}
|
|
1751
4215
|
legacyDispose?.();
|
|
1752
4216
|
legacyRoot?.remove();
|
|
1753
4217
|
legacyRoot = nextRoot;
|
|
1754
4218
|
pendingRoot = void 0;
|
|
1755
4219
|
legacyDispose = typeof nextDispose === "function" ? nextDispose : void 0;
|
|
1756
4220
|
legacySource = next;
|
|
4221
|
+
legacyJsEtag = nextEtag;
|
|
4222
|
+
legacyJsModified = nextModified;
|
|
1757
4223
|
return true;
|
|
1758
4224
|
} catch {
|
|
1759
4225
|
pendingRoot?.remove();
|
|
@@ -1763,6 +4229,26 @@ window.__ModuleLoader__.load({
|
|
|
1763
4229
|
};
|
|
1764
4230
|
let legacyScriptRevision = "";
|
|
1765
4231
|
let legacyStyleRevision = "";
|
|
4232
|
+
const disposeManifestExtension = (id) => {
|
|
4233
|
+
activations.remove(id);
|
|
4234
|
+
styleNodes.get(id)?.remove();
|
|
4235
|
+
styleNodes.delete(id);
|
|
4236
|
+
styleEtags.delete(id);
|
|
4237
|
+
scriptDigests.delete(id);
|
|
4238
|
+
activeHostGenerations.delete(id);
|
|
4239
|
+
activationKeys.delete(id);
|
|
4240
|
+
if (managedDefinitionIds.delete(id)) definitions.delete(id);
|
|
4241
|
+
};
|
|
4242
|
+
const managedManifestIdSources = () => [
|
|
4243
|
+
styleNodes.keys(),
|
|
4244
|
+
styleEtags.keys(),
|
|
4245
|
+
scriptDigests.keys(),
|
|
4246
|
+
managedDefinitionIds
|
|
4247
|
+
];
|
|
4248
|
+
const clearManifestExtensions = () => {
|
|
4249
|
+
publishAuthoritativeExtensionIds(manifestExtensionIds, /* @__PURE__ */ new Set(), managedManifestIdSources(), disposeManifestExtension);
|
|
4250
|
+
manifestEtag = "";
|
|
4251
|
+
};
|
|
1766
4252
|
const refreshExtensions = async (signal) => {
|
|
1767
4253
|
try {
|
|
1768
4254
|
const headers = {};
|
|
@@ -1771,30 +4257,57 @@ window.__ModuleLoader__.load({
|
|
|
1771
4257
|
credentials: "same-origin",
|
|
1772
4258
|
cache: "no-store",
|
|
1773
4259
|
headers,
|
|
1774
|
-
|
|
4260
|
+
signal
|
|
1775
4261
|
});
|
|
1776
|
-
if (response.status === 404)
|
|
4262
|
+
if (response.status === 404) {
|
|
4263
|
+
legacyScriptRevision = "";
|
|
4264
|
+
legacyStyleRevision = "";
|
|
4265
|
+
return handleMissingExtensionManifest(clearManifestExtensions, () => Promise.all([refreshLegacy(signal), refreshCssLegacy(legacyStyle, signal, legacyCssState)]), signal);
|
|
4266
|
+
}
|
|
1777
4267
|
if (response.status === 304) return true;
|
|
1778
4268
|
if (!response.ok) return false;
|
|
1779
4269
|
const nextManifestEtag = response.headers.get("etag") ?? "";
|
|
1780
|
-
const payload = await response.json();
|
|
1781
|
-
|
|
1782
|
-
const
|
|
4270
|
+
const payload = parseMobileExtensionManifest(await response.json());
|
|
4271
|
+
if (refreshAborted(signal) || payload === void 0) return false;
|
|
4272
|
+
const entries = payload.extensions;
|
|
4273
|
+
const seen = new Set(entries.map((entry) => entry.id));
|
|
4274
|
+
const scriptRevision = payload.legacy.scriptRevision;
|
|
4275
|
+
const styleRevision = payload.legacy.styleRevision;
|
|
1783
4276
|
let refreshComplete = true;
|
|
1784
4277
|
if (scriptRevision === "" || scriptRevision !== legacyScriptRevision) {
|
|
1785
4278
|
if (await refreshLegacy(signal)) legacyScriptRevision = scriptRevision;
|
|
1786
4279
|
else refreshComplete = false;
|
|
1787
4280
|
}
|
|
1788
4281
|
if (styleRevision === "" || styleRevision !== legacyStyleRevision) {
|
|
1789
|
-
if (await refreshCssLegacy(legacyStyle, signal)) legacyStyleRevision = styleRevision;
|
|
4282
|
+
if (await refreshCssLegacy(legacyStyle, signal, legacyCssState)) legacyStyleRevision = styleRevision;
|
|
1790
4283
|
else refreshComplete = false;
|
|
1791
4284
|
}
|
|
1792
|
-
const
|
|
1793
|
-
|
|
4285
|
+
const commitStyle = (id, change, css, etag) => {
|
|
4286
|
+
if (change === "retain") return;
|
|
4287
|
+
const oldStyle = styleNodes.get(id);
|
|
4288
|
+
if (change === "remove") {
|
|
4289
|
+
oldStyle?.remove();
|
|
4290
|
+
styleNodes.delete(id);
|
|
4291
|
+
styleEtags.delete(id);
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
4294
|
+
if (oldStyle?.textContent !== css) {
|
|
4295
|
+
const node = element("style");
|
|
4296
|
+
node.dataset.dshMobileExtensionStyle = id;
|
|
4297
|
+
node.textContent = css ?? "";
|
|
4298
|
+
document.head.append(node);
|
|
4299
|
+
styleNodes.set(id, node);
|
|
4300
|
+
oldStyle?.remove();
|
|
4301
|
+
}
|
|
4302
|
+
if (etag !== void 0 && etag !== "") styleEtags.set(id, etag);
|
|
4303
|
+
};
|
|
1794
4304
|
for (const entry of entries) {
|
|
1795
|
-
|
|
1796
|
-
|
|
4305
|
+
const hadActiveGeneration = activations.hasActive(entry.id);
|
|
4306
|
+
const previousHostGeneration = activeHostGenerations.get(entry.id);
|
|
4307
|
+
let previousDefinition;
|
|
4308
|
+
let evaluatedDefinition = false;
|
|
1797
4309
|
try {
|
|
4310
|
+
let styleChange = entry.styleUrl === void 0 ? "remove" : "retain";
|
|
1798
4311
|
let pendingCss;
|
|
1799
4312
|
let pendingStyleEtag;
|
|
1800
4313
|
const cssUrl = typeof entry.styleUrl === "string" ? entry.styleUrl : void 0;
|
|
@@ -1806,16 +4319,25 @@ window.__ModuleLoader__.load({
|
|
|
1806
4319
|
credentials: "same-origin",
|
|
1807
4320
|
cache: "no-store",
|
|
1808
4321
|
headers: cssHeaders,
|
|
1809
|
-
|
|
4322
|
+
signal
|
|
1810
4323
|
});
|
|
1811
4324
|
if (cssResponse.status !== 304) {
|
|
1812
4325
|
if (!cssResponse.ok) throw new Error("mobile extension style failed to load");
|
|
1813
4326
|
pendingStyleEtag = cssResponse.headers.get("etag") ?? void 0;
|
|
1814
4327
|
pendingCss = await cssResponse.text();
|
|
4328
|
+
styleChange = "replace";
|
|
4329
|
+
if (refreshAborted(signal)) return false;
|
|
1815
4330
|
}
|
|
1816
4331
|
}
|
|
1817
4332
|
const scriptUrl = typeof entry.scriptUrl === "string" ? entry.scriptUrl : void 0;
|
|
1818
|
-
if (scriptUrl
|
|
4333
|
+
if (scriptUrl === void 0) {
|
|
4334
|
+
if (refreshAborted(signal)) return false;
|
|
4335
|
+
commitStyle(entry.id, styleChange, pendingCss, pendingStyleEtag);
|
|
4336
|
+
activations.remove(entry.id);
|
|
4337
|
+
activeHostGenerations.delete(entry.id);
|
|
4338
|
+
scriptDigests.delete(entry.id);
|
|
4339
|
+
if (managedDefinitionIds.delete(entry.id)) definitions.delete(entry.id);
|
|
4340
|
+
} else {
|
|
1819
4341
|
const scriptHeaders = {};
|
|
1820
4342
|
const storedDigest = scriptDigests.get(entry.id);
|
|
1821
4343
|
if (storedDigest !== void 0) scriptHeaders["if-none-match"] = storedDigest;
|
|
@@ -1823,15 +4345,18 @@ window.__ModuleLoader__.load({
|
|
|
1823
4345
|
credentials: "same-origin",
|
|
1824
4346
|
cache: "no-store",
|
|
1825
4347
|
headers: scriptHeaders,
|
|
1826
|
-
|
|
4348
|
+
signal
|
|
1827
4349
|
});
|
|
4350
|
+
let nextDigest;
|
|
1828
4351
|
if (scriptResponse.status !== 304) {
|
|
1829
4352
|
if (!scriptResponse.ok) throw new Error("mobile extension script failed to load");
|
|
1830
4353
|
const source = await scriptResponse.text();
|
|
4354
|
+
if (refreshAborted(signal)) return false;
|
|
1831
4355
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(source));
|
|
4356
|
+
if (refreshAborted(signal)) return false;
|
|
1832
4357
|
const key = [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
1833
4358
|
if (scriptDigests.get(entry.id) !== key) {
|
|
1834
|
-
|
|
4359
|
+
previousDefinition = definitions.get(entry.id);
|
|
1835
4360
|
try {
|
|
1836
4361
|
expectedDefinitionId = entry.id;
|
|
1837
4362
|
try {
|
|
@@ -1844,9 +4369,8 @@ window.__ModuleLoader__.load({
|
|
|
1844
4369
|
}
|
|
1845
4370
|
const nextDefinition = definitions.get(entry.id);
|
|
1846
4371
|
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);
|
|
4372
|
+
evaluatedDefinition = true;
|
|
4373
|
+
nextDigest = key;
|
|
1850
4374
|
} catch (error) {
|
|
1851
4375
|
if (previousDefinition === void 0) definitions.delete(entry.id);
|
|
1852
4376
|
else definitions.set(entry.id, previousDefinition);
|
|
@@ -1855,37 +4379,35 @@ window.__ModuleLoader__.load({
|
|
|
1855
4379
|
}
|
|
1856
4380
|
}
|
|
1857
4381
|
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();
|
|
4382
|
+
const commitGeneration = () => {
|
|
4383
|
+
commitStyle(entry.id, styleChange, pendingCss, pendingStyleEtag);
|
|
4384
|
+
};
|
|
4385
|
+
if (definition === void 0) throw new Error("mobile extension definition is unavailable");
|
|
4386
|
+
if (evaluatedDefinition || !activations.hasActive(entry.id) || activeHostGenerations.get(entry.id) !== entry.generation) {
|
|
4387
|
+
if (!await activateDefinition(definition, signal, commitGeneration, entry.generation)) throw new Error("mobile extension activation failed");
|
|
4388
|
+
} else {
|
|
4389
|
+
if (refreshAborted(signal)) return false;
|
|
4390
|
+
commitGeneration();
|
|
1869
4391
|
}
|
|
1870
|
-
|
|
4392
|
+
managedDefinitionIds.add(entry.id);
|
|
4393
|
+
if (nextDigest !== void 0) scriptDigests.set(entry.id, nextDigest);
|
|
1871
4394
|
}
|
|
1872
4395
|
} catch {
|
|
4396
|
+
if (evaluatedDefinition) {
|
|
4397
|
+
if (previousDefinition === void 0) definitions.delete(entry.id);
|
|
4398
|
+
else definitions.set(entry.id, previousDefinition);
|
|
4399
|
+
}
|
|
4400
|
+
failClosedExtensionGenerationReplacement(hadActiveGeneration, previousHostGeneration, entry.generation, () => {
|
|
4401
|
+
disposeManifestExtension(entry.id);
|
|
4402
|
+
});
|
|
1873
4403
|
refreshComplete = false;
|
|
1874
4404
|
}
|
|
1875
4405
|
}
|
|
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 : "";
|
|
4406
|
+
if (refreshAborted(signal)) return false;
|
|
4407
|
+
if (refreshComplete) {
|
|
4408
|
+
publishAuthoritativeExtensionIds(manifestExtensionIds, seen, managedManifestIdSources(), disposeManifestExtension);
|
|
4409
|
+
manifestEtag = nextManifestEtag;
|
|
4410
|
+
} else manifestEtag = "";
|
|
1889
4411
|
return refreshComplete;
|
|
1890
4412
|
} catch {
|
|
1891
4413
|
return false;
|
|
@@ -1893,76 +4415,60 @@ window.__ModuleLoader__.load({
|
|
|
1893
4415
|
};
|
|
1894
4416
|
started = true;
|
|
1895
4417
|
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);
|
|
4418
|
+
const stopRefresh = startLifecycleRefreshScheduler(async (signal) => {
|
|
4419
|
+
await refreshExtensions(signal);
|
|
4420
|
+
});
|
|
4421
|
+
const stopEvents = startExtensionChangeStream(() => {
|
|
4422
|
+
stopRefresh.refresh();
|
|
4423
|
+
});
|
|
1930
4424
|
return () => {
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
document.removeEventListener("visibilitychange", resumeRefresh);
|
|
1935
|
-
window.removeEventListener("online", resumeRefresh);
|
|
4425
|
+
disposed = true;
|
|
4426
|
+
stopEvents();
|
|
4427
|
+
stopRefresh();
|
|
1936
4428
|
started = false;
|
|
1937
4429
|
legacyDispose?.();
|
|
4430
|
+
legacyDispose = void 0;
|
|
1938
4431
|
legacyRoot?.remove();
|
|
4432
|
+
legacyRoot = void 0;
|
|
1939
4433
|
legacyStyle.remove();
|
|
1940
|
-
|
|
4434
|
+
activations.dispose();
|
|
1941
4435
|
for (const node of styleNodes.values()) node.remove();
|
|
4436
|
+
styleNodes.clear();
|
|
1942
4437
|
document.querySelector("[data-dsh-mobile-extension-layer]")?.remove();
|
|
1943
4438
|
for (const host of document.querySelectorAll("[data-dsh-mobile-surface-host]")) host.remove();
|
|
1944
4439
|
if (previous === void 0) delete window.dshMobile;
|
|
1945
4440
|
else window.dshMobile = previous;
|
|
1946
4441
|
};
|
|
1947
4442
|
}
|
|
1948
|
-
|
|
1949
|
-
let cssModified = "";
|
|
1950
|
-
async function refreshCssLegacy(style, signal) {
|
|
4443
|
+
async function refreshCssLegacy(style, signal, state) {
|
|
1951
4444
|
try {
|
|
1952
4445
|
const headers = {};
|
|
1953
|
-
if (
|
|
1954
|
-
if (
|
|
1955
|
-
|
|
4446
|
+
if (state.etag !== "") headers["if-none-match"] = state.etag;
|
|
4447
|
+
if (state.modified !== "") headers["if-modified-since"] = state.modified;
|
|
4448
|
+
let response = await fetch("/mobile-access/custom.css", {
|
|
1956
4449
|
credentials: "same-origin",
|
|
1957
4450
|
cache: "no-store",
|
|
1958
4451
|
headers,
|
|
1959
|
-
|
|
4452
|
+
signal
|
|
1960
4453
|
});
|
|
4454
|
+
if (response.status === 304 && style.textContent === "") {
|
|
4455
|
+
state.etag = "";
|
|
4456
|
+
state.modified = "";
|
|
4457
|
+
response = await fetch("/mobile-access/custom.css", {
|
|
4458
|
+
credentials: "same-origin",
|
|
4459
|
+
cache: "no-store",
|
|
4460
|
+
signal
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
1961
4463
|
if (response.status === 304) return true;
|
|
1962
4464
|
if (response.ok) {
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
4465
|
+
const nextEtag = response.headers.get("etag") ?? "";
|
|
4466
|
+
const nextModified = response.headers.get("last-modified") ?? "";
|
|
4467
|
+
const css = await response.text();
|
|
4468
|
+
if (refreshAborted(signal)) return false;
|
|
4469
|
+
style.textContent = css;
|
|
4470
|
+
state.etag = nextEtag;
|
|
4471
|
+
state.modified = nextModified;
|
|
1966
4472
|
return true;
|
|
1967
4473
|
}
|
|
1968
4474
|
return false;
|
|
@@ -1970,17 +4476,20 @@ window.__ModuleLoader__.load({
|
|
|
1970
4476
|
return false;
|
|
1971
4477
|
}
|
|
1972
4478
|
}
|
|
4479
|
+
/** Theme-aware desktop Mobile Access panel styles. */
|
|
1973
4480
|
const CONTROL_STYLES = `
|
|
1974
4481
|
.dsh-mobile-control{position:fixed;z-index:1000;left:16px;bottom:112px;font:14px/1.45 system-ui;color:var(--dsw-alias-label-primary,#16181d)}
|
|
1975
4482
|
.dsh-mobile-control__panel{box-sizing:border-box;width:min(380px,calc(100vw - 32px));max-height:calc(100vh - 140px);overflow-y:auto;padding:16px;border:1px solid var(--dsw-alias-border-subtle,#e1e5eb);border-radius:18px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 18px 50px rgb(15 23 42 / 18%)}
|
|
1976
|
-
.dsh-mobile-control__header{display:flex;align-items:center;justify-content:space-between;gap:
|
|
1977
|
-
.dsh-mobile-control__app-download{display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;min-height:38px;margin:0 0 10px;padding:8px 11px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:600 12px/1.3 system-ui;text-decoration:none}.dsh-mobile-control__app-download[hidden]{display:none}.dsh-mobile-control__app-download::after{color:#2563eb;font-size:14px;content:"↗"}.dsh-mobile-control__app-download:hover{border-color:#9fb9e8;background
|
|
4483
|
+
.dsh-mobile-control__header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}.dsh-mobile-control__panel h2{margin:0;font-size:17px;line-height:24px}.dsh-mobile-control__header-actions{display:flex;align-items:center;gap:2px}.dsh-mobile-control__update-plugin,.dsh-mobile-control__diagnostic-entry,.dsh-mobile-control__close{display:inline-flex;align-items:center;justify-content:center;min-width:44px;height:44px;padding:0;border:0;border-radius:10px;background:transparent;color:inherit;cursor:pointer}.dsh-mobile-control__update-plugin,.dsh-mobile-control__diagnostic-entry{padding:0 8px;color:#2563eb;font:650 12px/1 system-ui;white-space:nowrap}.dsh-mobile-control__update-plugin[hidden]{display:none}.dsh-mobile-control__update-plugin:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__close{font-size:24px;line-height:1}.dsh-mobile-control__update-plugin:hover:not(:disabled),.dsh-mobile-control__diagnostic-entry:hover,.dsh-mobile-control__close:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}
|
|
4484
|
+
.dsh-mobile-control__release-notice{margin:-2px 0 10px;padding:8px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#eff6ff);color:var(--dsw-alias-label-primary,#1d4ed8);font-size:11px;line-height:1.45}.dsh-mobile-control__release-notice.is-error{color:#dc2626}.dsh-mobile-control__release-notice[hidden]{display:none}.dsh-mobile-control__app-download{display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;min-height:38px;margin:0 0 10px;padding:8px 11px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:600 12px/1.3 system-ui;text-decoration:none}.dsh-mobile-control__app-download[hidden]{display:none}.dsh-mobile-control__app-download::after{color:#2563eb;font-size:14px;content:"↗"}.dsh-mobile-control__app-download:hover{border-color:#9fb9e8;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-2,#f5f8ff));color:var(--dsw-alias-label-primary,#1d4ed8)}
|
|
1978
4485
|
.dsh-mobile-control__switcher{display:grid;grid-template-columns:repeat(2,1fr);gap:4px;margin:0 0 14px;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f3f5f8)}.dsh-mobile-control__switcher[hidden]{display:none}.dsh-mobile-control__tab{min-height:36px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-secondary,#606873);font:600 13px/1 system-ui;cursor:pointer}.dsh-mobile-control__tab.is-active{background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);box-shadow:0 1px 3px rgb(15 23 42 / 10%)}.dsh-mobile-control__view[hidden]{display:none}.dsh-mobile-control__intro{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.55}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions button[hidden]{display:none}
|
|
1979
|
-
.dsh-mobile-control__provider-section{position:relative;margin:0 0
|
|
1980
|
-
.dsh-mobile-
|
|
4486
|
+
.dsh-mobile-control__provider-section{position:relative;margin:0 0 14px}.dsh-mobile-control__section-title{margin:0 0 8px;color:var(--dsw-alias-label-primary,#16181d);font:650 13px/1.4 system-ui}.dsh-mobile-control__provider-section>.dsh-mobile-control__section-title{padding-right:42px}.dsh-mobile-control__provider-choices{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.dsh-mobile-control__provider{display:flex;min-width:0;flex-direction:column;gap:6px;min-height:94px;padding:10px 11px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);text-align:left;cursor:pointer;touch-action:manipulation;transition:border-color 160ms ease,background-color 160ms ease,box-shadow 160ms ease}.dsh-mobile-control__provider:hover{border-color:#6f96db;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-1,#f8fbff))}.dsh-mobile-control__provider.is-selected{border-color:#2563eb;background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-bg-layer-1,#f5f8ff));box-shadow:0 0 0 1px #2563eb inset}.dsh-mobile-control__provider:disabled{cursor:wait;opacity:.62}.dsh-mobile-control__provider-top{display:flex;min-width:0;align-items:flex-start;justify-content:space-between;gap:5px}.dsh-mobile-control__provider-top strong{min-width:0;font-size:12px;line-height:1.3}.dsh-mobile-control__provider-badge{flex:none;padding:2px 5px;border-radius:999px;background:#e8f0ff;color:#1d4ed8;font:650 9px/1.25 system-ui}.dsh-mobile-control__provider-badge.is-cpolar{background:#eaf8f2;color:#087454}.dsh-mobile-control__provider-description{color:var(--dsw-alias-label-secondary,#606873);font-size:10px;line-height:1.45}.dsh-mobile-control__provider-info{position:absolute;z-index:5;top:-13px;right:-8px}.dsh-mobile-control__provider-info-button{display:flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary,#475569);cursor:pointer;touch-action:manipulation}.dsh-mobile-control__provider-info-button:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f5f9);color:#2563eb}.dsh-mobile-control__provider-info-glyph{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:18px;height:18px;border:1.5px solid currentColor;border-radius:50%;font:700 12px/1 system-ui}.dsh-mobile-control__provider-info-popover{position:absolute;z-index:6;top:38px;right:4px;box-sizing:border-box;width:min(292px,calc(100vw - 72px));padding:10px 12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 10px 28px rgb(15 23 42 / 16%)}.dsh-mobile-control__provider-info-popover[hidden]{display:none}.dsh-mobile-control__provider-info-popover strong,.dsh-mobile-control__provider-info-popover span{display:block}.dsh-mobile-control__provider-info-popover strong{margin-bottom:3px;font-size:12px}.dsh-mobile-control__provider-info-popover span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}
|
|
4487
|
+
.dsh-mobile-control__self-hosted{margin:8px 0 0;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__self-hosted-summary{display:flex;box-sizing:border-box;min-height:48px;align-items:center;justify-content:space-between;gap:10px;padding:8px 11px;cursor:pointer;list-style-position:inside}.dsh-mobile-control__self-hosted-summary>span:first-child{display:flex;min-width:0;flex-direction:column;gap:1px}.dsh-mobile-control__self-hosted-summary strong{font-size:11px}.dsh-mobile-control__self-hosted-summary span span{color:var(--dsw-alias-label-secondary,#606873);font-size:9px;line-height:1.35}.dsh-mobile-control__provider-badge.is-frp{background:#eef0f3;color:#475569}.dsh-mobile-control__self-hosted-body{padding:0 8px 8px}.dsh-mobile-control__provider.is-frp{width:100%;min-height:64px;background:var(--dsw-alias-bg-layer-2,#fff)}
|
|
4488
|
+
.dsh-mobile-control__cpolar-setup{margin:0 0 12px;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__cpolar-setup[hidden],.dsh-mobile-control__cpolar-account[hidden],.dsh-mobile-control__details[hidden],.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions[hidden],.dsh-mobile-control__danger[hidden]{display:none}.dsh-mobile-control__component-status,.dsh-mobile-control__component-note{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__primary{width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:600 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__cpolar-account{margin-top:10px}.dsh-mobile-control__link-row{display:flex;flex-wrap:wrap;gap:6px 12px;margin:0 0 10px}.dsh-mobile-control__text-link{color:#2563eb;font-size:11px;text-decoration:none}.dsh-mobile-control__text-link:hover{text-decoration:underline}.dsh-mobile-control__token-label{display:flex;flex-direction:column;gap:5px;margin:0 0 8px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__token{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__cpolar-connect{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;min-height:44px;padding:10px 14px;border-radius:12px;font:650 13px/1.2 system-ui;cursor:pointer;transition:background-color 160ms ease,border-color 160ms ease,opacity 160ms ease}.dsh-mobile-control__cpolar-connect:hover:not(:disabled){border-color:#1d4ed8;background:#1d4ed8}.dsh-mobile-control__cpolar-connect:active:not(:disabled){border-color:#1e40af;background:#1e40af}.dsh-mobile-control__cpolar-connect:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__details{margin:10px 0 0;border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb);padding-top:9px}.dsh-mobile-control__details>summary{min-height:30px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:30px;cursor:pointer}.dsh-mobile-control__details-body{display:flex;flex-wrap:wrap;align-items:center;gap:7px 12px;padding:4px 0}.dsh-mobile-control__details-body p{flex:1 0 100%;margin:0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__storage{display:block;flex:1 0 100%;max-width:100%;overflow:hidden;padding:7px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-secondary,#475569);font:10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__danger{flex:1 0 100%;min-height:38px;margin-top:3px;padding:7px 10px;border:1px solid #dc2626;border-radius:9px;background:transparent;color:#dc2626;font:12px/1.3 system-ui;cursor:pointer}
|
|
4489
|
+
.dsh-mobile-control__frp-setup{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__frp-setup[hidden]{display:none}.dsh-mobile-control__frp-step{padding:11px 0}.dsh-mobile-control__frp-step + .dsh-mobile-control__frp-step{border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}.dsh-mobile-control__frp-step>strong{display:block;margin-bottom:3px;font-size:12px;line-height:1.4}.dsh-mobile-control__frp-step>p{margin:0 0 9px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__frp-step>.dsh-mobile-control__frp-requirement{padding:8px 9px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-primary,#384152);font-size:11px}.dsh-mobile-control__frp-fields{display:grid;grid-template-columns:minmax(0,1fr) 96px;gap:8px}.dsh-mobile-control__field{display:flex;min-width:0;flex-direction:column;gap:5px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__field:nth-child(3),.dsh-mobile-control__field:nth-child(4){grid-column:1/-1}.dsh-mobile-control__field input{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__frp-action{box-sizing:border-box;width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:650 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__frp-action:disabled{cursor:not-allowed;opacity:.5}.dsh-mobile-control__remote-workspace{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:15px;background:var(--dsw-alias-bg-layer-1,#f7f8fa)}.dsh-mobile-control__stage-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}.dsh-mobile-control__stage-header .dsh-mobile-control__section-title{margin:0}.dsh-mobile-control__stage-meta{display:flex;min-width:0;align-items:center;justify-content:flex-end;gap:5px}.dsh-mobile-control__stage-value{max-width:115px;overflow:hidden;color:var(--dsw-alias-label-primary,#16181d);font:650 10px/1.3 system-ui;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__state-badge{flex:none;padding:3px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#606873);font:650 9px/1.25 system-ui}.dsh-mobile-control__state-badge.is-ready{background:#e6f7f0;color:#087454}.dsh-mobile-control__state-badge.is-busy{background:#e8f0ff;color:#1d4ed8}.dsh-mobile-control__state-badge.is-attention{background:#fff4dc;color:#935100}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__status{box-sizing:border-box;margin:0 0 10px;padding:9px 10px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);font-size:11px;line-height:1.45}.dsh-mobile-control__provider-setup-body{margin:0 0 10px}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup{margin:0}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__section-title,.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__frp-setup>.dsh-mobile-control__section-title{display:none}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__details{margin:0;padding:9px 10px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__actions{margin-top:2px}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__qr{margin:10px 0 0}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__manage-row{margin-top:10px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}
|
|
1981
4490
|
.dsh-mobile-control__access{display:flex;align-items:baseline;gap:6px;min-width:0;margin:0 0 12px}.dsh-mobile-control__access[hidden]{display:none}.dsh-mobile-control__access-label{flex:none;color:var(--dsw-alias-label-secondary,#606873);white-space:nowrap}.dsh-mobile-control__access-label::after{content:":"}.dsh-mobile-control__access-link{min-width:0;overflow:hidden;color:#2563eb;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__access-link:hover{text-decoration:underline}.dsh-mobile-control__qr{display:flex;justify-content:center;margin:0 0 12px}.dsh-mobile-control__qr[hidden]{display:none}.dsh-mobile-control__qr img{border-radius:12px;background:#fff;padding:8px}
|
|
1982
4491
|
.dsh-mobile-control__status{margin:0 0 14px;overflow-wrap:anywhere;color:var(--dsw-alias-label-secondary,#606873)}.dsh-mobile-control__status::before{display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:#98a1ad;content:""}.dsh-mobile-control__status.is-running::before{background:#16a36a}.dsh-mobile-control__status.is-key{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;word-break:break-all}
|
|
1983
|
-
.dsh-mobile-control__guide{margin:0 0 14px;padding:12px;border:1px solid #
|
|
4492
|
+
.dsh-mobile-control__guide{margin:0 0 14px;padding:12px;border:1px solid #6f96db;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#eff6ff)}.dsh-mobile-control__guide[hidden]{display:none}.dsh-mobile-control__guide-title{margin:0;color:var(--dsw-alias-label-primary,#172554);font:650 13px/1.45 system-ui}.dsh-mobile-control__guide-summary,.dsh-mobile-control__guide-note{margin:4px 0 0;color:var(--dsw-alias-label-secondary,#475569);font-size:12px;line-height:1.5}.dsh-mobile-control__guide-steps{margin:8px 0 0;padding-left:20px;color:var(--dsw-alias-label-primary,#1e293b);font-size:12px;line-height:1.6}.dsh-mobile-control__guide-note{color:var(--dsw-alias-label-secondary,#64748b)}.dsh-mobile-control__guide-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}.dsh-mobile-control__guide-actions button{min-width:0;min-height:44px;padding:8px;border-radius:10px;font:12px/1.25 system-ui;cursor:pointer}.dsh-mobile-control__guide-actions button:disabled{cursor:not-allowed;opacity:.45}
|
|
1984
4493
|
.dsh-mobile-control__extensions{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px}
|
|
1985
4494
|
.dsh-mobile-control__view.is-diagnostics{--dsh-diagnostic-ok:#087454;--dsh-diagnostic-warning:#a35b00;--dsh-diagnostic-error:#c62828;--dsh-diagnostic-info:#526071}.dsh-mobile-control__diagnostic-summary{box-sizing:border-box;margin:0;padding:13px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:16px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__diagnostic-summary-main{display:grid;grid-template-columns:36px minmax(0,1fr);align-items:center;gap:11px}.dsh-mobile-control__diagnostic-summary-icon{position:relative;display:block;width:36px;height:36px;border-radius:50%;background:#e8edf3;color:var(--dsh-diagnostic-info)}.dsh-mobile-control__diagnostic-summary-icon::before,.dsh-mobile-control__diagnostic-summary-icon::after{position:absolute;content:""}.dsh-mobile-control__diagnostic-summary-body{display:flex;min-width:0;flex-direction:column;gap:2px}.dsh-mobile-control__diagnostic-summary-body strong{font-size:13px;line-height:1.35}.dsh-mobile-control__diagnostic-summary-body span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__diagnostic-summary-meta{display:block;margin-top:11px;padding-top:9px;border-top:1px solid var(--dsw-alias-border-subtle,#dbe1e8);color:var(--dsw-alias-label-secondary,#606873);font-size:10px;line-height:1.45}.dsh-mobile-control__diagnostic-summary.is-ok .dsh-mobile-control__diagnostic-summary-icon{background:#e6f7f0;color:var(--dsh-diagnostic-ok)}.dsh-mobile-control__diagnostic-summary.is-ok .dsh-mobile-control__diagnostic-summary-icon::before{top:10px;left:10px;width:13px;height:7px;border-bottom:2px solid currentColor;border-left:2px solid currentColor;transform:rotate(-45deg)}.dsh-mobile-control__diagnostic-summary.is-attention .dsh-mobile-control__diagnostic-summary-icon{background:#fff4dc;color:var(--dsh-diagnostic-warning)}.dsh-mobile-control__diagnostic-summary.is-error .dsh-mobile-control__diagnostic-summary-icon{background:#fdecec;color:var(--dsh-diagnostic-error)}.dsh-mobile-control__diagnostic-summary.is-attention .dsh-mobile-control__diagnostic-summary-icon::before,.dsh-mobile-control__diagnostic-summary.is-error .dsh-mobile-control__diagnostic-summary-icon::before{top:8px;left:17px;width:2px;height:13px;border-radius:2px;background:currentColor}.dsh-mobile-control__diagnostic-summary.is-attention .dsh-mobile-control__diagnostic-summary-icon::after,.dsh-mobile-control__diagnostic-summary.is-error .dsh-mobile-control__diagnostic-summary-icon::after{bottom:8px;left:17px;width:2px;height:2px;border-radius:50%;background:currentColor}.dsh-mobile-control__diagnostic-summary.is-running .dsh-mobile-control__diagnostic-summary-icon{background:#e8f0ff;color:#2563eb}.dsh-mobile-control__diagnostic-summary.is-running .dsh-mobile-control__diagnostic-summary-icon::before{inset:9px;border:2px solid rgb(37 99 235 / 24%);border-top-color:currentColor;border-radius:50%;animation:dsh-diagnostic-spin .8s linear infinite}
|
|
1986
4495
|
.dsh-mobile-control__diagnostic-summary.is-idle .dsh-mobile-control__diagnostic-summary-icon::before{top:8px;left:17px;width:2px;height:2px;border-radius:50%;background:currentColor}.dsh-mobile-control__diagnostic-summary.is-idle .dsh-mobile-control__diagnostic-summary-icon::after{top:13px;left:17px;width:2px;height:11px;border-radius:2px;background:currentColor}
|
|
@@ -1992,7 +4501,7 @@ window.__ModuleLoader__.load({
|
|
|
1992
4501
|
.dsh-mobile-control button:focus-visible,.dsh-mobile-control a:focus-visible,.dsh-mobile-control input:focus-visible,.dsh-mobile-control summary:focus-visible{outline:3px solid rgb(37 99 235 / 28%);outline-offset:2px}
|
|
1993
4502
|
.dsh-mobile-control__trigger{box-sizing:border-box;display:flex;align-items:center;gap:8px;width:calc(100% + 8px);height:34px;margin:4px -4px;padding:6px 2px 6px 10px;border:0;border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary,#16181d);font:14px/22px system-ui;cursor:pointer}.dsh-mobile-control__trigger:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}.dsh-mobile-control__trigger.is-rail{width:36px;height:36px;margin:8px 0 10px;padding:0;justify-content:center;border-radius:50%}.dsh-mobile-control__trigger-icon{position:relative;box-sizing:border-box;flex:none;width:14px;height:19px;border:1.7px solid currentColor;border-radius:3px}.dsh-mobile-control__trigger-icon::after{position:absolute;right:4px;bottom:2px;width:4px;height:1.5px;border-radius:2px;background:currentColor;content:""}.dsh-mobile-control__trigger-label{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
1994
4503
|
.dsh-mobile-control__manage-row{display:flex;justify-content:space-between;gap:8px;margin-top:10px}.dsh-mobile-control__manage{flex:1 1 0;min-width:0;min-height:34px;padding:6px 8px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:transparent;color:inherit;font:12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__devices{margin-top:10px;border:1px solid var(--dsw-alias-border-subtle,#e1e5eb);border-radius:10px;padding:8px;max-height:220px;overflow-y:auto}.dsh-mobile-control__device-empty{color:var(--dsw-alias-label-secondary,#606873);font-size:12px;margin:0}.dsh-mobile-control__device{display:flex;align-items:center;gap:8px;padding:6px 2px}.dsh-mobile-control__device + .dsh-mobile-control__device{border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}.dsh-mobile-control__device-label{flex:1 1 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}.dsh-mobile-control__device-meta{flex:none;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;white-space:nowrap}.dsh-mobile-control__device-revoke{flex:none;min-height:28px;padding:4px 8px;border:1px solid #dc2626;border-radius:8px;background:transparent;color:#dc2626;font:12px/1.2 system-ui;cursor:pointer}
|
|
1995
|
-
@media (prefers-reduced-motion:reduce){.dsh-mobile-control__provider,.dsh-mobile-control__cpolar-connect{transition:none}.dsh-mobile-control__diagnostic-summary.is-running .dsh-mobile-control__diagnostic-summary-icon::before,.dsh-mobile-control__diagnostic-checks{animation:none}}
|
|
4504
|
+
@media (max-width:359px){.dsh-mobile-control__provider-choices{grid-template-columns:1fr}.dsh-mobile-control__provider{min-height:68px}}@media (prefers-reduced-motion:reduce){.dsh-mobile-control__provider,.dsh-mobile-control__cpolar-connect{transition:none}.dsh-mobile-control__diagnostic-summary.is-running .dsh-mobile-control__diagnostic-summary-icon::before,.dsh-mobile-control__diagnostic-checks{animation:none}}
|
|
1996
4505
|
`;
|
|
1997
4506
|
/** Mount the desktop control or mobile feature enhancements. */
|
|
1998
4507
|
function apply(ctx) {
|
|
@@ -2008,31 +4517,39 @@ window.__ModuleLoader__.load({
|
|
|
2008
4517
|
document.head.append(style);
|
|
2009
4518
|
if (!loopback) {
|
|
2010
4519
|
const removeCustom = installCustomAssets();
|
|
2011
|
-
const removeSurface = installNativeMobileSurface
|
|
4520
|
+
const removeSurface = installDshLanguageBoundSurface(installNativeMobileSurface);
|
|
2012
4521
|
return () => {
|
|
2013
4522
|
removeCustom();
|
|
2014
4523
|
removeSurface();
|
|
2015
4524
|
style.remove();
|
|
2016
4525
|
};
|
|
2017
4526
|
}
|
|
2018
|
-
const
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
4527
|
+
const removeControl = installDshLanguageBoundSurface(() => {
|
|
4528
|
+
const control = installControl();
|
|
4529
|
+
const triggerLocale = selectedMobileControlLocale();
|
|
4530
|
+
const t = controlTranslator(triggerLocale);
|
|
4531
|
+
const disposeSlot = ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
4532
|
+
name: "sidebar.footer.action",
|
|
4533
|
+
id: "dsh-mobile"
|
|
4534
|
+
}, ({ wide }) => (0, react.createElement)("button", {
|
|
4535
|
+
"aria-expanded": false,
|
|
4536
|
+
"aria-label": t("mobileAccess"),
|
|
4537
|
+
className: `dsh-mobile-control__trigger${wide ? "" : " is-rail"}`,
|
|
4538
|
+
lang: triggerLocale,
|
|
4539
|
+
type: "button",
|
|
4540
|
+
title: t("mobileAccess"),
|
|
4541
|
+
onClick: control.toggle
|
|
4542
|
+
}, (0, react.createElement)("span", {
|
|
4543
|
+
"aria-hidden": true,
|
|
4544
|
+
className: "dsh-mobile-control__trigger-icon"
|
|
4545
|
+
}), wide ? (0, react.createElement)("span", { className: "dsh-mobile-control__trigger-label" }, t("mobileAccess")) : void 0)));
|
|
4546
|
+
return () => {
|
|
4547
|
+
disposeSlot();
|
|
4548
|
+
control.remove();
|
|
4549
|
+
};
|
|
4550
|
+
});
|
|
2033
4551
|
return () => {
|
|
2034
|
-
|
|
2035
|
-
control.remove();
|
|
4552
|
+
removeControl();
|
|
2036
4553
|
style.remove();
|
|
2037
4554
|
};
|
|
2038
4555
|
}, "dsh-mobile: stock mobile adaptation and local control");
|
|
@@ -2040,9 +4557,39 @@ window.__ModuleLoader__.load({
|
|
|
2040
4557
|
/** Client services required by the mobile adaptation. */
|
|
2041
4558
|
const inject = ["slots"];
|
|
2042
4559
|
//#endregion
|
|
4560
|
+
exports.CONTROL_STYLES = CONTROL_STYLES;
|
|
4561
|
+
exports.DIAGNOSTIC_REASON_MESSAGES = DIAGNOSTIC_REASON_MESSAGES;
|
|
4562
|
+
exports.MOBILE_CONTROL_MESSAGES = MOBILE_CONTROL_MESSAGES;
|
|
4563
|
+
exports.PerIdActivationLifecycle = PerIdActivationLifecycle;
|
|
2043
4564
|
exports.apply = apply;
|
|
4565
|
+
exports.bindClientResponseLifetime = bindClientResponseLifetime;
|
|
4566
|
+
exports.clientReleaseInfo = clientReleaseInfo;
|
|
4567
|
+
exports.combineClientSignalLifetime = combineClientSignalLifetime;
|
|
4568
|
+
exports.combineClientSignals = combineClientSignals;
|
|
4569
|
+
exports.createFrpServerTemplateForClipboard = createFrpServerTemplateForClipboard;
|
|
4570
|
+
exports.diagnosticEntriesForRender = diagnosticEntriesForRender;
|
|
4571
|
+
exports.diagnosticOverallForChecks = diagnosticOverallForChecks;
|
|
4572
|
+
exports.diagnosticServerCopy = diagnosticServerCopy;
|
|
4573
|
+
exports.extensionAssetUrl = extensionAssetUrl;
|
|
4574
|
+
exports.extensionGenerationHeaders = extensionGenerationHeaders;
|
|
4575
|
+
exports.extensionRouteUrl = extensionRouteUrl;
|
|
4576
|
+
exports.failClosedExtensionGenerationReplacement = failClosedExtensionGenerationReplacement;
|
|
4577
|
+
exports.handleMissingExtensionManifest = handleMissingExtensionManifest;
|
|
2044
4578
|
exports.inject = inject;
|
|
4579
|
+
exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
|
|
4580
|
+
exports.normalizeDiagnosticOverall = normalizeDiagnosticOverall;
|
|
4581
|
+
exports.normalizeDiagnosticStatus = normalizeDiagnosticStatus;
|
|
4582
|
+
exports.parseMobileExtensionManifest = parseMobileExtensionManifest;
|
|
4583
|
+
exports.publishAuthoritativeExtensionIds = publishAuthoritativeExtensionIds;
|
|
4584
|
+
exports.reconcileRemovedExtensions = reconcileRemovedExtensions;
|
|
4585
|
+
exports.registerUniqueDisposable = registerUniqueDisposable;
|
|
4586
|
+
exports.renderDiagnosticPayloadSafely = renderDiagnosticPayloadSafely;
|
|
4587
|
+
exports.selectMobileControlLocale = selectMobileControlLocale;
|
|
4588
|
+
exports.selectedMobileControlLocale = selectedMobileControlLocale;
|
|
4589
|
+
exports.startExtensionChangeStream = startExtensionChangeStream;
|
|
4590
|
+
exports.startLifecycleRefreshScheduler = startLifecycleRefreshScheduler;
|
|
2045
4591
|
exports.trustAuthenticatedGatewayConnection = trustAuthenticatedGatewayConnection;
|
|
4592
|
+
exports.validateDiagnosticChecks = validateDiagnosticChecks;
|
|
2046
4593
|
return module.exports;
|
|
2047
4594
|
}
|
|
2048
4595
|
});
|