dsh-mobile 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -664,6 +664,9 @@ window.__ModuleLoader__.load({
664
664
  frpVhostProbeFailed: "The VPS address could not be checked. Confirm DNS and retry.",
665
665
  frpLaunchFailed: "frpc failed to start.",
666
666
  frpTimeout: "The public endpoint did not become ready. Check frps, Caddy, DNS, and firewall.",
667
+ frpProxyNameInUse: "The frps already has a proxy with this installation name in use — usually another DSH computer, or a session that has not expired yet. Disable self-hosted FRP on that machine, or remove that proxy on the frps, then retry.",
668
+ frpRemotePortInUse: "Another proxy on the frps already owns this public entry port. Choose a different entry port and retry.",
669
+ frpProxyStartFailed: "The frps refused frpc's proxy registration. Check its allowPorts, the ports already in use, and its log.",
667
670
  frpDiscoveryMismatch: "The public domain points to another DSH computer. Check the Caddy and frps mappings.",
668
671
  frpDiscoveryInvalid: "The public domain did not return a valid DSH Mobile discovery response.",
669
672
  frpStopped: "The FRP connection stopped.",
@@ -751,6 +754,9 @@ window.__ModuleLoader__.load({
751
754
  frpVhostProbeFailed: "Impossibile controllare il VPS. Verifica il DNS e riprova.",
752
755
  frpLaunchFailed: "Avvio di frpc non riuscito.",
753
756
  frpTimeout: "Endpoint pubblico non pronto. Controlla frps, Caddy, DNS e firewall.",
757
+ frpProxyNameInUse: "Sul frps è già in uso un proxy con il nome di questa installazione — di solito un altro computer DSH o una sessione non ancora scaduta. Disattiva l’FRP autogestito su quella macchina, oppure rimuovi quel proxy sul frps, e riprova.",
758
+ frpRemotePortInUse: "Un altro proxy sul frps occupa già questa porta di ingresso pubblica. Scegli un’altra porta di ingresso e riprova.",
759
+ frpProxyStartFailed: "Il frps ha rifiutato la registrazione del proxy da parte di frpc. Controlla allowPorts, le porte già in uso e il log del server.",
754
760
  frpDiscoveryMismatch: "Il dominio pubblico punta a un altro computer DSH. Controlla Caddy e frps.",
755
761
  frpDiscoveryInvalid: "Il dominio pubblico non ha restituito una risposta DSH Mobile valida.",
756
762
  frpStopped: "Connessione FRP arrestata.",
@@ -838,6 +844,9 @@ window.__ModuleLoader__.load({
838
844
  frpVhostProbeFailed: "无法检查 VPS 地址,请确认 DNS 后重试。",
839
845
  frpLaunchFailed: "frpc 未能启动。",
840
846
  frpTimeout: "公网端点未能就绪,请检查 frps、Caddy、DNS 和防火墙。",
847
+ frpProxyNameInUse: "该 frps 上已有本机安装名对应的 FRP 代理在使用中——通常来自另一台 DSH 电脑,或尚未过期的旧连接。请在那台机器上关闭自建 FRP,或在 frps 上移除该代理后重试。",
848
+ frpRemotePortInUse: "该公网入口端口已被 frps 上的其他代理占用,请改用其他入口端口后重试。",
849
+ frpProxyStartFailed: "frps 拒绝了 frpc 的代理注册,请检查 frps 的 allowPorts、端口占用与服务器日志。",
841
850
  frpDiscoveryMismatch: "公开域名连接到了另一台 DSH 电脑,请检查 Caddy 与 frps 映射。",
842
851
  frpDiscoveryInvalid: "公开域名没有返回有效的 DSH Mobile 发现信息。",
843
852
  frpStopped: "FRP 连接已停止。",
@@ -2040,13 +2049,39 @@ window.__ModuleLoader__.load({
2040
2049
  return JSON.stringify(value);
2041
2050
  }
2042
2051
  /**
2052
+ * Legacy proxy name, kept only as the default of {@link createFrpcToml}.
2053
+ *
2054
+ * A shared frps accepts one proxy per name across *all* its clients, so a fixed
2055
+ * name let any other DSH installation — or a not yet reaped session of this one —
2056
+ * own `dsh-mobile` first. frpc then logged `start error: proxy name [dsh-mobile]
2057
+ * is already in use`, kept running without publishing the entry port, and the
2058
+ * start-up self-check ended in `frp_start_timeout` ("the public endpoint did not
2059
+ * become ready"). Product callers therefore pass an installation-derived name
2060
+ * from {@link frpProxyName}; this constant only keeps the exported API and the
2061
+ * byte-for-byte compatibility test working for legacy callers.
2062
+ */
2063
+ const FRP_DEFAULT_PROXY_NAME = "dsh-mobile";
2064
+ /**
2065
+ * Validate a proxy name for the generated frpc configuration.
2066
+ *
2067
+ * frps accepts one proxy per name on the whole server, so the name must be a
2068
+ * single safe token: no whitespace, no quotes, no path or bracket characters.
2069
+ */
2070
+ function validateFrpProxyName(value) {
2071
+ if (typeof value !== "string" || value.length < 1 || value.length > 128 || !/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error("frp_proxy_name_invalid");
2072
+ return value;
2073
+ }
2074
+ /**
2043
2075
  * Build the single-purpose frpc configuration for the current loopback gateway.
2044
2076
  *
2045
2077
  * The default (public-CA) entry is an HTTP vhost behind Caddy and is emitted byte
2046
2078
  * for byte as before. The self-signed entry cannot use a vhost at all: frps only
2047
2079
  * forwards raw TCP, and the gateway terminates TLS on that connection.
2080
+ *
2081
+ * `proxyName` defaults to the legacy fixed name only for legacy callers; the
2082
+ * provider and the attach preview always pass {@link frpProxyName}.
2048
2083
  */
2049
- function createFrpcToml(settings, localPort) {
2084
+ function createFrpcToml(settings, localPort, proxyName = FRP_DEFAULT_PROXY_NAME) {
2050
2085
  if (!Number.isSafeInteger(localPort) || localPort < 1 || localPort > 65535) throw new Error("frp_local_port_invalid");
2051
2086
  const header = [
2052
2087
  `serverAddr = ${tomlString(settings.serverAddress)}`,
@@ -2056,7 +2091,7 @@ window.__ModuleLoader__.load({
2056
2091
  "transport.tls.enable = true",
2057
2092
  "",
2058
2093
  "[[proxies]]",
2059
- "name = \"dsh-mobile\""
2094
+ `name = ${tomlString(validateFrpProxyName(proxyName))}`
2060
2095
  ];
2061
2096
  if (isFrpSelfSignedIngress(settings)) return [
2062
2097
  ...header,
@@ -2125,7 +2160,7 @@ window.__ModuleLoader__.load({
2125
2160
  * the provider writes stays byte-for-byte identical to the upstream output.
2126
2161
  */
2127
2162
  function createFrpAttachFrpcToml(settings, options = {}) {
2128
- const frpcToml = createFrpcToml(settings, attachLocalPort(options));
2163
+ const frpcToml = createFrpcToml(settings, attachLocalPort(options), options.proxyName ?? "dsh-mobile");
2129
2164
  return options.revealToken === true ? frpcToml : redactFrpcTomlToken(frpcToml);
2130
2165
  }
2131
2166
  const DEFAULT_ATTACH_CONFIG_FILE = "<DSH Mobile 私有目录>/remote/frp/config/frpc.toml";
@@ -2792,6 +2827,48 @@ window.__ModuleLoader__.load({
2792
2827
  function firstByClassSuffix(root, suffix) {
2793
2828
  return Array.from(root.querySelectorAll("[class]")).find((element) => classToken(element, suffix));
2794
2829
  }
2830
+ /**
2831
+ * Whether this composer is the landing hero rather than the composer of an open
2832
+ * conversation. DSH marks the hero composer's root with a `_hero` class and drops
2833
+ * it once a session owns the surface, which is the only difference available
2834
+ * without the layout's session id.
2835
+ */
2836
+ function composerIsLandingHero(composer, depth = 4) {
2837
+ let node = composer;
2838
+ for (let step = 0; step < depth && node !== null; step += 1) {
2839
+ if (classToken(node, "_hero")) return true;
2840
+ node = node.parentElement;
2841
+ }
2842
+ return false;
2843
+ }
2844
+ /** Resolve only identities published by the current conversation or selected session row. */
2845
+ function resolveComposerSessionOrigin(composer, selectedRow, selectedRowToken) {
2846
+ const conversationRoot = composer?.closest("[data-conversation-session]") ?? null;
2847
+ const conversationId = conversationRoot?.getAttribute("data-conversation-session");
2848
+ if (conversationRoot !== null && conversationId) return {
2849
+ sessionRoot: conversationRoot,
2850
+ sessionId: conversationId
2851
+ };
2852
+ const dedicatedRoot = composer?.closest("[data-dsh-mobile-session]") ?? null;
2853
+ const dedicatedId = dedicatedRoot?.getAttribute("data-dsh-mobile-session");
2854
+ if (dedicatedRoot !== null && dedicatedId) return {
2855
+ sessionRoot: dedicatedRoot,
2856
+ sessionId: dedicatedId
2857
+ };
2858
+ if (selectedRow === null) return {
2859
+ sessionRoot: null,
2860
+ sessionId: null
2861
+ };
2862
+ const identity = selectedRow.getAttribute("data-session-id") ?? selectedRow.getAttribute("aria-label") ?? selectedRow.textContent?.trim() ?? "";
2863
+ return {
2864
+ sessionRoot: selectedRow,
2865
+ sessionId: `${selectedRowToken(selectedRow)}:${identity}`
2866
+ };
2867
+ }
2868
+ /** An id-less non-hero composer still accepts App soft-keyboard line breaks, but not camera results. */
2869
+ function activeSessionForSoftEnter(composer, sessionId) {
2870
+ return sessionId !== null || composer !== null && composer.closest(".dshm-main") !== null && !composerIsLandingHero(composer);
2871
+ }
2795
2872
  /** Find the stock DSH application frame without mistaking a feature card for the shell. */
2796
2873
  function resolveNativeMobileFrame(root, dedicatedCenter) {
2797
2874
  if (dedicatedCenter !== void 0) return void 0;
@@ -2962,6 +3039,38 @@ window.__ModuleLoader__.load({
2962
3039
  function drawerScrimVisible(sidebarCollapsed, overlayActive) {
2963
3040
  return !sidebarCollapsed && overlayActive;
2964
3041
  }
3042
+ /** Let the stock page close one visible layer before Android navigates its WebView. */
3043
+ function installStockMobileBack(view, current, actions) {
3044
+ const onBack = (event) => {
3045
+ if (!event.cancelable || event.defaultPrevented) return;
3046
+ const layers = current();
3047
+ if (layers === null) return;
3048
+ if (layers.detailsOpen) {
3049
+ event.preventDefault();
3050
+ actions.closeDetails();
3051
+ } else if (layers.drawerOpen) {
3052
+ event.preventDefault();
3053
+ actions.closeDrawer();
3054
+ } else if (layers.mainPanelOpen) {
3055
+ event.preventDefault();
3056
+ actions.closeMainPanel();
3057
+ }
3058
+ };
3059
+ view.addEventListener("dsh-mobile:native-back", onBack);
3060
+ return () => {
3061
+ view.removeEventListener("dsh-mobile:native-back", onBack);
3062
+ };
3063
+ }
3064
+ function stockLayoutBackControl(value) {
3065
+ return typeof value === "object" && value !== null ? value : void 0;
3066
+ }
3067
+ /** Whether the stock layout can return from a selected main panel. */
3068
+ function stockMainPanelOpen(value) {
3069
+ const layout = stockLayoutBackControl(value);
3070
+ if (typeof layout?.selectPanel !== "function" || typeof layout.panelInfo?.getSnapshot !== "function") return false;
3071
+ const snapshot = layout.panelInfo.getSnapshot();
3072
+ return typeof snapshot === "object" && snapshot !== null && typeof snapshot.activePanelId === "string";
3073
+ }
2965
3074
  /** Only a known on-screen keyboard in an active App composer may change Enter into a line break. */
2966
3075
  function isSoftKeyboardEnterLineBreak(event, context) {
2967
3076
  if (context.nativeState?.imeVisible !== true || context.nativeState.noHardwareKeyboard !== true || !context.editable || !context.activeSession || context.commandMenuOpen || context.recentlyComposing) return false;
@@ -3009,7 +3118,7 @@ window.__ModuleLoader__.load({
3009
3118
  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;
3010
3119
  }
3011
3120
  /** Add mobile semantics without replacing feature trees. */
3012
- function installNativeMobileSurface() {
3121
+ function installNativeMobileSurface(backServices) {
3013
3122
  document.documentElement.classList.add("dsh-native-mobile-active");
3014
3123
  const browserLanguages = navigator.languages.length > 0 ? navigator.languages : [navigator.language];
3015
3124
  const language = resolveNativeMobileLanguage(document.documentElement.lang, browserLanguages);
@@ -3106,27 +3215,15 @@ window.__ModuleLoader__.load({
3106
3215
  const sessionTokens = /* @__PURE__ */ new WeakMap();
3107
3216
  let nextSessionToken = 0;
3108
3217
  const currentSessionOrigin = () => {
3109
- const dedicatedRoot = boundComposer?.closest("[data-dsh-mobile-session]") ?? null;
3110
- const dedicatedId = dedicatedRoot?.getAttribute("data-dsh-mobile-session");
3111
- if (dedicatedRoot !== null && typeof dedicatedId === "string" && dedicatedId !== "") return {
3112
- sessionRoot: dedicatedRoot,
3113
- sessionId: dedicatedId
3114
- };
3115
3218
  const selectedRow = document.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]");
3116
- if (selectedRow === null) return {
3117
- sessionRoot: null,
3118
- sessionId: null
3119
- };
3120
- let token = sessionTokens.get(selectedRow);
3121
- if (token === void 0) {
3122
- token = `stock-${String(++nextSessionToken)}`;
3123
- sessionTokens.set(selectedRow, token);
3124
- }
3125
- const identity = selectedRow.getAttribute("data-session-id") ?? selectedRow.getAttribute("aria-label") ?? selectedRow.textContent?.trim() ?? "";
3126
- return {
3127
- sessionRoot: selectedRow,
3128
- sessionId: `${token}:${identity}`
3129
- };
3219
+ return resolveComposerSessionOrigin(boundComposer, selectedRow, (row) => {
3220
+ let token = sessionTokens.get(row);
3221
+ if (token === void 0) {
3222
+ token = `stock-${String(++nextSessionToken)}`;
3223
+ sessionTokens.set(row, token);
3224
+ }
3225
+ return token;
3226
+ });
3130
3227
  };
3131
3228
  const mediaRequestContext = () => {
3132
3229
  const session = currentSessionOrigin();
@@ -3472,7 +3569,7 @@ window.__ModuleLoader__.load({
3472
3569
  disposeEnterBinding = editor === null || composerCard === null ? void 0 : bindComposerSoftEnter(editor, () => ({
3473
3570
  nativeState: window.__DSH_MOBILE_NATIVE__ === void 0 ? null : window.__DSH_MOBILE_KEYBOARD_STATE__,
3474
3571
  editable: editor.isConnected && editor.getAttribute("contenteditable") === "true" && editor.getAttribute("aria-disabled") !== "true" && editor.getAttribute("aria-haspopup") !== "menu" && composerCard.getAttribute("aria-busy") !== "true",
3475
- activeSession: currentSessionOrigin().sessionId !== null,
3572
+ activeSession: activeSessionForSoftEnter(composerCard, currentSessionOrigin().sessionId),
3476
3573
  commandMenuOpen: composerCard.querySelector("[data-trigger-menu],button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]") !== null
3477
3574
  }));
3478
3575
  };
@@ -3621,6 +3718,33 @@ window.__ModuleLoader__.load({
3621
3718
  if (sidebar?.dataset.open === "true") toggle?.click();
3622
3719
  });
3623
3720
  sync();
3721
+ const stopStockBack = installStockMobileBack(window, () => {
3722
+ if (frame?.isConnected !== true) return null;
3723
+ const lastColumn = frame.style.gridTemplateColumns.trim().split(/\s+/).at(-1);
3724
+ return {
3725
+ detailsOpen: overlayQuery.matches && lastColumn !== void 0 && lastColumn !== "" && lastColumn !== "0px" && lastColumn !== "0",
3726
+ drawerOpen: overlayQuery.matches && sidebarRoot?.isConnected === true && toggle?.isConnected === true && !classToken(sidebarRoot, "_collapsed"),
3727
+ mainPanelOpen: stockMainPanelOpen(backServices.getLayout())
3728
+ };
3729
+ }, {
3730
+ closeDetails: () => {
3731
+ const layout = stockLayoutBackControl(backServices.getLayout());
3732
+ if (typeof layout?.closeRightbar !== "function") return;
3733
+ const sidebarRight = backServices.getSidebarRight();
3734
+ if (typeof sidebarRight === "object" && sidebarRight !== null) {
3735
+ const control = sidebarRight;
3736
+ if (typeof control.isExpanded === "function" && typeof control.toggleExpanded === "function" && control.isExpanded()) control.toggleExpanded();
3737
+ }
3738
+ layout.closeRightbar();
3739
+ },
3740
+ closeDrawer: () => {
3741
+ toggle?.click();
3742
+ },
3743
+ closeMainPanel: () => {
3744
+ const layout = stockLayoutBackControl(backServices.getLayout());
3745
+ if (typeof layout?.selectPanel === "function") layout.selectPanel(null);
3746
+ }
3747
+ });
3624
3748
  const disposeTaskWatcher = installTaskCompletionWatcher({
3625
3749
  completionFallback: false,
3626
3750
  label: (kind, sessionLabel) => kind === "done" ? {
@@ -3633,6 +3757,7 @@ window.__ModuleLoader__.load({
3633
3757
  });
3634
3758
  return () => {
3635
3759
  disposed = true;
3760
+ stopStockBack();
3636
3761
  disposeTaskWatcher();
3637
3762
  mediaRequestGeneration += 1;
3638
3763
  mediaPickerAbortController.abort();
@@ -3737,6 +3862,64 @@ window.__ModuleLoader__.load({
3737
3862
  function parseMobileRemoteProvider(value) {
3738
3863
  return value === "cpolar" || value === "cloudflared" || value === "frp" || value === "origin" ? value : "tailscale";
3739
3864
  }
3865
+ /**
3866
+ * Set a surface's visibility; opening a page or overlay brings it to the front.
3867
+ * @param container - the registered surface element.
3868
+ * @param open - whether to show it.
3869
+ */
3870
+ function setMobileSurfaceOpen(container, open) {
3871
+ container.hidden = !open;
3872
+ if (open && container.isConnected && (container.dataset.dshMobileSurfacePlacement === "page" || container.dataset.dshMobileSurfacePlacement === "overlay")) container.parentElement?.append(container);
3873
+ }
3874
+ function visibleBackLayer(view, element) {
3875
+ if (!element.isConnected || element.hidden || element.closest("[hidden],[inert],[aria-hidden=\"true\"]") !== null) return false;
3876
+ const style = view.getComputedStyle(element);
3877
+ return style.display !== "none" && style.visibility !== "hidden" && element.getClientRects().length > 0;
3878
+ }
3879
+ /**
3880
+ * Consume page-level surfaces and dismissible DSH overlays before layout navigation.
3881
+ * @param view - the page window receiving the native Back request.
3882
+ * @param page - the current DSH document.
3883
+ * @param blocked - feedback for a foreground layer without a safe close action.
3884
+ * @returns a disposer for the capture listener.
3885
+ */
3886
+ function installMobileNativeBack(view, page, blocked) {
3887
+ const onBack = (event) => {
3888
+ if (!event.cancelable || event.defaultPrevented) return;
3889
+ const claim = () => {
3890
+ event.preventDefault();
3891
+ event.stopImmediatePropagation();
3892
+ };
3893
+ const surface = [...page.querySelectorAll("[data-dsh-mobile-surface-placement=\"page\"], [data-dsh-mobile-surface-placement=\"overlay\"]")].filter((element) => visibleBackLayer(view, element)).at(-1);
3894
+ if (surface !== void 0) {
3895
+ setMobileSurfaceOpen(surface, false);
3896
+ claim();
3897
+ return;
3898
+ }
3899
+ const layers = [...page.querySelectorAll("[role=\"dialog\"][aria-modal=\"true\"], [role=\"menu\"]")].filter((element) => visibleBackLayer(view, element));
3900
+ const top = layers.at(-1);
3901
+ if (top === void 0) return;
3902
+ claim();
3903
+ if (top.getAttribute("role") === "dialog") {
3904
+ const wrapper = top.parentElement;
3905
+ const mask = wrapper?.firstElementChild;
3906
+ const maskStyle = mask === void 0 || mask === null ? void 0 : view.getComputedStyle(mask);
3907
+ if (wrapper?.getAttribute("role") === "presentation" && wrapper.children.length === 2 && wrapper.lastElementChild === top && mask?.tagName === "DIV" && mask.getAttribute("aria-hidden") === "true" && mask.isConnected && !mask.hidden && maskStyle?.display !== "none" && maskStyle?.visibility !== "hidden" && mask.getClientRects().length > 0) {
3908
+ mask.click();
3909
+ return;
3910
+ }
3911
+ } else if (top.getAttribute("data-menu-material") === "translucent" && layers.filter((layer) => layer.getAttribute("role") === "menu").length === 1) {
3912
+ page.dispatchEvent(new Event("pointerdown", { bubbles: true }));
3913
+ return;
3914
+ }
3915
+ blocked();
3916
+ };
3917
+ const capture = { capture: true };
3918
+ view.addEventListener("dsh-mobile:native-back", onBack, capture);
3919
+ return () => {
3920
+ view.removeEventListener("dsh-mobile:native-back", onBack, capture);
3921
+ };
3922
+ }
3740
3923
  const queuedDefinitions = [];
3741
3924
  let queuedLegacyMount;
3742
3925
  if (typeof window !== "undefined" && window.dshMobile === void 0) window.dshMobile = {
@@ -3757,6 +3940,18 @@ window.__ModuleLoader__.load({
3757
3940
  function selectedMobileControlLocale() {
3758
3941
  return selectMobileControlLocale(document.documentElement.lang, navigator.languages?.length ? navigator.languages : [navigator.language]);
3759
3942
  }
3943
+ /**
3944
+ * Guidance when an open third-party layer has no safe close action.
3945
+ * @param locale - current Mobile UI language.
3946
+ * @returns localized guidance.
3947
+ */
3948
+ function mobileBackBlockedMessage(locale) {
3949
+ return {
3950
+ en: "Close the open panel first.",
3951
+ it: "Chiudi prima il pannello aperto.",
3952
+ zh: "请先关闭当前弹层。"
3953
+ }[locale];
3954
+ }
3760
3955
  /** Remount one plugin-owned surface when DSH changes the document language. */
3761
3956
  function installDshLanguageBoundSurface(install) {
3762
3957
  let locale = selectedMobileControlLocale();
@@ -4205,6 +4400,9 @@ window.__ModuleLoader__.load({
4205
4400
  frp_vhost_probe_failed: "frpVhostProbeFailed",
4206
4401
  frp_launch_failed: "frpLaunchFailed",
4207
4402
  frp_start_timeout: "frpTimeout",
4403
+ frp_proxy_name_in_use: "frpProxyNameInUse",
4404
+ frp_remote_port_in_use: "frpRemotePortInUse",
4405
+ frp_proxy_start_failed: "frpProxyStartFailed",
4208
4406
  frp_discovery_mismatch: "frpDiscoveryMismatch",
4209
4407
  frp_discovery_invalid: "frpDiscoveryInvalid",
4210
4408
  frp_stopped: "frpStopped",
@@ -4266,6 +4464,9 @@ window.__ModuleLoader__.load({
4266
4464
  "frp_vhost_probe_failed",
4267
4465
  "frp_launch_failed",
4268
4466
  "frp_start_timeout",
4467
+ "frp_proxy_name_in_use",
4468
+ "frp_remote_port_in_use",
4469
+ "frp_proxy_start_failed",
4269
4470
  "frp_discovery_mismatch",
4270
4471
  "frp_discovery_invalid",
4271
4472
  "frp_stopped",
@@ -4812,10 +5013,12 @@ window.__ModuleLoader__.load({
4812
5013
  frpCopyAttachPlan.type = "button";
4813
5014
  frpCopyAttachPlan.textContent = t("frpAttachPlan");
4814
5015
  frpCopyAttachPlan.hidden = true;
5016
+ frpCopyAttachPlan.disabled = true;
4815
5017
  const frpCopyAttachToken = element("button", "dsh-mobile-control__secondary dsh-mobile-control__frp-action");
4816
5018
  frpCopyAttachToken.type = "button";
4817
5019
  frpCopyAttachToken.textContent = t("frpAttachToken");
4818
5020
  frpCopyAttachToken.hidden = true;
5021
+ frpCopyAttachToken.disabled = true;
4819
5022
  const frpSelfCheckButton = element("button", "dsh-mobile-control__secondary dsh-mobile-control__frp-action");
4820
5023
  frpSelfCheckButton.type = "button";
4821
5024
  frpSelfCheckButton.textContent = t("frpAttachSelfCheck");
@@ -5395,6 +5598,8 @@ window.__ModuleLoader__.load({
5395
5598
  let configuredFrpOrigin = "";
5396
5599
  let configuredFrpVhostPort = 7080;
5397
5600
  let configuredFrpPublicPort = 33080;
5601
+ /** Proxy name the host writes into its frpc.toml; the local clipboard copy must match. */
5602
+ let configuredFrpProxyName;
5398
5603
  let frpConfiguredMode = "deploy";
5399
5604
  let frpConfiguredEntryTls = "public-ip-cert";
5400
5605
  let frpModeDraft = false;
@@ -5825,6 +6030,7 @@ window.__ModuleLoader__.load({
5825
6030
  if (frpConfigured && !frpEntryTlsDraft) frpEntryTls.value = configuredFrpEntryTls;
5826
6031
  if (typeof frpConfiguration.vhostHttpPort === "number") configuredFrpVhostPort = frpConfiguration.vhostHttpPort;
5827
6032
  if (typeof frpConfiguration.publicPort === "number") configuredFrpPublicPort = frpConfiguration.publicPort;
6033
+ configuredFrpProxyName = typeof frpConfiguration.proxyName === "string" && frpConfiguration.proxyName !== "" ? frpConfiguration.proxyName : void 0;
5828
6034
  if (!frpVhostDraft) {
5829
6035
  if (frpMode.value === "attach" && configuredFrpMode === "attach" && configuredFrpEntryTls === "public-ip-cert") {
5830
6036
  frpVhostPort.value = String(configuredFrpVhostPort);
@@ -5835,12 +6041,14 @@ window.__ModuleLoader__.load({
5835
6041
  const attachSelected = frpMode.value === "attach";
5836
6042
  const selfSignedSelected = attachSelected && frpEntryTls.value === "self-signed";
5837
6043
  frpStep2Title.textContent = t(attachSelected ? "frpStep2TitleAttach" : "frpStep2Title");
5838
- frpStep2Text.textContent = t(attachSelected ? "frpStep2TextAttach" : "frpStep2Text");
6044
+ frpStep2Text.textContent = attachSelected && configuredFrpProxyName === void 0 ? t("loadingRemoteStatus") : t(attachSelected ? "frpStep2TextAttach" : "frpStep2Text");
5839
6045
  frpStep4Text.textContent = t(attachSelected ? "frpStep4TextAttach" : "frpStep4Text");
5840
6046
  frpAppRequirement.textContent = t(selfSignedSelected ? "frpAppRequirementSelfSigned" : "frpAppRequirement");
5841
6047
  frpCopyTemplate.hidden = attachSelected;
5842
6048
  frpCopyAttachPlan.hidden = !attachSelected;
5843
6049
  frpCopyAttachToken.hidden = !attachSelected;
6050
+ frpCopyAttachPlan.disabled = remoteProviderBusy || configuredFrpProxyName === void 0;
6051
+ frpCopyAttachToken.disabled = remoteProviderBusy || configuredFrpProxyName === void 0;
5844
6052
  frpSelfCheckButton.hidden = !frpConfigured;
5845
6053
  frpVhostPort.disabled = selfSignedSelected;
5846
6054
  frpVhostLabel.hidden = selfSignedSelected;
@@ -6318,6 +6526,13 @@ window.__ModuleLoader__.load({
6318
6526
  }
6319
6527
  remoteStatus.textContent = message;
6320
6528
  };
6529
+ const frpClipboardProxyName = () => {
6530
+ if (configuredFrpProxyName === void 0) {
6531
+ remoteStatus.textContent = t("loadingRemoteStatus");
6532
+ loadRemote();
6533
+ }
6534
+ return configuredFrpProxyName;
6535
+ };
6321
6536
  frpCopyAttachPlan.addEventListener("click", () => {
6322
6537
  const form = frpForm();
6323
6538
  if (!validFrpServer(form.serverAddress) || !Number.isSafeInteger(form.serverPort) || form.serverPort < 1 || form.serverPort > 65535 || form.publicOrigin === "") {
@@ -6331,7 +6546,9 @@ window.__ModuleLoader__.load({
6331
6546
  showFrpAttachError(code);
6332
6547
  return;
6333
6548
  }
6334
- navigator.clipboard.writeText(createFrpAttachTemplateForClipboard(attachForm)).then(() => {
6549
+ const proxyName = frpClipboardProxyName();
6550
+ if (proxyName === void 0) return;
6551
+ navigator.clipboard.writeText(createFrpAttachTemplateForClipboard(attachForm, { proxyName })).then(() => {
6335
6552
  remoteStatus.textContent = t("frpAttachPlanCopied");
6336
6553
  }, () => {
6337
6554
  remoteStatus.textContent = t("frpAttachPlanFailed", { error: t("templateCopyFailed") });
@@ -6359,7 +6576,7 @@ window.__ModuleLoader__.load({
6359
6576
  remoteStatus.textContent = t("frpAttachPlanFailed", { error: String(error) });
6360
6577
  }).finally(() => {
6361
6578
  remoteProviderBusy = false;
6362
- frpCopyAttachPlan.disabled = false;
6579
+ frpCopyAttachPlan.disabled = configuredFrpProxyName === void 0;
6363
6580
  });
6364
6581
  });
6365
6582
  /** Copy a token-bearing frpc.toml only from the token the user just typed. */
@@ -6376,7 +6593,12 @@ window.__ModuleLoader__.load({
6376
6593
  showFrpAttachError(code);
6377
6594
  return;
6378
6595
  }
6379
- navigator.clipboard.writeText(createFrpAttachFrpcTomlForClipboard(attachForm, { revealToken: true })).then(() => {
6596
+ const proxyName = frpClipboardProxyName();
6597
+ if (proxyName === void 0) return;
6598
+ navigator.clipboard.writeText(createFrpAttachFrpcTomlForClipboard(attachForm, {
6599
+ revealToken: true,
6600
+ proxyName
6601
+ })).then(() => {
6380
6602
  remoteStatus.textContent = t("frpAttachTokenCopied");
6381
6603
  }, () => {
6382
6604
  remoteStatus.textContent = t("frpAttachTokenFailed", { error: t("templateCopyFailed") });
@@ -7880,6 +8102,9 @@ window.__ModuleLoader__.load({
7880
8102
  shellLayer().append(node);
7881
8103
  window.setTimeout(() => node.remove(), 2600);
7882
8104
  };
8105
+ const stopNativeBack = installMobileNativeBack(window, document, () => {
8106
+ toast(mobileBackBlockedMessage(selectedMobileControlLocale()));
8107
+ });
7883
8108
  const materializeNativeFile = (value) => {
7884
8109
  if (typeof value !== "object" || value === null || !("base64" in value) || !("name" in value)) return value;
7885
8110
  const candidate = value;
@@ -8012,6 +8237,7 @@ window.__ModuleLoader__.load({
8012
8237
  return registerUniqueDisposable(surfaces, surfaceIds, surface.id, () => {
8013
8238
  const container = element("section");
8014
8239
  container.dataset.dshMobileSurface = surface.id;
8240
+ container.dataset.dshMobileSurfacePlacement = surface.placement;
8015
8241
  container.hidden = surface.placement === "page" || surface.placement === "overlay";
8016
8242
  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";
8017
8243
  const host = () => surface.placement === "page" || surface.placement === "overlay" ? shellLayer() : surfaceHost(surface.placement) ?? shellLayer();
@@ -8064,12 +8290,12 @@ window.__ModuleLoader__.load({
8064
8290
  open: (surfaceId) => {
8065
8291
  ensureCurrent();
8066
8292
  const entry = surfaces.get(surfaceId);
8067
- if (entry !== void 0) entry.container.hidden = false;
8293
+ if (entry !== void 0) setMobileSurfaceOpen(entry.container, true);
8068
8294
  },
8069
8295
  close: (surfaceId) => {
8070
8296
  ensureCurrent();
8071
8297
  const entry = surfaces.get(surfaceId);
8072
- if (entry !== void 0) entry.container.hidden = true;
8298
+ if (entry !== void 0) setMobileSurfaceOpen(entry.container, false);
8073
8299
  },
8074
8300
  toast: (message) => {
8075
8301
  ensureCurrent();
@@ -8441,6 +8667,7 @@ window.__ModuleLoader__.load({
8441
8667
  });
8442
8668
  return () => {
8443
8669
  disposed = true;
8670
+ stopNativeBack();
8444
8671
  stopEvents();
8445
8672
  stopRefresh();
8446
8673
  started = false;
@@ -8560,7 +8787,10 @@ window.__ModuleLoader__.load({
8560
8787
  };
8561
8788
  });
8562
8789
  const removeCustom = installCustomAssets();
8563
- const removeSurface = installDshLanguageBoundSurface(installNativeMobileSurface);
8790
+ const removeSurface = installDshLanguageBoundSurface(() => installNativeMobileSurface({
8791
+ getLayout: () => ctx.get("layout"),
8792
+ getSidebarRight: () => ctx.get("sidebarRight")
8793
+ }));
8564
8794
  return () => {
8565
8795
  removeSettingsAction();
8566
8796
  removeCustom();
@@ -8643,6 +8873,8 @@ window.__ModuleLoader__.load({
8643
8873
  exports.handleMissingExtensionManifest = handleMissingExtensionManifest;
8644
8874
  exports.inject = inject;
8645
8875
  exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
8876
+ exports.installMobileNativeBack = installMobileNativeBack;
8877
+ exports.mobileBackBlockedMessage = mobileBackBlockedMessage;
8646
8878
  exports.normalizeDiagnosticOverall = normalizeDiagnosticOverall;
8647
8879
  exports.normalizeDiagnosticStatus = normalizeDiagnosticStatus;
8648
8880
  exports.parseMobileExtensionManifest = parseMobileExtensionManifest;
@@ -8653,6 +8885,7 @@ window.__ModuleLoader__.load({
8653
8885
  exports.renderDiagnosticPayloadSafely = renderDiagnosticPayloadSafely;
8654
8886
  exports.selectMobileControlLocale = selectMobileControlLocale;
8655
8887
  exports.selectedMobileControlLocale = selectedMobileControlLocale;
8888
+ exports.setMobileSurfaceOpen = setMobileSurfaceOpen;
8656
8889
  exports.startExtensionChangeStream = startExtensionChangeStream;
8657
8890
  exports.startLifecycleRefreshScheduler = startLifecycleRefreshScheduler;
8658
8891
  exports.trustAuthenticatedGatewayConnection = trustAuthenticatedGatewayConnection;