main
ts 177 lines 4.28 KB
Raw
1 import { useEffect, useMemo, useState, type ChangeEvent } from "react";
2 import {
3 buildDefaultExposeName,
4 normalizeExposeName,
5 } from "@/lib/exposeName";
6 import {
7 buildTunnelCommand,
8 buildTunnelDisplayCommand,
9 type TunnelCommandOS,
10 } from "@/lib/tunnelCommand";
11
12 export const DEFAULT_HOST = "3000";
13
14 const FALLBACK_ORIGIN = "https://localhost:4017";
15 const TUNNEL_NAME_SEED_STORAGE_KEY = "portal:tunnel-name-seed";
16
17 export function readCurrentOrigin(): string {
18 if (typeof window !== "undefined") {
19 return window.location.origin;
20 }
21
22 return FALLBACK_ORIGIN;
23 }
24
25 function readTunnelNameSeed(): string {
26 if (typeof window === "undefined") {
27 return "web_portal";
28 }
29
30 try {
31 const existing = window.localStorage.getItem(TUNNEL_NAME_SEED_STORAGE_KEY);
32 if (existing && existing.trim() !== "") {
33 return existing;
34 }
35
36 const next =
37 typeof window.crypto?.randomUUID === "function"
38 ? `web_${window.crypto.randomUUID()}`
39 : `web_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
40
41 window.localStorage.setItem(TUNNEL_NAME_SEED_STORAGE_KEY, next);
42 return next;
43 } catch {
44 return "web_portal";
45 }
46 }
47
48 function nextTunnelNameShuffleKey(): string {
49 if (
50 typeof window !== "undefined" &&
51 typeof window.crypto?.randomUUID === "function"
52 ) {
53 return window.crypto.randomUUID();
54 }
55
56 return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
57 }
58
59 interface TunnelCommandExtras {
60 relayUrls?: string[];
61 discovery?: boolean;
62 thumbnailURL?: string;
63 enableUDP?: boolean;
64 udpPort?: string;
65 }
66
67 export function useTunnelCommand(extras: TunnelCommandExtras = {}) {
68 const currentOrigin = useMemo(() => readCurrentOrigin(), []);
69 const [nameSeed] = useState(readTunnelNameSeed);
70
71 const [target, setTarget] = useState(DEFAULT_HOST);
72 const [name, setName] = useState("");
73 const [nameShuffleKey, setNameShuffleKey] = useState("default");
74 const [copied, setCopied] = useState(false);
75 const [os, setOs] = useState<TunnelCommandOS>("unix");
76
77 const resolvedNameSeed = `${nameSeed}:${nameShuffleKey}`;
78 const generatedName = buildDefaultExposeName(target, resolvedNameSeed);
79 const normalizedName = normalizeExposeName(name);
80 const effectiveName = normalizedName === "" ? generatedName : normalizedName;
81 const commandOptions = useMemo(
82 () => ({
83 currentOrigin,
84 target,
85 name: effectiveName,
86 nameSeed,
87 relayUrls: extras.relayUrls ?? [currentOrigin],
88 discovery: extras.discovery ?? true,
89 thumbnailURL: extras.thumbnailURL ?? "",
90 enableUDP: extras.enableUDP ?? false,
91 udpPort: extras.udpPort ?? "",
92 os,
93 }),
94 [
95 currentOrigin,
96 effectiveName,
97 extras.discovery,
98 extras.enableUDP,
99 extras.relayUrls,
100 extras.thumbnailURL,
101 extras.udpPort,
102 nameSeed,
103 os,
104 target,
105 ]
106 );
107 const copyCommand = useMemo(
108 () => buildTunnelCommand(commandOptions),
109 [commandOptions]
110 );
111 const displayCommand = useMemo(
112 () => buildTunnelDisplayCommand(commandOptions),
113 [commandOptions]
114 );
115 const { installBlock, runBlock } = useMemo(
116 () => {
117 const lines = displayCommand.split("\n");
118 const installLineCount = os === "windows" ? 2 : 1;
119
120 return {
121 installBlock: lines.slice(0, installLineCount).join("\n"),
122 runBlock: lines.slice(installLineCount).join("\n"),
123 };
124 },
125 [displayCommand, os]
126 );
127
128 useEffect(() => {
129 if (!copied) {
130 return;
131 }
132
133 const timer = window.setTimeout(() => {
134 setCopied(false);
135 }, 2000);
136
137 return () => {
138 window.clearTimeout(timer);
139 };
140 }, [copied]);
141
142 const handleCopy = async () => {
143 try {
144 await navigator.clipboard.writeText(copyCommand);
145 setCopied(true);
146 } catch (error) {
147 console.error("Failed to copy tunnel command", error);
148 }
149 };
150
151 const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
152 setName(event.target.value);
153 };
154
155 const handleShuffleName = () => {
156 setName("");
157 setNameShuffleKey(nextTunnelNameShuffleKey());
158 };
159
160 return {
161 currentOrigin,
162 nameSeed,
163 target,
164 setTarget,
165 name,
166 copied,
167 os,
168 setOs,
169 generatedName,
170 effectiveName,
171 installBlock,
172 runBlock,
173 handleCopy,
174 handleNameChange,
175 handleShuffleName,
176 };
177 }