dsh-mobile 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.en.md +53 -16
- package/README.md +60 -20
- package/SECURITY.md +4 -1
- package/THIRD_PARTY_NOTICES.md +5 -0
- package/bin/dsh-mobile-funnel-win32-x64.exe +0 -0
- package/lib/cli.js +50 -6
- package/lib/client.js +667 -17
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +8 -3
- package/lib/index.mjs +1606 -140
- package/package.json +9 -3
package/lib/client.js
CHANGED
|
@@ -87,6 +87,9 @@ window.__ModuleLoader__.load({
|
|
|
87
87
|
[data-dsh-mobile-center] pre { max-width:100%; overflow-x:auto; }
|
|
88
88
|
[data-dsh-mobile-center] :is(img,video,canvas,svg) { max-width:100%; }
|
|
89
89
|
[data-dsh-mobile-message-scroll] { box-sizing:border-box !important; width:100% !important; padding:8px 10px 20px !important; }
|
|
90
|
+
[data-dsh-mobile-history-loader] { position:relative !important; min-height:1px !important; }
|
|
91
|
+
[data-dsh-mobile-history-loader] button:not(:disabled) { position:absolute !important; width:1px !important; height:1px !important; margin:-1px !important; padding:0 !important; clip-path:inset(50%) !important; opacity:0 !important; overflow:hidden !important; pointer-events:none !important; }
|
|
92
|
+
[data-dsh-mobile-history-loader] button:disabled { min-height:28px !important; padding:4px 12px !important; }
|
|
90
93
|
[data-dsh-mobile-message-column] { box-sizing:border-box !important; width:100% !important; max-width:none !important; margin:0 !important; padding:0 !important; gap:10px !important; }
|
|
91
94
|
[data-dsh-mobile-message-column] > * { width:100% !important; max-width:100% !important; }
|
|
92
95
|
[data-dsh-mobile-message-column] [data-disclosure-row] { box-sizing:border-box !important; display:grid !important; grid-template-columns:16px minmax(0,1fr) !important; grid-auto-rows:auto !important; align-items:center !important; column-gap:6px !important; width:100% !important; height:auto !important; min-height:40px !important; padding:4px 0 !important; }
|
|
@@ -159,6 +162,11 @@ window.__ModuleLoader__.load({
|
|
|
159
162
|
function firstByClassSuffix(root, suffix) {
|
|
160
163
|
return Array.from(root.querySelectorAll("[class]")).find((element) => classToken(element, suffix));
|
|
161
164
|
}
|
|
165
|
+
const AUTO_HISTORY_THRESHOLD_PX = 64;
|
|
166
|
+
/** Whether a user-driven scroll moved upward into the automatic history-loading zone. */
|
|
167
|
+
function shouldAutoLoadEarlier(previousTop, currentTop) {
|
|
168
|
+
return currentTop <= AUTO_HISTORY_THRESHOLD_PX && currentTop < previousTop - .5;
|
|
169
|
+
}
|
|
162
170
|
/** Add mobile semantics without replacing feature trees. */
|
|
163
171
|
function installNativeMobileSurface() {
|
|
164
172
|
document.documentElement.classList.add("dsh-native-mobile-active");
|
|
@@ -213,6 +221,28 @@ window.__ModuleLoader__.load({
|
|
|
213
221
|
let transitionRestartFrame = 0;
|
|
214
222
|
let transitionTimer = 0;
|
|
215
223
|
let transitionTarget;
|
|
224
|
+
let historyScroller;
|
|
225
|
+
let historyPreviousTop = 0;
|
|
226
|
+
const historyLoadButton = () => {
|
|
227
|
+
return (historyScroller === void 0 ? void 0 : firstByClassSuffix(historyScroller, "_older"))?.querySelector("button") ?? void 0;
|
|
228
|
+
};
|
|
229
|
+
const onHistoryScroll = () => {
|
|
230
|
+
if (historyScroller === void 0) return;
|
|
231
|
+
const currentTop = Math.max(0, historyScroller.scrollTop);
|
|
232
|
+
const shouldLoad = shouldAutoLoadEarlier(historyPreviousTop, currentTop);
|
|
233
|
+
historyPreviousTop = currentTop;
|
|
234
|
+
if (!shouldLoad) return;
|
|
235
|
+
const button = historyLoadButton();
|
|
236
|
+
if (button === void 0 || button.disabled || button.getAttribute("aria-disabled") === "true") return;
|
|
237
|
+
button.click();
|
|
238
|
+
};
|
|
239
|
+
const bindHistoryScroller = (next) => {
|
|
240
|
+
if (historyScroller === next) return;
|
|
241
|
+
historyScroller?.removeEventListener("scroll", onHistoryScroll);
|
|
242
|
+
historyScroller = next;
|
|
243
|
+
historyPreviousTop = next?.scrollTop ?? 0;
|
|
244
|
+
historyScroller?.addEventListener("scroll", onHistoryScroll, { passive: true });
|
|
245
|
+
};
|
|
216
246
|
const animateNavigation = (event) => {
|
|
217
247
|
if (!(event.target instanceof Element)) return;
|
|
218
248
|
const trigger = event.target.closest("button,a,[role=\"tab\"],[aria-selected]");
|
|
@@ -254,13 +284,28 @@ window.__ModuleLoader__.load({
|
|
|
254
284
|
const center = frame === void 0 ? dedicatedCenter : firstByClassSuffix(frame, "_centerCol");
|
|
255
285
|
const details = frame === void 0 ? void 0 : firstByClassSuffix(frame, "_detailsCol");
|
|
256
286
|
const handle = frame === void 0 ? void 0 : firstByClassSuffix(frame, "_handle");
|
|
257
|
-
if (center === void 0)
|
|
287
|
+
if (center === void 0) {
|
|
288
|
+
bindHistoryScroller(void 0);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
258
291
|
if (center !== void 0) {
|
|
259
292
|
center.dataset.dshMobileCenter = "true";
|
|
260
293
|
center.querySelector("header")?.setAttribute("data-dsh-mobile-header", "true");
|
|
261
294
|
viewArea = firstByClassSuffix(center, "_viewArea");
|
|
262
295
|
if (viewArea !== void 0) viewArea.dataset.dshMobileView = "true";
|
|
263
296
|
const conversation = center.querySelector("[data-conversation-scroll]");
|
|
297
|
+
bindHistoryScroller(conversation ?? void 0);
|
|
298
|
+
const historyLoader = conversation === null ? void 0 : firstByClassSuffix(conversation, "_older");
|
|
299
|
+
if (historyLoader !== void 0) {
|
|
300
|
+
historyLoader.dataset.dshMobileHistoryLoader = "true";
|
|
301
|
+
historyLoader.setAttribute("aria-live", "polite");
|
|
302
|
+
const button = historyLoader.querySelector("button");
|
|
303
|
+
if (button !== null) {
|
|
304
|
+
button.tabIndex = -1;
|
|
305
|
+
if (button.disabled) button.removeAttribute("aria-hidden");
|
|
306
|
+
else button.setAttribute("aria-hidden", "true");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
264
309
|
const messageColumn = conversation === null ? void 0 : firstByClassSuffix(conversation, "_column");
|
|
265
310
|
const messageScroll = messageColumn?.parentElement;
|
|
266
311
|
if (messageColumn !== void 0 && messageScroll !== null && messageScroll !== void 0 && classToken(messageScroll, "_scroll")) {
|
|
@@ -337,7 +382,11 @@ window.__ModuleLoader__.load({
|
|
|
337
382
|
childList: true,
|
|
338
383
|
subtree: true,
|
|
339
384
|
attributes: true,
|
|
340
|
-
attributeFilter: [
|
|
385
|
+
attributeFilter: [
|
|
386
|
+
"class",
|
|
387
|
+
"style",
|
|
388
|
+
"disabled"
|
|
389
|
+
]
|
|
341
390
|
});
|
|
342
391
|
backdrop.addEventListener("click", () => {
|
|
343
392
|
if (sidebar?.dataset.open === "true") toggle?.click();
|
|
@@ -353,6 +402,7 @@ window.__ModuleLoader__.load({
|
|
|
353
402
|
if (transitionRestartFrame !== 0) cancelAnimationFrame(transitionRestartFrame);
|
|
354
403
|
if (transitionTimer !== 0) clearTimeout(transitionTimer);
|
|
355
404
|
transitionTarget?.removeAttribute("data-dsh-mobile-view-transition");
|
|
405
|
+
historyScroller?.removeEventListener("scroll", onHistoryScroll);
|
|
356
406
|
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
357
407
|
document.removeEventListener("keydown", onKeyDown, true);
|
|
358
408
|
document.removeEventListener("click", animateNavigation);
|
|
@@ -406,6 +456,19 @@ window.__ModuleLoader__.load({
|
|
|
406
456
|
if (!response.ok) throw new Error(typeof body.error === "string" ? body.error : `HTTP ${String(response.status)}`);
|
|
407
457
|
return body;
|
|
408
458
|
}
|
|
459
|
+
function officialFunnelSetupUrl(value) {
|
|
460
|
+
if (typeof value !== "string" || value.length > 2048) return "";
|
|
461
|
+
let url;
|
|
462
|
+
try {
|
|
463
|
+
url = new URL(value);
|
|
464
|
+
} catch {
|
|
465
|
+
return "";
|
|
466
|
+
}
|
|
467
|
+
const normalized = url.toString().replace(/\/$/u, "");
|
|
468
|
+
if (normalized === "https://tailscale.com/s/no-funnel" || normalized === "https://tailscale.com/s/https") return normalized;
|
|
469
|
+
if (url.protocol !== "https:" || url.hostname !== "login.tailscale.com" || url.port !== "" || url.username !== "" || url.password !== "") return "";
|
|
470
|
+
return url.toString();
|
|
471
|
+
}
|
|
409
472
|
function installControl() {
|
|
410
473
|
const root = element("div", "dsh-mobile-control");
|
|
411
474
|
const panel = element("section", "dsh-mobile-control__panel");
|
|
@@ -418,6 +481,17 @@ window.__ModuleLoader__.load({
|
|
|
418
481
|
close.type = "button";
|
|
419
482
|
close.textContent = "×";
|
|
420
483
|
close.setAttribute("aria-label", "收起移动访问");
|
|
484
|
+
const switcher = element("div", "dsh-mobile-control__switcher");
|
|
485
|
+
const lanTab = element("button", "dsh-mobile-control__tab is-active");
|
|
486
|
+
lanTab.type = "button";
|
|
487
|
+
lanTab.textContent = "局域网";
|
|
488
|
+
const remoteTab = element("button", "dsh-mobile-control__tab");
|
|
489
|
+
remoteTab.type = "button";
|
|
490
|
+
remoteTab.textContent = "远程";
|
|
491
|
+
lanTab.setAttribute("aria-pressed", "true");
|
|
492
|
+
remoteTab.setAttribute("aria-pressed", "false");
|
|
493
|
+
switcher.append(lanTab, remoteTab);
|
|
494
|
+
const lanView = element("div", "dsh-mobile-control__view");
|
|
421
495
|
const access = element("div", "dsh-mobile-control__access");
|
|
422
496
|
access.hidden = true;
|
|
423
497
|
const accessLabel = element("span", "dsh-mobile-control__access-label");
|
|
@@ -451,13 +525,269 @@ window.__ModuleLoader__.load({
|
|
|
451
525
|
manageRow.append(manageDevices, resetAll);
|
|
452
526
|
const devicePanel = element("div", "dsh-mobile-control__devices");
|
|
453
527
|
devicePanel.hidden = true;
|
|
528
|
+
const remoteView = element("div", "dsh-mobile-control__view is-remote");
|
|
529
|
+
remoteView.hidden = true;
|
|
530
|
+
const remoteIntro = element("p", "dsh-mobile-control__intro");
|
|
531
|
+
remoteIntro.textContent = "选择更适合你的远程通道。切换或关闭远程访问不会影响局域网。";
|
|
532
|
+
const providerSection = element("section", "dsh-mobile-control__provider-section");
|
|
533
|
+
const providerHeading = element("h3", "dsh-mobile-control__section-title");
|
|
534
|
+
providerHeading.textContent = "选择连接方式";
|
|
535
|
+
const providerInfo = element("div", "dsh-mobile-control__provider-info");
|
|
536
|
+
const providerInfoButton = element("button", "dsh-mobile-control__provider-info-button");
|
|
537
|
+
providerInfoButton.type = "button";
|
|
538
|
+
providerInfoButton.setAttribute("aria-label", "查看远程连接安全与网络说明");
|
|
539
|
+
providerInfoButton.setAttribute("aria-expanded", "false");
|
|
540
|
+
providerInfoButton.setAttribute("aria-controls", "dsh-mobile-provider-info");
|
|
541
|
+
providerInfoButton.setAttribute("aria-describedby", "dsh-mobile-provider-info");
|
|
542
|
+
const providerInfoGlyph = element("span", "dsh-mobile-control__provider-info-glyph");
|
|
543
|
+
providerInfoGlyph.textContent = "i";
|
|
544
|
+
providerInfoGlyph.setAttribute("aria-hidden", "true");
|
|
545
|
+
const providerInfoPopover = element("div", "dsh-mobile-control__provider-info-popover");
|
|
546
|
+
providerInfoPopover.id = "dsh-mobile-provider-info";
|
|
547
|
+
providerInfoPopover.setAttribute("role", "tooltip");
|
|
548
|
+
providerInfoPopover.hidden = true;
|
|
549
|
+
const providerInfoTitle = element("strong");
|
|
550
|
+
providerInfoTitle.textContent = "你始终可以放心";
|
|
551
|
+
const providerInfoText = element("span");
|
|
552
|
+
providerInfoText.textContent = "只有已配对设备能进入 DSH。cpolar 按需安装并可彻底清理;Tailscale 在中国大陆网络下可能连接缓慢、中断或无法使用,国内网络建议优先尝试 cpolar。";
|
|
553
|
+
providerInfoButton.append(providerInfoGlyph);
|
|
554
|
+
providerInfoPopover.append(providerInfoTitle, providerInfoText);
|
|
555
|
+
providerInfo.append(providerInfoButton, providerInfoPopover);
|
|
556
|
+
const providerChoices = element("div", "dsh-mobile-control__provider-choices");
|
|
557
|
+
providerChoices.setAttribute("role", "radiogroup");
|
|
558
|
+
providerChoices.setAttribute("aria-label", "远程连接方式");
|
|
559
|
+
const tailscaleChoice = element("button", "dsh-mobile-control__provider");
|
|
560
|
+
tailscaleChoice.type = "button";
|
|
561
|
+
tailscaleChoice.setAttribute("role", "radio");
|
|
562
|
+
tailscaleChoice.setAttribute("aria-checked", "true");
|
|
563
|
+
const tailscaleChoiceTop = element("span", "dsh-mobile-control__provider-top");
|
|
564
|
+
const tailscaleChoiceName = element("strong");
|
|
565
|
+
tailscaleChoiceName.textContent = "Tailscale Funnel";
|
|
566
|
+
const tailscaleChoiceBadge = element("span", "dsh-mobile-control__provider-badge");
|
|
567
|
+
tailscaleChoiceBadge.textContent = "内置";
|
|
568
|
+
const tailscaleChoiceDescription = element("span", "dsh-mobile-control__provider-description");
|
|
569
|
+
tailscaleChoiceDescription.textContent = "覆盖更广;中国大陆网络可能不稳定,首次需登录并允许 Funnel。";
|
|
570
|
+
tailscaleChoiceTop.append(tailscaleChoiceName, tailscaleChoiceBadge);
|
|
571
|
+
tailscaleChoice.append(tailscaleChoiceTop, tailscaleChoiceDescription);
|
|
572
|
+
const cpolarChoice = element("button", "dsh-mobile-control__provider");
|
|
573
|
+
cpolarChoice.type = "button";
|
|
574
|
+
cpolarChoice.setAttribute("role", "radio");
|
|
575
|
+
cpolarChoice.setAttribute("aria-checked", "false");
|
|
576
|
+
const cpolarChoiceTop = element("span", "dsh-mobile-control__provider-top");
|
|
577
|
+
const cpolarChoiceName = element("strong");
|
|
578
|
+
cpolarChoiceName.textContent = "cpolar";
|
|
579
|
+
const cpolarChoiceBadge = element("span", "dsh-mobile-control__provider-badge is-cpolar");
|
|
580
|
+
cpolarChoiceBadge.textContent = "国内网络优先";
|
|
581
|
+
const cpolarChoiceDescription = element("span", "dsh-mobile-control__provider-description");
|
|
582
|
+
cpolarChoiceDescription.textContent = "按需安装官方组件,适合国内网络环境。";
|
|
583
|
+
cpolarChoiceTop.append(cpolarChoiceName, cpolarChoiceBadge);
|
|
584
|
+
cpolarChoice.append(cpolarChoiceTop, cpolarChoiceDescription);
|
|
585
|
+
providerChoices.append(tailscaleChoice, cpolarChoice);
|
|
586
|
+
providerSection.append(providerHeading, providerInfo, providerChoices);
|
|
587
|
+
const cpolarSetup = element("section", "dsh-mobile-control__cpolar-setup");
|
|
588
|
+
cpolarSetup.hidden = true;
|
|
589
|
+
const cpolarSetupTitle = element("h3", "dsh-mobile-control__section-title");
|
|
590
|
+
cpolarSetupTitle.textContent = "准备 cpolar";
|
|
591
|
+
const cpolarComponentStatus = element("p", "dsh-mobile-control__component-status");
|
|
592
|
+
cpolarComponentStatus.textContent = "正在检查组件…";
|
|
593
|
+
const cpolarInstall = element("button", "dsh-mobile-control__primary");
|
|
594
|
+
cpolarInstall.type = "button";
|
|
595
|
+
cpolarInstall.textContent = "安装官方组件";
|
|
596
|
+
const cpolarAccount = element("div", "dsh-mobile-control__cpolar-account");
|
|
597
|
+
cpolarAccount.hidden = true;
|
|
598
|
+
const cpolarAccountText = element("p", "dsh-mobile-control__component-note");
|
|
599
|
+
cpolarAccountText.textContent = "登录 cpolar 官网后复制 Authtoken。令牌只保存在本机插件私有目录,不会显示在页面或日志中。";
|
|
600
|
+
const cpolarAccountLinks = element("div", "dsh-mobile-control__link-row");
|
|
601
|
+
const cpolarSignup = element("a", "dsh-mobile-control__text-link");
|
|
602
|
+
cpolarSignup.href = "https://dashboard.cpolar.com/signup";
|
|
603
|
+
cpolarSignup.target = "_blank";
|
|
604
|
+
cpolarSignup.rel = "noopener noreferrer";
|
|
605
|
+
cpolarSignup.textContent = "注册 cpolar";
|
|
606
|
+
const cpolarDashboard = element("a", "dsh-mobile-control__text-link");
|
|
607
|
+
cpolarDashboard.href = "https://dashboard.cpolar.com/auth";
|
|
608
|
+
cpolarDashboard.target = "_blank";
|
|
609
|
+
cpolarDashboard.rel = "noopener noreferrer";
|
|
610
|
+
cpolarDashboard.textContent = "打开控制台获取令牌";
|
|
611
|
+
cpolarAccountLinks.append(cpolarSignup, cpolarDashboard);
|
|
612
|
+
const cpolarTokenLabel = element("label", "dsh-mobile-control__token-label");
|
|
613
|
+
cpolarTokenLabel.textContent = "Authtoken";
|
|
614
|
+
const cpolarToken = element("input", "dsh-mobile-control__token");
|
|
615
|
+
cpolarToken.type = "password";
|
|
616
|
+
cpolarToken.autocomplete = "off";
|
|
617
|
+
cpolarToken.spellcheck = false;
|
|
618
|
+
cpolarToken.placeholder = "粘贴 cpolar Authtoken";
|
|
619
|
+
cpolarTokenLabel.append(cpolarToken);
|
|
620
|
+
const cpolarConfigure = element("button", "dsh-mobile-control__primary dsh-mobile-control__cpolar-connect");
|
|
621
|
+
cpolarConfigure.type = "button";
|
|
622
|
+
cpolarConfigure.textContent = "保存并连接";
|
|
623
|
+
cpolarAccount.append(cpolarAccountText, cpolarAccountLinks, cpolarTokenLabel, cpolarConfigure);
|
|
624
|
+
const cpolarDetails = element("details", "dsh-mobile-control__details");
|
|
625
|
+
const cpolarDetailsSummary = element("summary");
|
|
626
|
+
cpolarDetailsSummary.textContent = "组件来源与清理说明";
|
|
627
|
+
const cpolarDetailsBody = element("div", "dsh-mobile-control__details-body");
|
|
628
|
+
const cpolarDetailsText = element("p");
|
|
629
|
+
cpolarDetailsText.textContent = "仅在你点击安装后从 cpolar 官网下载并校验固定版本。不会写入系统服务、开机启动、注册表或 PATH。";
|
|
630
|
+
const cpolarStorage = element("code", "dsh-mobile-control__storage");
|
|
631
|
+
cpolarStorage.textContent = "插件私有目录";
|
|
632
|
+
const cpolarOfficial = element("a", "dsh-mobile-control__text-link");
|
|
633
|
+
cpolarOfficial.href = "https://www.cpolar.com/download";
|
|
634
|
+
cpolarOfficial.target = "_blank";
|
|
635
|
+
cpolarOfficial.rel = "noopener noreferrer";
|
|
636
|
+
cpolarOfficial.textContent = "官方下载安装页";
|
|
637
|
+
const cpolarTerms = element("a", "dsh-mobile-control__text-link");
|
|
638
|
+
cpolarTerms.href = "https://www.cpolar.com/tos";
|
|
639
|
+
cpolarTerms.target = "_blank";
|
|
640
|
+
cpolarTerms.rel = "noopener noreferrer";
|
|
641
|
+
cpolarTerms.textContent = "服务条款";
|
|
642
|
+
const cpolarPurge = element("button", "dsh-mobile-control__danger");
|
|
643
|
+
cpolarPurge.type = "button";
|
|
644
|
+
cpolarPurge.textContent = "彻底移除 cpolar 组件与配置";
|
|
645
|
+
cpolarDetailsBody.append(cpolarDetailsText, cpolarStorage, cpolarOfficial, cpolarTerms, cpolarPurge);
|
|
646
|
+
cpolarDetails.append(cpolarDetailsSummary, cpolarDetailsBody);
|
|
647
|
+
cpolarSetup.append(cpolarSetupTitle, cpolarComponentStatus, cpolarInstall, cpolarAccount, cpolarDetails);
|
|
648
|
+
const tailscaleInfo = element("details", "dsh-mobile-control__details");
|
|
649
|
+
const tailscaleInfoSummary = element("summary");
|
|
650
|
+
tailscaleInfoSummary.textContent = "Tailscale 使用说明";
|
|
651
|
+
const tailscaleInfoBody = element("div", "dsh-mobile-control__details-body");
|
|
652
|
+
const tailscaleInfoText = element("p");
|
|
653
|
+
tailscaleInfoText.textContent = "运行组件已随插件提供。首次连接会打开 Tailscale 官方登录和 Funnel 授权页;插件不会接触你的账号密码。";
|
|
654
|
+
tailscaleInfoBody.append(tailscaleInfoText);
|
|
655
|
+
tailscaleInfo.append(tailscaleInfoSummary, tailscaleInfoBody);
|
|
656
|
+
const remoteAccess = element("div", "dsh-mobile-control__access");
|
|
657
|
+
remoteAccess.hidden = true;
|
|
658
|
+
const remoteAccessLabel = element("span", "dsh-mobile-control__access-label");
|
|
659
|
+
remoteAccessLabel.textContent = "远程地址";
|
|
660
|
+
const remoteAccessLink = element("a", "dsh-mobile-control__access-link");
|
|
661
|
+
remoteAccessLink.target = "_blank";
|
|
662
|
+
remoteAccessLink.rel = "noreferrer";
|
|
663
|
+
remoteAccess.append(remoteAccessLabel, remoteAccessLink);
|
|
664
|
+
const remoteQr = element("div", "dsh-mobile-control__qr");
|
|
665
|
+
remoteQr.hidden = true;
|
|
666
|
+
const remoteStatus = element("p", "dsh-mobile-control__status");
|
|
667
|
+
remoteStatus.textContent = "正在读取远程状态…";
|
|
668
|
+
remoteStatus.setAttribute("aria-live", "polite");
|
|
669
|
+
const remoteGuide = element("section", "dsh-mobile-control__guide");
|
|
670
|
+
remoteGuide.hidden = true;
|
|
671
|
+
remoteGuide.setAttribute("aria-label", "Tailscale Funnel 启用步骤");
|
|
672
|
+
const remoteGuideTitle = element("h3", "dsh-mobile-control__guide-title");
|
|
673
|
+
remoteGuideTitle.textContent = "远程访问设置 · 第 2 步";
|
|
674
|
+
const remoteGuideSummary = element("p", "dsh-mobile-control__guide-summary");
|
|
675
|
+
remoteGuideSummary.textContent = "Tailscale 登录已完成。还需为这台电脑允许 Funnel,官方页面会同时启用 HTTPS。";
|
|
676
|
+
const remoteGuideSteps = element("ol", "dsh-mobile-control__guide-steps");
|
|
677
|
+
for (const text of [
|
|
678
|
+
"打开当前节点的 Tailscale 官方授权页。",
|
|
679
|
+
"确认启用 Funnel;无需再次登录 DSH。",
|
|
680
|
+
"返回 DSH,插件会自动检查并建立连接。"
|
|
681
|
+
]) {
|
|
682
|
+
const item = element("li");
|
|
683
|
+
item.textContent = text;
|
|
684
|
+
remoteGuideSteps.append(item);
|
|
685
|
+
}
|
|
686
|
+
const remoteGuideNote = element("p", "dsh-mobile-control__guide-note");
|
|
687
|
+
remoteGuideNote.textContent = "需要使用 Owner、Admin 或 Network admin 账号。";
|
|
688
|
+
const remoteGuideActions = element("div", "dsh-mobile-control__guide-actions");
|
|
689
|
+
const remoteSetup = element("button", "dsh-mobile-control__primary");
|
|
690
|
+
remoteSetup.type = "button";
|
|
691
|
+
remoteSetup.textContent = "继续完成 Funnel 授权";
|
|
692
|
+
const remoteSetupRetry = element("button", "dsh-mobile-control__secondary");
|
|
693
|
+
remoteSetupRetry.type = "button";
|
|
694
|
+
remoteSetupRetry.textContent = "已完成,立即重试";
|
|
695
|
+
remoteGuideActions.append(remoteSetup, remoteSetupRetry);
|
|
696
|
+
remoteGuide.append(remoteGuideTitle, remoteGuideSummary, remoteGuideSteps, remoteGuideNote, remoteGuideActions);
|
|
697
|
+
const remoteActions = element("div", "dsh-mobile-control__actions");
|
|
698
|
+
const remoteToggle = element("button", "dsh-mobile-control__primary");
|
|
699
|
+
remoteToggle.type = "button";
|
|
700
|
+
remoteToggle.textContent = "启用远程访问";
|
|
701
|
+
const remoteLogin = element("button", "dsh-mobile-control__primary");
|
|
702
|
+
remoteLogin.type = "button";
|
|
703
|
+
remoteLogin.textContent = "继续登录";
|
|
704
|
+
remoteLogin.hidden = true;
|
|
705
|
+
const remoteReconnect = element("button", "dsh-mobile-control__secondary");
|
|
706
|
+
remoteReconnect.type = "button";
|
|
707
|
+
remoteReconnect.textContent = "重新连接";
|
|
708
|
+
remoteReconnect.hidden = true;
|
|
709
|
+
const remotePair = element("button", "dsh-mobile-control__secondary");
|
|
710
|
+
remotePair.type = "button";
|
|
711
|
+
remotePair.textContent = "生成远程配对二维码";
|
|
712
|
+
remotePair.disabled = true;
|
|
713
|
+
remoteActions.append(remoteToggle, remoteLogin, remoteReconnect, remotePair);
|
|
714
|
+
const remoteManageRow = element("div", "dsh-mobile-control__manage-row");
|
|
715
|
+
const remoteDevices = element("button", "dsh-mobile-control__manage");
|
|
716
|
+
remoteDevices.type = "button";
|
|
717
|
+
remoteDevices.textContent = "管理远程设备";
|
|
718
|
+
remoteDevices.disabled = true;
|
|
719
|
+
const remoteReset = element("button", "dsh-mobile-control__manage");
|
|
720
|
+
remoteReset.type = "button";
|
|
721
|
+
remoteReset.textContent = "退出并清除远程登录";
|
|
722
|
+
remoteManageRow.append(remoteDevices, remoteReset);
|
|
723
|
+
const remoteDevicePanel = element("div", "dsh-mobile-control__devices");
|
|
724
|
+
remoteDevicePanel.hidden = true;
|
|
454
725
|
header.append(title, close);
|
|
455
726
|
actions.append(toggle, pair, linkPair);
|
|
456
|
-
|
|
727
|
+
lanView.append(access, qrBox, status, extensionStatus, actions, manageRow, devicePanel);
|
|
728
|
+
remoteView.append(remoteIntro, providerSection, cpolarSetup, tailscaleInfo, remoteAccess, remoteQr, remoteStatus, remoteGuide, remoteActions, remoteManageRow, remoteDevicePanel);
|
|
729
|
+
panel.append(header, switcher, lanView, remoteView);
|
|
457
730
|
root.append(panel);
|
|
458
731
|
document.body.append(root);
|
|
459
732
|
let running = false;
|
|
460
733
|
let origin = "";
|
|
734
|
+
let remoteRunning = false;
|
|
735
|
+
let remoteReady = false;
|
|
736
|
+
let remoteProvider = "tailscale";
|
|
737
|
+
let remoteLoginUrl = "";
|
|
738
|
+
let remoteSetupUrl = "";
|
|
739
|
+
let remoteSetupPending = false;
|
|
740
|
+
let remoteSetupOpenedAt = 0;
|
|
741
|
+
let remoteReconnectBusy = false;
|
|
742
|
+
let remoteProviderBusy = false;
|
|
743
|
+
let cpolarInstalled = false;
|
|
744
|
+
let cpolarConfigured = false;
|
|
745
|
+
let providerInfoPinned = false;
|
|
746
|
+
let providerInfoHovered = false;
|
|
747
|
+
const syncProviderInfo = () => {
|
|
748
|
+
const open = providerInfoPinned || providerInfoHovered || providerInfo.contains(document.activeElement);
|
|
749
|
+
providerInfoPopover.hidden = !open;
|
|
750
|
+
providerInfoButton.setAttribute("aria-expanded", String(open));
|
|
751
|
+
};
|
|
752
|
+
providerInfo.addEventListener("pointerenter", () => {
|
|
753
|
+
providerInfoHovered = true;
|
|
754
|
+
syncProviderInfo();
|
|
755
|
+
});
|
|
756
|
+
providerInfo.addEventListener("pointerleave", () => {
|
|
757
|
+
providerInfoHovered = false;
|
|
758
|
+
syncProviderInfo();
|
|
759
|
+
});
|
|
760
|
+
providerInfo.addEventListener("focusin", syncProviderInfo);
|
|
761
|
+
providerInfo.addEventListener("focusout", () => {
|
|
762
|
+
window.setTimeout(syncProviderInfo, 0);
|
|
763
|
+
});
|
|
764
|
+
providerInfoButton.addEventListener("click", () => {
|
|
765
|
+
providerInfoPinned = !providerInfoPinned;
|
|
766
|
+
syncProviderInfo();
|
|
767
|
+
});
|
|
768
|
+
providerInfoButton.addEventListener("keydown", (event) => {
|
|
769
|
+
if (event.key !== "Escape") return;
|
|
770
|
+
providerInfoPinned = false;
|
|
771
|
+
providerInfoHovered = false;
|
|
772
|
+
providerInfoPopover.hidden = true;
|
|
773
|
+
providerInfoButton.setAttribute("aria-expanded", "false");
|
|
774
|
+
});
|
|
775
|
+
const selectView = (remote) => {
|
|
776
|
+
lanView.hidden = remote;
|
|
777
|
+
remoteView.hidden = !remote;
|
|
778
|
+
lanTab.classList.toggle("is-active", !remote);
|
|
779
|
+
remoteTab.classList.toggle("is-active", remote);
|
|
780
|
+
lanTab.setAttribute("aria-pressed", String(!remote));
|
|
781
|
+
remoteTab.setAttribute("aria-pressed", String(remote));
|
|
782
|
+
title.textContent = remote ? "远程访问" : "局域网访问";
|
|
783
|
+
};
|
|
784
|
+
lanTab.addEventListener("click", () => {
|
|
785
|
+
selectView(false);
|
|
786
|
+
});
|
|
787
|
+
remoteTab.addEventListener("click", () => {
|
|
788
|
+
selectView(true);
|
|
789
|
+
loadRemote();
|
|
790
|
+
});
|
|
461
791
|
const setOpen = (open) => {
|
|
462
792
|
panel.hidden = !open;
|
|
463
793
|
for (const trigger of document.querySelectorAll(".dsh-mobile-control__trigger")) trigger.setAttribute("aria-expanded", String(open));
|
|
@@ -470,7 +800,7 @@ window.__ModuleLoader__.load({
|
|
|
470
800
|
accessLink.textContent = origin;
|
|
471
801
|
accessLink.title = origin;
|
|
472
802
|
status.classList.toggle("is-running", running);
|
|
473
|
-
status.textContent = running ? "
|
|
803
|
+
status.textContent = running ? "局域网访问已开启。" : "局域网访问已关闭。";
|
|
474
804
|
const extensionData = data.extensions;
|
|
475
805
|
if (extensionData !== null && typeof extensionData === "object") {
|
|
476
806
|
const loaded = typeof extensionData.loaded === "number" ? extensionData.loaded : 0;
|
|
@@ -479,16 +809,16 @@ window.__ModuleLoader__.load({
|
|
|
479
809
|
extensionStatus.textContent = failed === 0 ? `扩展:${String(loaded)} 个已加载` : `扩展:${String(loaded)} 个已加载,${String(failed)} 个加载失败`;
|
|
480
810
|
} else extensionStatus.hidden = true;
|
|
481
811
|
if (!running) qrBox.hidden = true;
|
|
482
|
-
toggle.textContent = running ? "
|
|
812
|
+
toggle.textContent = running ? "关闭局域网访问" : "开启局域网访问";
|
|
483
813
|
pair.disabled = !running;
|
|
484
814
|
linkPair.disabled = !running;
|
|
485
815
|
manageDevices.disabled = !running;
|
|
486
816
|
resetAll.disabled = !running;
|
|
487
817
|
};
|
|
488
|
-
const showQr = (svg) => {
|
|
489
|
-
|
|
818
|
+
const showQr = (svg, target = qrBox) => {
|
|
819
|
+
target.replaceChildren();
|
|
490
820
|
if (svg === "") {
|
|
491
|
-
|
|
821
|
+
target.hidden = true;
|
|
492
822
|
return;
|
|
493
823
|
}
|
|
494
824
|
const image = element("img");
|
|
@@ -496,11 +826,11 @@ window.__ModuleLoader__.load({
|
|
|
496
826
|
image.width = 176;
|
|
497
827
|
image.height = 176;
|
|
498
828
|
image.src = `data:image/svg+xml;base64,${btoa(svg)}`;
|
|
499
|
-
|
|
500
|
-
|
|
829
|
+
target.hidden = false;
|
|
830
|
+
target.append(image);
|
|
501
831
|
};
|
|
502
832
|
const openPairing = (target) => {
|
|
503
|
-
requestJson("/api/mobile-access/pairing/open", {
|
|
833
|
+
requestJson("/api/mobile-access/lan/pairing/open", {
|
|
504
834
|
method: "POST",
|
|
505
835
|
body: "{}"
|
|
506
836
|
}).then(async (data) => {
|
|
@@ -526,7 +856,7 @@ window.__ModuleLoader__.load({
|
|
|
526
856
|
};
|
|
527
857
|
toggle.addEventListener("click", () => {
|
|
528
858
|
toggle.disabled = true;
|
|
529
|
-
requestJson("/api/mobile-access/control", {
|
|
859
|
+
requestJson("/api/mobile-access/lan/control", {
|
|
530
860
|
method: "POST",
|
|
531
861
|
body: JSON.stringify({ running: !running })
|
|
532
862
|
}).then(render, (error) => {
|
|
@@ -556,7 +886,7 @@ window.__ModuleLoader__.load({
|
|
|
556
886
|
revoke.textContent = "撤销";
|
|
557
887
|
const id = typeof device.id === "string" ? device.id : "";
|
|
558
888
|
revoke.addEventListener("click", () => {
|
|
559
|
-
requestJson("/api/mobile-access/devices/revoke", {
|
|
889
|
+
requestJson("/api/mobile-access/lan/devices/revoke", {
|
|
560
890
|
method: "POST",
|
|
561
891
|
body: JSON.stringify({ deviceId: id })
|
|
562
892
|
}).then(loadDevices, (error) => {
|
|
@@ -568,7 +898,7 @@ window.__ModuleLoader__.load({
|
|
|
568
898
|
}
|
|
569
899
|
};
|
|
570
900
|
const loadDevices = () => {
|
|
571
|
-
requestJson("/api/mobile-access/devices").then(renderDevices, (error) => {
|
|
901
|
+
requestJson("/api/mobile-access/lan/devices").then(renderDevices, (error) => {
|
|
572
902
|
status.textContent = String(error);
|
|
573
903
|
});
|
|
574
904
|
};
|
|
@@ -579,13 +909,315 @@ window.__ModuleLoader__.load({
|
|
|
579
909
|
});
|
|
580
910
|
resetAll.addEventListener("click", () => {
|
|
581
911
|
if (!window.confirm("确定要移除所有配对设备吗?此操作会立即终止已连接设备。")) return;
|
|
582
|
-
requestJson("/api/mobile-access/devices/reset", {
|
|
912
|
+
requestJson("/api/mobile-access/lan/devices/reset", {
|
|
583
913
|
method: "POST",
|
|
584
914
|
body: JSON.stringify({ confirm: true })
|
|
585
915
|
}).then(loadDevices, (error) => {
|
|
586
916
|
status.textContent = String(error);
|
|
587
917
|
});
|
|
588
918
|
});
|
|
919
|
+
const renderRemote = (data) => {
|
|
920
|
+
remoteRunning = data.running === true;
|
|
921
|
+
remoteProvider = data.provider === "cpolar" ? "cpolar" : "tailscale";
|
|
922
|
+
const cpolar = remoteProvider === "cpolar";
|
|
923
|
+
tailscaleChoice.classList.toggle("is-selected", !cpolar);
|
|
924
|
+
cpolarChoice.classList.toggle("is-selected", cpolar);
|
|
925
|
+
tailscaleChoice.setAttribute("aria-checked", String(!cpolar));
|
|
926
|
+
cpolarChoice.setAttribute("aria-checked", String(cpolar));
|
|
927
|
+
tailscaleChoice.disabled = remoteProviderBusy;
|
|
928
|
+
cpolarChoice.disabled = remoteProviderBusy;
|
|
929
|
+
cpolarSetup.hidden = !cpolar;
|
|
930
|
+
tailscaleInfo.hidden = cpolar;
|
|
931
|
+
remoteReset.textContent = cpolar ? "关闭并清除远程设备" : "退出并清除远程登录";
|
|
932
|
+
const providers = data.providers !== null && typeof data.providers === "object" ? data.providers : {};
|
|
933
|
+
const cpolarProvider = providers.cpolar !== null && typeof providers.cpolar === "object" ? providers.cpolar : {};
|
|
934
|
+
const component = cpolarProvider.component !== null && typeof cpolarProvider.component === "object" ? cpolarProvider.component : {};
|
|
935
|
+
cpolarInstalled = component.installed === true;
|
|
936
|
+
cpolarConfigured = component.configured === true;
|
|
937
|
+
cpolarChoiceBadge.textContent = cpolarConfigured ? "已就绪" : cpolarInstalled ? "已安装" : "国内网络优先";
|
|
938
|
+
const cpolarSupported = component.supported !== false;
|
|
939
|
+
const componentVersion = typeof component.version === "string" ? component.version : "";
|
|
940
|
+
const componentDownloadBytes = typeof component.downloadBytes === "number" ? component.downloadBytes : 0;
|
|
941
|
+
const componentStorage = typeof component.storagePath === "string" ? component.storagePath : "DSH Mobile 插件私有目录";
|
|
942
|
+
cpolarStorage.textContent = componentStorage;
|
|
943
|
+
cpolarStorage.title = componentStorage;
|
|
944
|
+
cpolarInstall.hidden = cpolarInstalled || !cpolarSupported;
|
|
945
|
+
cpolarInstall.textContent = componentDownloadBytes > 0 ? `安装官方组件 · ${(componentDownloadBytes / 1024 / 1024).toFixed(1)} MB` : "安装官方组件";
|
|
946
|
+
cpolarInstall.disabled = remoteProviderBusy;
|
|
947
|
+
cpolarAccount.hidden = !cpolarInstalled || cpolarConfigured;
|
|
948
|
+
cpolarConfigure.disabled = remoteProviderBusy;
|
|
949
|
+
cpolarPurge.hidden = !cpolarInstalled && !cpolarConfigured;
|
|
950
|
+
cpolarComponentStatus.textContent = !cpolarSupported ? "当前仅支持 Windows x64。你仍可选择内置的 Tailscale Funnel。" : !cpolarInstalled ? "尚未安装。只有点击下方按钮后,才会从 cpolar 官网下载固定版本。" : !cpolarConfigured ? `官方组件 ${componentVersion} 已校验,下一步只需保存账号令牌。` : `官方组件 ${componentVersion} 与本机账号配置已就绪。`;
|
|
951
|
+
const state = typeof data.state === "string" ? data.state : "error";
|
|
952
|
+
const errorCode = typeof data.errorCode === "string" ? data.errorCode : "";
|
|
953
|
+
const remoteOrigin = typeof data.origin === "string" ? data.origin : "";
|
|
954
|
+
remoteLoginUrl = typeof data.loginUrl === "string" ? data.loginUrl : "";
|
|
955
|
+
const candidateSetupUrl = cpolar ? "" : officialFunnelSetupUrl(data.setupUrl);
|
|
956
|
+
remoteSetupUrl = candidateSetupUrl !== "" ? candidateSetupUrl : {
|
|
957
|
+
funnel_permission_required: "https://tailscale.com/s/no-funnel",
|
|
958
|
+
funnel_https_required: "https://tailscale.com/s/https",
|
|
959
|
+
funnel_start_failed: "https://tailscale.com/s/no-funnel"
|
|
960
|
+
}[errorCode] ?? "";
|
|
961
|
+
const needsFunnelSetup = state === "error" && remoteSetupUrl !== "";
|
|
962
|
+
remoteReady = remoteRunning && state === "ready" && remoteOrigin !== "";
|
|
963
|
+
remoteAccess.hidden = !remoteReady;
|
|
964
|
+
remoteAccessLink.href = remoteOrigin;
|
|
965
|
+
remoteAccessLink.textContent = remoteOrigin;
|
|
966
|
+
remoteAccessLink.title = remoteOrigin;
|
|
967
|
+
remoteStatus.classList.toggle("is-running", remoteReady);
|
|
968
|
+
const labels = {
|
|
969
|
+
off: "远程访问未启用。局域网访问不受影响。",
|
|
970
|
+
unavailable: cpolar ? "cpolar 尚未安装或未完成本机账号配置。" : "当前电脑缺少 Funnel 运行组件,请重新安装完整插件包。",
|
|
971
|
+
starting: cpolar ? "正在连接 cpolar 国内节点…" : "正在启动 Tailscale 安全通道…",
|
|
972
|
+
"needs-login": "需要在浏览器完成一次 Tailscale 登录。插件不会读取你的密码。",
|
|
973
|
+
connecting: cpolar ? "公网地址已分配,正在启动 DSH 认证网关…" : "登录完成,正在建立公开 HTTPS 地址…",
|
|
974
|
+
ready: "远程访问已就绪。只有已配对设备可以进入 DSH。",
|
|
975
|
+
error: "远程连接未建立。可重新连接,局域网访问仍可正常使用。"
|
|
976
|
+
};
|
|
977
|
+
remoteStatus.textContent = remoteSetupPending && needsFunnelSetup ? "Tailscale 官方页面已打开。完成启用后返回 DSH,这里会自动重新连接。" : state === "error" ? {
|
|
978
|
+
funnel_permission_required: "登录已完成。请继续授权 Funnel,完成后会自动建立远程连接。",
|
|
979
|
+
funnel_https_required: "登录已完成。请继续授权 Funnel,官方页面会同时启用 HTTPS。",
|
|
980
|
+
funnel_start_failed: "登录已完成。请继续完成 Tailscale Funnel 的首次授权。",
|
|
981
|
+
tailscale_dns_missing: "Tailscale 暂未提供远程地址。请重新连接并确认已完成登录。",
|
|
982
|
+
gateway_start_failed: "远程网关启动失败。请重新连接,局域网访问不受影响。",
|
|
983
|
+
control_channel_failed: "远程组件连接中断。请重新连接。",
|
|
984
|
+
cpolar_component_missing: "cpolar 官方组件尚未安装。请先完成上方准备步骤。",
|
|
985
|
+
cpolar_component_invalid: "cpolar 组件校验失败。请彻底移除后重新安装。",
|
|
986
|
+
cpolar_config_missing: "cpolar 尚未保存账号令牌。请先完成上方准备步骤。",
|
|
987
|
+
cpolar_config_invalid: "cpolar 本机配置无效。请重新保存账号令牌。",
|
|
988
|
+
cpolar_port_unavailable: "无法分配本机远程网关端口,请重试。",
|
|
989
|
+
cpolar_launch_failed: "cpolar 客户端未能启动。",
|
|
990
|
+
cpolar_start_timeout: "连接 cpolar 国内节点超时,请重新连接。",
|
|
991
|
+
cpolar_stopped: "cpolar 连接已停止。",
|
|
992
|
+
cpolar_exited: "cpolar 连接意外退出,请重新连接。",
|
|
993
|
+
cpolar_invalid_output: "cpolar 返回了无法识别的状态。",
|
|
994
|
+
cpolar_invalid_origin: "cpolar 返回的公网地址未通过校验。"
|
|
995
|
+
}[errorCode] ?? labels.error : labels[state] ?? labels.error;
|
|
996
|
+
remoteGuide.hidden = !needsFunnelSetup;
|
|
997
|
+
remoteSetup.disabled = remoteSetupUrl === "" || remoteReconnectBusy;
|
|
998
|
+
remoteSetupRetry.disabled = remoteReconnectBusy;
|
|
999
|
+
remoteToggle.textContent = remoteRunning ? "关闭远程访问" : "启用远程访问";
|
|
1000
|
+
remoteToggle.disabled = remoteProviderBusy || cpolar && (!cpolarInstalled || !cpolarConfigured);
|
|
1001
|
+
remoteLogin.hidden = cpolar || state !== "needs-login" || remoteLoginUrl === "";
|
|
1002
|
+
remoteReconnect.hidden = needsFunnelSetup || state !== "error" && state !== "unavailable" || cpolar && (!cpolarInstalled || !cpolarConfigured);
|
|
1003
|
+
remoteActions.hidden = cpolar && (!cpolarInstalled || !cpolarConfigured);
|
|
1004
|
+
remotePair.disabled = !remoteReady;
|
|
1005
|
+
remoteDevices.disabled = !remoteReady;
|
|
1006
|
+
if (!remoteReady) remoteQr.hidden = true;
|
|
1007
|
+
if (!needsFunnelSetup) remoteSetupPending = false;
|
|
1008
|
+
};
|
|
1009
|
+
const loadRemote = () => {
|
|
1010
|
+
requestJson("/api/mobile-access/remote/control").then(renderRemote, (error) => {
|
|
1011
|
+
remoteStatus.textContent = String(error);
|
|
1012
|
+
});
|
|
1013
|
+
};
|
|
1014
|
+
const chooseRemoteProvider = (provider) => {
|
|
1015
|
+
if (remoteProviderBusy || provider === remoteProvider) return;
|
|
1016
|
+
if (remoteRunning && !window.confirm("切换连接方式会先关闭当前远程通道。局域网和配对设备不会受影响,是否继续?")) return;
|
|
1017
|
+
remoteProviderBusy = true;
|
|
1018
|
+
tailscaleChoice.disabled = true;
|
|
1019
|
+
cpolarChoice.disabled = true;
|
|
1020
|
+
remoteStatus.textContent = provider === "cpolar" ? "正在切换到 cpolar…" : "正在切换到 Tailscale Funnel…";
|
|
1021
|
+
requestJson("/api/mobile-access/remote/provider", {
|
|
1022
|
+
method: "POST",
|
|
1023
|
+
body: JSON.stringify({ provider })
|
|
1024
|
+
}).then(renderRemote, (error) => {
|
|
1025
|
+
remoteStatus.textContent = String(error);
|
|
1026
|
+
}).finally(() => {
|
|
1027
|
+
remoteProviderBusy = false;
|
|
1028
|
+
loadRemote();
|
|
1029
|
+
});
|
|
1030
|
+
};
|
|
1031
|
+
tailscaleChoice.addEventListener("click", () => {
|
|
1032
|
+
chooseRemoteProvider("tailscale");
|
|
1033
|
+
});
|
|
1034
|
+
cpolarChoice.addEventListener("click", () => {
|
|
1035
|
+
chooseRemoteProvider("cpolar");
|
|
1036
|
+
});
|
|
1037
|
+
cpolarInstall.addEventListener("click", () => {
|
|
1038
|
+
if (remoteProviderBusy) return;
|
|
1039
|
+
if (!window.confirm("将从 cpolar 官方网站下载并校验固定版本(约 7.3 MB),仅解压到 DSH Mobile 私有目录。不会安装系统服务、写入 PATH/注册表或设置开机启动。是否继续?")) return;
|
|
1040
|
+
remoteProviderBusy = true;
|
|
1041
|
+
cpolarInstall.disabled = true;
|
|
1042
|
+
cpolarInstall.textContent = "正在下载并校验…";
|
|
1043
|
+
remoteStatus.textContent = "正在安装 cpolar 官方组件。完成前请保持 DSH 运行。";
|
|
1044
|
+
requestJson("/api/mobile-access/remote/cpolar/component/install", {
|
|
1045
|
+
method: "POST",
|
|
1046
|
+
body: JSON.stringify({ confirm: true })
|
|
1047
|
+
}).then(renderRemote, (error) => {
|
|
1048
|
+
remoteStatus.textContent = `组件安装失败:${String(error)}`;
|
|
1049
|
+
}).finally(() => {
|
|
1050
|
+
remoteProviderBusy = false;
|
|
1051
|
+
loadRemote();
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
cpolarConfigure.addEventListener("click", () => {
|
|
1055
|
+
if (remoteProviderBusy) return;
|
|
1056
|
+
const authtoken = cpolarToken.value.trim();
|
|
1057
|
+
if (authtoken.length < 20 || /\s/u.test(authtoken)) {
|
|
1058
|
+
remoteStatus.textContent = "请粘贴 cpolar 控制台提供的完整 Authtoken。";
|
|
1059
|
+
cpolarToken.focus();
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
remoteProviderBusy = true;
|
|
1063
|
+
cpolarConfigure.disabled = true;
|
|
1064
|
+
cpolarConfigure.setAttribute("aria-busy", "true");
|
|
1065
|
+
cpolarConfigure.textContent = "正在保存…";
|
|
1066
|
+
requestJson("/api/mobile-access/remote/cpolar/configure", {
|
|
1067
|
+
method: "POST",
|
|
1068
|
+
body: JSON.stringify({ authtoken })
|
|
1069
|
+
}).then(() => {
|
|
1070
|
+
cpolarToken.value = "";
|
|
1071
|
+
remoteStatus.textContent = "账号配置已保存,正在建立 cpolar 远程通道…";
|
|
1072
|
+
return requestJson("/api/mobile-access/remote/control", {
|
|
1073
|
+
method: "POST",
|
|
1074
|
+
body: JSON.stringify({ running: true })
|
|
1075
|
+
});
|
|
1076
|
+
}).then(renderRemote, (error) => {
|
|
1077
|
+
remoteStatus.textContent = `配置失败:${String(error)}`;
|
|
1078
|
+
}).finally(() => {
|
|
1079
|
+
remoteProviderBusy = false;
|
|
1080
|
+
cpolarConfigure.setAttribute("aria-busy", "false");
|
|
1081
|
+
cpolarConfigure.textContent = "保存并连接";
|
|
1082
|
+
loadRemote();
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
cpolarPurge.addEventListener("click", () => {
|
|
1086
|
+
if (remoteProviderBusy) return;
|
|
1087
|
+
if (!window.confirm("彻底移除 DSH Mobile 私有目录中的 cpolar 组件、令牌配置和运行日志?不会影响局域网、DSH 数据或系统中的其他程序。")) return;
|
|
1088
|
+
remoteProviderBusy = true;
|
|
1089
|
+
cpolarPurge.disabled = true;
|
|
1090
|
+
remoteStatus.textContent = "正在关闭通道并清理 DSH Mobile 管理的 cpolar 文件…";
|
|
1091
|
+
requestJson("/api/mobile-access/remote/cpolar/component/purge", {
|
|
1092
|
+
method: "POST",
|
|
1093
|
+
body: JSON.stringify({ confirm: true })
|
|
1094
|
+
}).then(renderRemote, (error) => {
|
|
1095
|
+
remoteStatus.textContent = `清理失败:${String(error)}`;
|
|
1096
|
+
}).finally(() => {
|
|
1097
|
+
remoteProviderBusy = false;
|
|
1098
|
+
cpolarPurge.disabled = false;
|
|
1099
|
+
loadRemote();
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
remoteToggle.addEventListener("click", () => {
|
|
1103
|
+
remoteToggle.disabled = true;
|
|
1104
|
+
requestJson("/api/mobile-access/remote/control", {
|
|
1105
|
+
method: "POST",
|
|
1106
|
+
body: JSON.stringify({ running: !remoteRunning })
|
|
1107
|
+
}).then(renderRemote, (error) => {
|
|
1108
|
+
remoteStatus.textContent = String(error);
|
|
1109
|
+
}).finally(loadRemote);
|
|
1110
|
+
});
|
|
1111
|
+
remoteLogin.addEventListener("click", () => {
|
|
1112
|
+
if (remoteLoginUrl !== "") window.open(remoteLoginUrl, "_blank", "noopener,noreferrer");
|
|
1113
|
+
});
|
|
1114
|
+
const reconnectRemote = () => {
|
|
1115
|
+
if (remoteReconnectBusy) return;
|
|
1116
|
+
remoteReconnectBusy = true;
|
|
1117
|
+
remoteReconnect.disabled = true;
|
|
1118
|
+
remoteSetup.disabled = true;
|
|
1119
|
+
remoteSetupRetry.disabled = true;
|
|
1120
|
+
remoteStatus.textContent = remoteProvider === "cpolar" ? "正在重新连接 cpolar 国内节点…" : "正在确认 Tailscale 设置并重新连接…";
|
|
1121
|
+
requestJson("/api/mobile-access/remote/reconnect", {
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
body: "{}"
|
|
1124
|
+
}).then(renderRemote, (error) => {
|
|
1125
|
+
remoteStatus.textContent = String(error);
|
|
1126
|
+
}).finally(() => {
|
|
1127
|
+
remoteReconnectBusy = false;
|
|
1128
|
+
remoteReconnect.disabled = false;
|
|
1129
|
+
remoteSetup.disabled = remoteSetupUrl === "";
|
|
1130
|
+
remoteSetupRetry.disabled = false;
|
|
1131
|
+
});
|
|
1132
|
+
};
|
|
1133
|
+
remoteReconnect.addEventListener("click", reconnectRemote);
|
|
1134
|
+
remoteSetupRetry.addEventListener("click", () => {
|
|
1135
|
+
remoteSetupPending = false;
|
|
1136
|
+
reconnectRemote();
|
|
1137
|
+
});
|
|
1138
|
+
remoteSetup.addEventListener("click", () => {
|
|
1139
|
+
if (remoteSetupUrl === "") return;
|
|
1140
|
+
remoteSetupPending = true;
|
|
1141
|
+
remoteSetupOpenedAt = Date.now();
|
|
1142
|
+
remoteStatus.textContent = "Tailscale 官方页面已打开。完成启用后返回 DSH,这里会自动重新连接。";
|
|
1143
|
+
window.open(remoteSetupUrl, "_blank", "noopener,noreferrer");
|
|
1144
|
+
});
|
|
1145
|
+
const retryAfterSetup = () => {
|
|
1146
|
+
if (!remoteSetupPending || document.visibilityState === "hidden" || Date.now() - remoteSetupOpenedAt < 800) return;
|
|
1147
|
+
remoteSetupPending = false;
|
|
1148
|
+
reconnectRemote();
|
|
1149
|
+
};
|
|
1150
|
+
window.addEventListener("focus", retryAfterSetup);
|
|
1151
|
+
document.addEventListener("visibilitychange", retryAfterSetup);
|
|
1152
|
+
remotePair.addEventListener("click", () => {
|
|
1153
|
+
remotePair.disabled = true;
|
|
1154
|
+
requestJson("/api/mobile-access/remote/pairing/open", {
|
|
1155
|
+
method: "POST",
|
|
1156
|
+
body: "{}"
|
|
1157
|
+
}).then(async (data) => {
|
|
1158
|
+
const pairUrl = typeof data.pairUrl === "string" ? data.pairUrl : "";
|
|
1159
|
+
showQr(typeof data.qrSvg === "string" ? data.qrSvg : "", remoteQr);
|
|
1160
|
+
if (pairUrl !== "") try {
|
|
1161
|
+
await navigator.clipboard.writeText(pairUrl);
|
|
1162
|
+
} catch {}
|
|
1163
|
+
remoteStatus.textContent = "远程配对二维码已生成。请在 App 的“远程访问”中扫描。";
|
|
1164
|
+
}, (error) => {
|
|
1165
|
+
remoteStatus.textContent = String(error);
|
|
1166
|
+
}).finally(() => {
|
|
1167
|
+
remotePair.disabled = !remoteReady;
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
const renderRemoteDevices = (data) => {
|
|
1171
|
+
const devices = Array.isArray(data.devices) ? data.devices : [];
|
|
1172
|
+
remoteDevicePanel.replaceChildren();
|
|
1173
|
+
if (devices.length === 0) {
|
|
1174
|
+
const empty = element("p", "dsh-mobile-control__device-empty");
|
|
1175
|
+
empty.textContent = "暂无远程配对设备。";
|
|
1176
|
+
remoteDevicePanel.append(empty);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
for (const device of devices) {
|
|
1180
|
+
const row = element("div", "dsh-mobile-control__device");
|
|
1181
|
+
const label = element("span", "dsh-mobile-control__device-label");
|
|
1182
|
+
label.textContent = typeof device.label === "string" ? device.label : "设备";
|
|
1183
|
+
const meta = element("span", "dsh-mobile-control__device-meta");
|
|
1184
|
+
meta.textContent = `到期 ${formatTime(device.expiresAt)}`;
|
|
1185
|
+
const revoke = element("button", "dsh-mobile-control__device-revoke");
|
|
1186
|
+
revoke.type = "button";
|
|
1187
|
+
revoke.textContent = "撤销";
|
|
1188
|
+
const id = typeof device.id === "string" ? device.id : "";
|
|
1189
|
+
revoke.addEventListener("click", () => {
|
|
1190
|
+
requestJson("/api/mobile-access/remote/devices/revoke", {
|
|
1191
|
+
method: "POST",
|
|
1192
|
+
body: JSON.stringify({ deviceId: id })
|
|
1193
|
+
}).then(loadRemoteDevices, (error) => {
|
|
1194
|
+
remoteStatus.textContent = String(error);
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
row.append(label, meta, revoke);
|
|
1198
|
+
remoteDevicePanel.append(row);
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
const loadRemoteDevices = () => {
|
|
1202
|
+
requestJson("/api/mobile-access/remote/devices").then(renderRemoteDevices, (error) => {
|
|
1203
|
+
remoteStatus.textContent = String(error);
|
|
1204
|
+
});
|
|
1205
|
+
};
|
|
1206
|
+
remoteDevices.addEventListener("click", () => {
|
|
1207
|
+
const show = remoteDevicePanel.hidden;
|
|
1208
|
+
remoteDevicePanel.hidden = !show;
|
|
1209
|
+
if (show) loadRemoteDevices();
|
|
1210
|
+
});
|
|
1211
|
+
remoteReset.addEventListener("click", () => {
|
|
1212
|
+
const prompt = remoteProvider === "cpolar" ? "关闭 cpolar 远程通道并移除所有远程配对设备?不会修改你的 cpolar 账号或其他隧道。" : "退出电脑上的 Tailscale 登录并移除所有远程配对设备?局域网配置不会改变。";
|
|
1213
|
+
if (!window.confirm(prompt)) return;
|
|
1214
|
+
requestJson("/api/mobile-access/remote/reset", {
|
|
1215
|
+
method: "POST",
|
|
1216
|
+
body: JSON.stringify({ confirm: true })
|
|
1217
|
+
}).then(renderRemote, (error) => {
|
|
1218
|
+
remoteStatus.textContent = String(error);
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
589
1221
|
pair.addEventListener("click", () => {
|
|
590
1222
|
pair.disabled = true;
|
|
591
1223
|
openPairing("key");
|
|
@@ -599,14 +1231,26 @@ window.__ModuleLoader__.load({
|
|
|
599
1231
|
});
|
|
600
1232
|
const dismiss = (event) => {
|
|
601
1233
|
if (panel.hidden || !(event.target instanceof Node)) return;
|
|
1234
|
+
if (!providerInfo.contains(event.target)) {
|
|
1235
|
+
providerInfoPinned = false;
|
|
1236
|
+
providerInfoHovered = false;
|
|
1237
|
+
syncProviderInfo();
|
|
1238
|
+
}
|
|
602
1239
|
if (!panel.contains(event.target) && !document.querySelector(".dsh-mobile-control__trigger")?.contains(event.target)) setOpen(false);
|
|
603
1240
|
};
|
|
604
1241
|
document.addEventListener("pointerdown", dismiss);
|
|
605
|
-
requestJson("/api/mobile-access/control").then(render, (error) => {
|
|
1242
|
+
requestJson("/api/mobile-access/lan/control").then(render, (error) => {
|
|
606
1243
|
status.textContent = String(error);
|
|
607
1244
|
});
|
|
1245
|
+
loadRemote();
|
|
1246
|
+
const remotePoll = window.setInterval(() => {
|
|
1247
|
+
if (!panel.hidden && !remoteView.hidden) loadRemote();
|
|
1248
|
+
}, 1500);
|
|
608
1249
|
return {
|
|
609
1250
|
remove: () => {
|
|
1251
|
+
window.clearInterval(remotePoll);
|
|
1252
|
+
window.removeEventListener("focus", retryAfterSetup);
|
|
1253
|
+
document.removeEventListener("visibilitychange", retryAfterSetup);
|
|
610
1254
|
document.removeEventListener("pointerdown", dismiss);
|
|
611
1255
|
root.remove();
|
|
612
1256
|
},
|
|
@@ -1005,14 +1649,20 @@ window.__ModuleLoader__.load({
|
|
|
1005
1649
|
}
|
|
1006
1650
|
const CONTROL_STYLES = `
|
|
1007
1651
|
.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)}
|
|
1008
|
-
.dsh-mobile-control__panel{box-sizing:border-box;width:min(
|
|
1652
|
+
.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%)}
|
|
1009
1653
|
.dsh-mobile-control__header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.dsh-mobile-control__panel h2{margin:0;font-size:17px;line-height:24px}.dsh-mobile-control__close{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:0;border-radius:10px;background:transparent;color:inherit;font-size:24px;line-height:1;cursor:pointer}.dsh-mobile-control__close:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}
|
|
1654
|
+
.dsh-mobile-control__switcher{display:grid;grid-template-columns:1fr 1fr;gap:4px;margin:0 0 14px;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f3f5f8)}.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}
|
|
1655
|
+
.dsh-mobile-control__provider-section{position:relative;margin:0 0 12px}.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;gap:8px}.dsh-mobile-control__provider{display:flex;flex-direction:column;gap:5px;min-height:68px;padding:11px 12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:#fff;color:inherit;text-align:left;cursor:pointer;transition:border-color 160ms ease,background-color 160ms ease,box-shadow 160ms ease}.dsh-mobile-control__provider:hover{border-color:#9fb9e8;background:#f8fbff}.dsh-mobile-control__provider.is-selected{border-color:#2563eb;background:#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;align-items:center;justify-content:space-between;gap:8px}.dsh-mobile-control__provider-top strong{font-size:13px}.dsh-mobile-control__provider-badge{flex:none;padding:3px 7px;border-radius:999px;background:#e8f0ff;color:#1d4ed8;font:650 10px/1.2 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:11px;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:#475569;cursor:pointer;touch-action:manipulation}.dsh-mobile-control__provider-info-button:hover{background:#f1f5f9;color:#1d4ed8}.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}
|
|
1656
|
+
.dsh-mobile-control__cpolar-setup{margin:0 0 12px;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:#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:#fff;color:inherit;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:#f3f5f8;color:#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:#b91c1c;font:12px/1.3 system-ui;cursor:pointer}
|
|
1010
1657
|
.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}
|
|
1011
1658
|
.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}
|
|
1659
|
+
.dsh-mobile-control__guide{margin:0 0 14px;padding:12px;border:1px solid #bfdbfe;border-radius:12px;background:#eff6ff}.dsh-mobile-control__guide[hidden]{display:none}.dsh-mobile-control__guide-title{margin:0;color:#172554;font:650 13px/1.45 system-ui}.dsh-mobile-control__guide-summary,.dsh-mobile-control__guide-note{margin:4px 0 0;color:#475569;font-size:12px;line-height:1.5}.dsh-mobile-control__guide-steps{margin:8px 0 0;padding-left:20px;color:#1e293b;font-size:12px;line-height:1.6}.dsh-mobile-control__guide-note{color:#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}
|
|
1012
1660
|
.dsh-mobile-control__extensions{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px}
|
|
1013
1661
|
.dsh-mobile-control__actions{display:flex;flex-wrap:nowrap;gap:6px}.dsh-mobile-control__actions button{flex:1 1 0;min-width:0;min-height:40px;padding:8px 4px;border-radius:10px;font:12px/1.2 system-ui;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dsh-mobile-control__secondary{border:1px solid var(--dsw-alias-border-normal,#cfd5dd);background:transparent;color:inherit}.dsh-mobile-control__primary{border:1px solid #2563eb;background:#2563eb;color:#fff}.dsh-mobile-control__actions button:disabled{cursor:not-allowed;opacity:.45}
|
|
1662
|
+
.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}
|
|
1014
1663
|
.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}
|
|
1015
1664
|
.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}
|
|
1665
|
+
@media (prefers-reduced-motion:reduce){.dsh-mobile-control__provider,.dsh-mobile-control__cpolar-connect{transition:none}}
|
|
1016
1666
|
`;
|
|
1017
1667
|
/** Mount the desktop control or mobile feature enhancements. */
|
|
1018
1668
|
function apply(ctx) {
|