dsh-remote-tunnel-easy 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,617 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-remote-tunnel-easy",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+
10
+ //#region css
11
+ const css = [
12
+ ".dshmh-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;min-width:0;list-style:none;transition:border-color .16s,background .16s;overflow:hidden}",
13
+ ".dshmh-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
14
+ ".dshmh-header{width:100%;color:inherit;cursor:pointer;text-align:left;font:inherit;background:0 0;border:0;align-items:center;gap:8px;padding:10px 14px;display:flex}",
15
+ ".dshmh-header:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
16
+ ".dshmh-headText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex;overflow:hidden}",
17
+ ".dshmh-name{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;font-weight:600;overflow:hidden}",
18
+ ".dshmh-description{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}",
19
+ ".dshmh-pending{color:var(--dsw-alias-state-warn-primary);white-space:nowrap;flex:none;font-size:12px}",
20
+ ".dshmh-chevron{color:var(--dsw-alias-label-tertiary);flex:none;font-size:13px;transition:transform .12s}",
21
+ ".dshmh-chevronOpen{transform:rotate(180deg)}",
22
+ ".dshmh-body{flex-direction:column;gap:14px;padding:0 14px 14px;display:flex}",
23
+ ".dshmh-qrRow{display:flex;gap:16px;align-items:center;flex-wrap:wrap}",
24
+ ".dshmh-qr{border:1px solid var(--dsw-alias-border-l2);background:#ffffff;border-radius:6px;padding:10px;flex:none}",
25
+ ".dshmh-qr svg{display:block}",
26
+ ".dshmh-linkBlock{flex-direction:column;gap:8px;min-width:0;flex:1;display:flex}",
27
+ ".dshmh-link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;word-break:break-all}",
28
+ ".dshmh-link:hover{text-decoration:underline}",
29
+ ".dshmh-hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}",
30
+ ".dshmh-failed{color:var(--dsw-alias-state-error-primary);font-size:12px}",
31
+ ".dshmh-field{flex-direction:column;gap:4px;min-width:0;display:flex}",
32
+ ".dshmh-label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}",
33
+ ".dshmh-input{border:1px solid var(--dsw-alias-border-l2);font:inherit;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border-radius:6px;padding:6px 8px;font-size:13px;transition:border-color .13s,box-shadow .13s;width:100%}",
34
+ ".dshmh-input:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
35
+ ".dshmh-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
36
+ ".dshmh-input:disabled{opacity:.6;cursor:default}",
37
+ ".dshmh-check{gap:6px;align-items:center;display:flex;color:var(--dsw-alias-label-primary);font-size:13px;cursor:pointer}",
38
+ ".dshmh-check input{accent-color:var(--dsw-alias-state-business-primary)}",
39
+ ".dshmh-footer{justify-content:flex-end;align-items:center;gap:8px;display:flex;flex-wrap:wrap}",
40
+ ".dshmh-btn{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px;transition:background-color .13s,border-color .13s,color .13s}",
41
+ ".dshmh-save{border:1px solid var(--dsw-alias-button-info-fill);background:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground)}",
42
+ ".dshmh-save:hover:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover)}",
43
+ ".dshmh-save:disabled{opacity:.5;cursor:default}",
44
+ ].join("");
45
+ const tagId = "dsh-remote-tunnel-easy/card.css";
46
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
47
+ const tag = document.createElement("style");
48
+ tag.dataset.plugin = "dsh-remote-tunnel-easy";
49
+ tag.dataset.pluginCss = tagId;
50
+ tag.textContent = css;
51
+ document.head.appendChild(tag);
52
+ }
53
+ //#endregion
54
+
55
+ const BRIDGE_PREFIX = "/api/dsh-remote-handoff-settings";
56
+ const NS = "remote-handoff";
57
+ const lang = typeof navigator !== "undefined" && /^zh\b/i.test(navigator.language ?? "") ? "zh" : "en";
58
+ const I18N = {
59
+ zh: {
60
+ description: "手机免配置访问 —— 扫描二维码即可在手机上打开当前会话",
61
+ starting: "隧道启动中…",
62
+ installing: "正在准备 cloudflared…",
63
+ ready: "扫描二维码在手机上打开当前会话",
64
+ failed: "隧道不可用",
65
+ openLink: "在手机上打开链接 ↗",
66
+ copy: "复制链接",
67
+ copied: "已复制",
68
+ regenerate: "重新生成",
69
+ regenerating: "正在重新生成…",
70
+ target: "隧道目标(本机 harness 地址)",
71
+ cloudflaredPath: "cloudflared 路径",
72
+ sessionHandoff: "会话握手(扫描后进入当前会话)",
73
+ save: "保存",
74
+ saving: "保存中…",
75
+ unsaved: "未保存",
76
+ saveFailed: "保存失败",
77
+ pluginUpdate: "插件更新",
78
+ checkUpdate: "检查更新",
79
+ checkingUpdate: "检查中…",
80
+ upToDate: "已是最新",
81
+ updateAvailable: "有可用更新",
82
+ updating: "更新中…",
83
+ updated: "已更新 — 重启 dsh web 生效",
84
+ updateFailed: "更新失败",
85
+ restartRequired: "重启 dsh web 以应用更新",
86
+ unavailable: "设置不可用 —— remote-handoff 桥接未暴露。",
87
+ },
88
+ en: {
89
+ description: "Hassle-free remote access — scan the QR to open the current session on your phone",
90
+ starting: "Starting tunnel…",
91
+ installing: "Preparing cloudflared…",
92
+ ready: "Scan the QR to open the current session on your phone",
93
+ failed: "Tunnel unavailable",
94
+ openLink: "Open link on phone ↗",
95
+ copy: "Copy link",
96
+ copied: "Copied",
97
+ regenerate: "Regenerate",
98
+ regenerating: "Regenerating…",
99
+ target: "Tunnel target (local harness address)",
100
+ cloudflaredPath: "cloudflared path",
101
+ sessionHandoff: "Session handoff (scan opens the current session)",
102
+ save: "Save",
103
+ saving: "Saving…",
104
+ unsaved: "Unsaved",
105
+ saveFailed: "Save failed",
106
+ pluginUpdate: "Plugin update",
107
+ checkUpdate: "Check for updates",
108
+ checkingUpdate: "Checking…",
109
+ upToDate: "Up to date",
110
+ updateAvailable: "Update available",
111
+ updating: "Updating…",
112
+ updated: "Updated — restart dsh web to apply",
113
+ updateFailed: "Update failed",
114
+ restartRequired: "Restart dsh web to apply",
115
+ unavailable: "Settings unavailable — remote-handoff bridge not exposed.",
116
+ },
117
+ };
118
+
119
+ async function bridgeDescribe() {
120
+ const response = await fetch(`${BRIDGE_PREFIX}/describe`, {
121
+ method: "POST",
122
+ headers: { "content-type": "application/json" },
123
+ body: "{}",
124
+ });
125
+ return response.json();
126
+ }
127
+
128
+ async function bridgeMutate(payload) {
129
+ const response = await fetch(`${BRIDGE_PREFIX}/mutate`, {
130
+ method: "POST",
131
+ headers: { "content-type": "application/json" },
132
+ body: JSON.stringify(payload),
133
+ });
134
+ return response.json();
135
+ }
136
+
137
+ async function bridgeStatus() {
138
+ const response = await fetch(`${BRIDGE_PREFIX}/status`, {
139
+ method: "POST",
140
+ headers: { "content-type": "application/json" },
141
+ body: "{}",
142
+ });
143
+ return response.json();
144
+ }
145
+
146
+ async function bridgeRegenerate() {
147
+ const response = await fetch(`${BRIDGE_PREFIX}/regenerate`, {
148
+ method: "POST",
149
+ headers: { "content-type": "application/json" },
150
+ body: "{}",
151
+ });
152
+ return response.json();
153
+ }
154
+
155
+ async function bridgeUpdateStatus() {
156
+ const response = await fetch(`${BRIDGE_PREFIX}/update-status`, {
157
+ method: "POST",
158
+ headers: { "content-type": "application/json" },
159
+ body: "{}",
160
+ });
161
+ return response.json();
162
+ }
163
+
164
+ async function bridgeCheckUpdate() {
165
+ const response = await fetch(`${BRIDGE_PREFIX}/check-update`, {
166
+ method: "POST",
167
+ headers: { "content-type": "application/json" },
168
+ body: "{}",
169
+ });
170
+ return response.json();
171
+ }
172
+
173
+ async function bridgeUpdate() {
174
+ const response = await fetch(`${BRIDGE_PREFIX}/update`, {
175
+ method: "POST",
176
+ headers: { "content-type": "application/json" },
177
+ body: "{}",
178
+ });
179
+ return response.json();
180
+ }
181
+
182
+ /** Render a QR matrix (count×count 0/1 cells) to a compact SVG element tree. */
183
+ function qrSvg(matrix, size) {
184
+ const cell = size / matrix.count;
185
+ const dark = [];
186
+ for (let r = 0; r < matrix.count; r++) {
187
+ for (let c = 0; c < matrix.count; c++) {
188
+ if (matrix.modules[r][c]) {
189
+ dark.push(
190
+ react_jsx_runtime.jsx("rect", {
191
+ key: r + ":" + c,
192
+ x: c * cell,
193
+ y: r * cell,
194
+ width: cell,
195
+ height: cell,
196
+ fill: "#000000",
197
+ })
198
+ );
199
+ }
200
+ }
201
+ }
202
+ return react_jsx_runtime.jsx("svg", {
203
+ xmlns: "http://www.w3.org/2000/svg",
204
+ viewBox: "0 0 " + size + " " + size,
205
+ width: size,
206
+ height: size,
207
+ children: [
208
+ react_jsx_runtime.jsx("rect", { x: 0, y: 0, width: size, height: size, fill: "#ffffff" }),
209
+ dark,
210
+ ],
211
+ });
212
+ }
213
+
214
+ function RemoteHandoffCard() {
215
+ const [open, setOpen] = react.useState(false);
216
+ const [state, setState] = react.useState({ status: "loading" });
217
+ const [tunnelStatus, setTunnelStatus] = react.useState({ state: "starting" });
218
+ const [target, setTarget] = react.useState("");
219
+ const [autoTarget, setAutoTarget] = react.useState("");
220
+ const [cloudflaredPath, setCloudflaredPath] = react.useState("");
221
+ const [sessionHandoff, setSessionHandoff] = react.useState(true);
222
+ const [dirty, setDirty] = react.useState(false);
223
+ const [saving, setSaving] = react.useState(false);
224
+ const [failed, setFailed] = react.useState(false);
225
+ const [copied, setCopied] = react.useState(false);
226
+ const [regenerating, setRegenerating] = react.useState(false);
227
+ const [updateStatus, setUpdateStatus] = react.useState({ state: "idle", currentVersion: null, latestVersion: null });
228
+ const [checking, setChecking] = react.useState(false);
229
+ const [updating, setUpdating] = react.useState(false);
230
+
231
+ const load = react.useCallback(async () => {
232
+ try {
233
+ const result = await bridgeDescribe();
234
+ if (result.ok) {
235
+ const view = result.value.namespaces.find((n) => n.ns === NS);
236
+ if (view) {
237
+ const v = view.value ?? {};
238
+ setTarget(v.tunnelTarget ?? "");
239
+ setCloudflaredPath(v.cloudflaredPath ?? "");
240
+ setSessionHandoff(v.sessionHandoff !== false);
241
+ setState({ status: "ready", writable: result.value.writable });
242
+ } else {
243
+ setState({ status: "unavailable" });
244
+ }
245
+ } else {
246
+ setState({ status: "unavailable" });
247
+ }
248
+ } catch {
249
+ setState({ status: "unavailable" });
250
+ }
251
+ }, []);
252
+
253
+ const pollStatus = react.useCallback(async () => {
254
+ try {
255
+ const result = await bridgeStatus();
256
+ if (result.ok) {
257
+ setTunnelStatus(result.value);
258
+ // Track the Host-resolved auto target so the input can prefill and
259
+ // stay truthful when the persisted `tunnelTarget` is empty ("auto").
260
+ if (result.value && typeof result.value.resolvedTarget === "string") {
261
+ setAutoTarget(result.value.resolvedTarget);
262
+ }
263
+ }
264
+ } catch {}
265
+ }, []);
266
+
267
+ const regenerate = async () => {
268
+ setRegenerating(true);
269
+ try {
270
+ await bridgeRegenerate();
271
+ } catch {
272
+ // Network/lifecycle failure: the next pollStatus reveals the state.
273
+ }
274
+ pollStatus();
275
+ };
276
+
277
+ // Clear the "regenerating" flag once the Host reports a terminal state
278
+ // (ready with a fresh URL, or failed) so the button re-enables.
279
+ react.useEffect(() => {
280
+ if (regenerating && (tunnelStatus.state === "ready" || tunnelStatus.state === "failed")) {
281
+ setRegenerating(false);
282
+ }
283
+ }, [regenerating, tunnelStatus.state]);
284
+
285
+ // Fast-poll while regenerating so the new QR appears promptly.
286
+ react.useEffect(() => {
287
+ if (!regenerating) return;
288
+ const t = setInterval(pollStatus, 700);
289
+ return () => clearInterval(t);
290
+ }, [regenerating, pollStatus]);
291
+
292
+ react.useEffect(() => {
293
+ load();
294
+ }, [load]);
295
+
296
+ react.useEffect(() => {
297
+ pollStatus();
298
+ const t = setInterval(pollStatus, 5000);
299
+ return () => clearInterval(t);
300
+ }, [pollStatus]);
301
+
302
+ const refreshUpdateStatus = react.useCallback(async () => {
303
+ try {
304
+ const result = await bridgeUpdateStatus();
305
+ if (result.ok) setUpdateStatus(result.value);
306
+ } catch {}
307
+ }, []);
308
+
309
+ const doCheckUpdate = async () => {
310
+ setChecking(true);
311
+ try {
312
+ const result = await bridgeCheckUpdate();
313
+ if (result.ok) setUpdateStatus(result.value);
314
+ else if (result.value) setUpdateStatus(result.value);
315
+ else if (result.error || result.message) setUpdateStatus((prev) => ({ ...prev, state: "failed", error: result.message || result.error }));
316
+ } catch {}
317
+ setChecking(false);
318
+ };
319
+
320
+ const doUpdate = async () => {
321
+ setUpdating(true);
322
+ try {
323
+ const result = await bridgeUpdate();
324
+ if (result.ok) setUpdateStatus(result.value);
325
+ else if (result.value) setUpdateStatus(result.value);
326
+ else if (result.error || result.message) setUpdateStatus((prev) => ({ ...prev, state: "failed", error: result.message || result.error }));
327
+ } catch {}
328
+ setUpdating(false);
329
+ };
330
+
331
+ react.useEffect(() => {
332
+ refreshUpdateStatus();
333
+ }, [refreshUpdateStatus]);
334
+
335
+ const save = async () => {
336
+ setSaving(true);
337
+ setFailed(false);
338
+ try {
339
+ const ops = [
340
+ { op: "set", path: ["tunnelTarget"], value: target },
341
+ { op: "set", path: ["cloudflaredPath"], value: cloudflaredPath },
342
+ { op: "set", path: ["sessionHandoff"], value: sessionHandoff },
343
+ ];
344
+ const result = await bridgeMutate({ ns: NS, ops });
345
+ if (result.ok) {
346
+ setDirty(false);
347
+ load();
348
+ } else {
349
+ setFailed(true);
350
+ }
351
+ } catch {
352
+ setFailed(true);
353
+ } finally {
354
+ setSaving(false);
355
+ }
356
+ };
357
+
358
+ const copyLink = async () => {
359
+ if (!tunnelStatus.accessUrl) return;
360
+ try {
361
+ await navigator.clipboard.writeText(tunnelStatus.accessUrl);
362
+ setCopied(true);
363
+ setTimeout(() => setCopied(false), 1500);
364
+ } catch {}
365
+ };
366
+
367
+ if (state.status === "loading") return null;
368
+ if (state.status === "unavailable") {
369
+ const zh = lang === "zh";
370
+ return react_jsx_runtime.jsx("li", {
371
+ className: "dshmh-card",
372
+ children: react_jsx_runtime.jsx("div", {
373
+ className: "dshmh-body",
374
+ style: { padding: "14px" },
375
+ children: react_jsx_runtime.jsx("span", {
376
+ className: "dshmh-failed",
377
+ children: zh ? I18N.zh.unavailable : I18N.en.unavailable,
378
+ }),
379
+ }),
380
+ });
381
+ }
382
+
383
+ const t = lang === "zh" ? I18N.zh : I18N.en;
384
+ const hasQr = tunnelStatus.state === "ready" && tunnelStatus.matrix;
385
+ const installing = tunnelStatus.state !== "ready" && tunnelStatus.state !== "failed"
386
+ && typeof tunnelStatus.phase === "string" && tunnelStatus.phase !== "starting tunnel…";
387
+ const statusLabel = tunnelStatus.state === "ready"
388
+ ? t.ready
389
+ : tunnelStatus.state === "failed" ? t.failed : (installing ? t.installing : t.starting);
390
+ const targetValue = target || autoTarget;
391
+
392
+ return react_jsx_runtime.jsx("li", {
393
+ className: open ? "dshmh-card dshmh-cardOpen" : "dshmh-card",
394
+ children: [
395
+ react_jsx_runtime.jsx("button", {
396
+ type: "button",
397
+ className: "dshmh-header",
398
+ "aria-expanded": open,
399
+ onClick: () => setOpen(!open),
400
+ children: [
401
+ react_jsx_runtime.jsx("span", {
402
+ className: "dshmh-headText",
403
+ children: [
404
+ react_jsx_runtime.jsx("span", { className: "dshmh-name", children: "DSH Remote" }),
405
+ react_jsx_runtime.jsx("span", { className: "dshmh-description", children: t.description }),
406
+ ],
407
+ }),
408
+ dirty ? react_jsx_runtime.jsx("span", { className: "dshmh-pending", children: t.unsaved }) : null,
409
+ react_jsx_runtime.jsx("span", { className: "dshmh-pending", children: statusLabel }),
410
+ react_jsx_runtime.jsx("span", {
411
+ className: open ? "dshmh-chevron dshmh-chevronOpen" : "dshmh-chevron",
412
+ children: "▾",
413
+ }),
414
+ ],
415
+ }),
416
+ open
417
+ ? react_jsx_runtime.jsx("div", {
418
+ className: "dshmh-body",
419
+ children: [
420
+ tunnelStatus.state === "failed"
421
+ ? react_jsx_runtime.jsx("span", {
422
+ className: "dshmh-failed",
423
+ children: tunnelStatus.error ? t.failed + ": " + tunnelStatus.error : t.failed,
424
+ })
425
+ : null,
426
+ hasQr
427
+ ? react_jsx_runtime.jsx("div", {
428
+ className: "dshmh-qrRow",
429
+ children: [
430
+ react_jsx_runtime.jsx("div", {
431
+ className: "dshmh-qr",
432
+ children: qrSvg(tunnelStatus.matrix, 176),
433
+ }),
434
+ react_jsx_runtime.jsx("div", {
435
+ className: "dshmh-linkBlock",
436
+ children: [
437
+ react_jsx_runtime.jsx("a", {
438
+ className: "dshmh-link",
439
+ href: tunnelStatus.accessUrl,
440
+ target: "_blank",
441
+ rel: "noopener noreferrer",
442
+ children: tunnelStatus.accessUrl,
443
+ }),
444
+ react_jsx_runtime.jsx("div", {
445
+ style: { display: "flex", gap: "8px", flexWrap: "wrap" },
446
+ children: [
447
+ react_jsx_runtime.jsx("a", {
448
+ className: "dshmh-link",
449
+ href: tunnelStatus.accessUrl,
450
+ target: "_blank",
451
+ rel: "noopener noreferrer",
452
+ children: t.openLink,
453
+ }),
454
+ react_jsx_runtime.jsx("button", {
455
+ className: "dshmh-link",
456
+ type: "button",
457
+ style: { background: "none", border: "none", cursor: "pointer", padding: 0 },
458
+ onClick: copyLink,
459
+ children: copied ? t.copied : t.copy,
460
+ }),
461
+ react_jsx_runtime.jsx("button", {
462
+ className: "dshmh-btn dshmh-save",
463
+ type: "button",
464
+ onClick: regenerate,
465
+ disabled: regenerating,
466
+ children: regenerating ? t.regenerating : t.regenerate,
467
+ }),
468
+ ],
469
+ }),
470
+ ],
471
+ }),
472
+ ],
473
+ })
474
+ : null,
475
+ react_jsx_runtime.jsx("div", {
476
+ className: "dshmh-field",
477
+ children: [
478
+ react_jsx_runtime.jsx("span", { className: "dshmh-label", children: t.target }),
479
+ react_jsx_runtime.jsx("input", {
480
+ className: "dshmh-input",
481
+ value: targetValue,
482
+ placeholder: autoTarget || "http://127.0.0.1:3080",
483
+ disabled: saving,
484
+ onChange: (e) => { setTarget(e.target.value); setDirty(true); },
485
+ }),
486
+ ],
487
+ }),
488
+ react_jsx_runtime.jsx("div", {
489
+ className: "dshmh-field",
490
+ children: [
491
+ react_jsx_runtime.jsx("span", { className: "dshmh-label", children: t.cloudflaredPath }),
492
+ react_jsx_runtime.jsx("input", {
493
+ className: "dshmh-input",
494
+ value: cloudflaredPath,
495
+ disabled: saving,
496
+ onChange: (e) => { setCloudflaredPath(e.target.value); setDirty(true); },
497
+ }),
498
+ ],
499
+ }),
500
+ react_jsx_runtime.jsx("label", {
501
+ className: "dshmh-check",
502
+ children: [
503
+ react_jsx_runtime.jsx("input", {
504
+ type: "checkbox",
505
+ checked: sessionHandoff,
506
+ disabled: saving,
507
+ onChange: (e) => { setSessionHandoff(e.target.checked); setDirty(true); },
508
+ }),
509
+ t.sessionHandoff,
510
+ ],
511
+ }),
512
+ react_jsx_runtime.jsx("div", {
513
+ className: "dshmh-field",
514
+ children: [
515
+ react_jsx_runtime.jsx("span", { className: "dshmh-label", children: t.pluginUpdate }),
516
+ react_jsx_runtime.jsx("div", {
517
+ style: { display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" },
518
+ children: [
519
+ updateStatus.state === "idle"
520
+ ? react_jsx_runtime.jsx("span", { className: "dshmh-hint", children: updateStatus.currentVersion ? "v" + updateStatus.currentVersion : "" })
521
+ : updateStatus.state === "checking"
522
+ ? react_jsx_runtime.jsx("span", { className: "dshmh-hint", children: t.checkingUpdate })
523
+ : updateStatus.state === "available"
524
+ ? react_jsx_runtime.jsx("span", { className: "dshmh-hint", children: "v" + updateStatus.currentVersion + " → v" + updateStatus.latestVersion })
525
+ : updateStatus.state === "up-to-date"
526
+ ? react_jsx_runtime.jsx("span", { className: "dshmh-hint", children: t.upToDate + (updateStatus.currentVersion ? " (v" + updateStatus.currentVersion + ")" : "") })
527
+ : updateStatus.state === "updating"
528
+ ? react_jsx_runtime.jsx("span", { className: "dshmh-hint", children: t.updating })
529
+ : updateStatus.state === "updated"
530
+ ? react_jsx_runtime.jsx("span", { className: "dshmh-hint", children: t.updated })
531
+ : updateStatus.state === "failed"
532
+ ? react_jsx_runtime.jsx("span", { className: "dshmh-failed", children: (updateStatus.error ? t.updateFailed + ": " + updateStatus.error : t.updateFailed) })
533
+ : react_jsx_runtime.jsx("span", { className: "dshmh-hint", children: updateStatus.currentVersion ? "v" + updateStatus.currentVersion : "" }),
534
+ updateStatus.state === "available"
535
+ ? react_jsx_runtime.jsx("button", {
536
+ className: "dshmh-btn dshmh-save",
537
+ type: "button",
538
+ onClick: doUpdate,
539
+ disabled: updating || checking,
540
+ children: t.updateAvailable,
541
+ })
542
+ : updateStatus.state === "up-to-date" || updateStatus.state === "idle"
543
+ ? react_jsx_runtime.jsx("button", {
544
+ className: "dshmh-btn dshmh-save",
545
+ type: "button",
546
+ onClick: doCheckUpdate,
547
+ disabled: checking || updating,
548
+ children: checking ? t.checkingUpdate : t.checkUpdate,
549
+ })
550
+ : updateStatus.state === "failed"
551
+ ? react_jsx_runtime.jsx("button", {
552
+ className: "dshmh-btn dshmh-save",
553
+ type: "button",
554
+ onClick: doCheckUpdate,
555
+ disabled: checking || updating,
556
+ children: t.checkUpdate,
557
+ })
558
+ : updateStatus.state === "updated"
559
+ ? react_jsx_runtime.jsx("span", { className: "dshmh-hint", children: t.restartRequired })
560
+ : updateStatus.state === "checking" || updateStatus.state === "updating"
561
+ ? null
562
+ : react_jsx_runtime.jsx("button", {
563
+ className: "dshmh-btn dshmh-save",
564
+ type: "button",
565
+ onClick: doCheckUpdate,
566
+ disabled: checking || updating,
567
+ children: t.checkUpdate,
568
+ }),
569
+ ],
570
+ }),
571
+ updateStatus.requiresRestart
572
+ ? react_jsx_runtime.jsx("span", { className: "dshmh-hint", children: t.restartRequired })
573
+ : null,
574
+ ],
575
+ }),
576
+ react_jsx_runtime.jsx("div", {
577
+ className: "dshmh-footer",
578
+ children: [
579
+ failed ? react_jsx_runtime.jsx("span", { className: "dshmh-failed", children: t.saveFailed }) : null,
580
+ react_jsx_runtime.jsx("button", {
581
+ className: "dshmh-btn dshmh-save",
582
+ type: "button",
583
+ onClick: save,
584
+ disabled: saving || !dirty,
585
+ children: saving ? t.saving : t.save,
586
+ }),
587
+ ],
588
+ }),
589
+ ],
590
+ })
591
+ : null,
592
+ ],
593
+ });
594
+ }
595
+
596
+ const inject = ["slots"];
597
+
598
+ function apply(ctx) {
599
+ ctx.slots.inject("settings.plugin.item", () =>
600
+ ctx.slots.register(
601
+ {
602
+ name: "settings.plugin.item",
603
+ key: "remote-handoff",
604
+ id: "dsh-remote-tunnel-easy",
605
+ order: 130,
606
+ inject: () => ({}),
607
+ },
608
+ RemoteHandoffCard
609
+ )
610
+ );
611
+ }
612
+
613
+ exports.apply = apply;
614
+ exports.inject = inject;
615
+ return module.exports;
616
+ },
617
+ });