124 lines
3.8 KiB
TypeScript
124 lines
3.8 KiB
TypeScript
/**
|
|
* ConnectDialog — name + role picker to join a session
|
|
*/
|
|
import { Component, createSignal, onMount, Show } from "solid-js";
|
|
import {
|
|
connectStream,
|
|
hydrateFromServer,
|
|
useJournalStream,
|
|
setMyName,
|
|
setMyRole,
|
|
setSessionId,
|
|
} from "../stores/journalStream";
|
|
|
|
export const ConnectDialog: Component = () => {
|
|
const stream = useJournalStream();
|
|
const [playerName, setPlayerName] = createSignal(stream.myName);
|
|
const [role, setRole] = createSignal<"gm" | "player" | "observer">(
|
|
stream.myRole,
|
|
);
|
|
const [connecting, setConnecting] = createSignal(false);
|
|
const [error, setError] = createSignal<string | null>(null);
|
|
const [autoJoined, setAutoJoined] = createSignal(false);
|
|
|
|
// Auto-join when URL has both session and player params
|
|
onMount(() => {
|
|
const params = new URL(window.location.href).searchParams;
|
|
if (
|
|
params.has("session") &&
|
|
params.has("player") &&
|
|
params.has("autojoin")
|
|
) {
|
|
setPlayerName(params.get("player") || "");
|
|
setRole("player");
|
|
setAutoJoined(true);
|
|
handleConnect();
|
|
}
|
|
});
|
|
|
|
const handleConnect = async () => {
|
|
const name = playerName().trim() || stream.myName;
|
|
if (!name) return;
|
|
|
|
// In dev, the MQTT broker lives on the CLI server (port 3000), not the
|
|
// rsbuild dev server. In production, the CLI serves everything itself.
|
|
const brokerUrl =
|
|
process.env.NODE_ENV === "development"
|
|
? `ws://localhost:3000`
|
|
: `ws://${window.location.host}`;
|
|
|
|
setConnecting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
setMyName(name);
|
|
setMyRole(role());
|
|
const sessionId = stream.sessionId || "default";
|
|
setSessionId(sessionId);
|
|
await hydrateFromServer(sessionId);
|
|
await connectStream(sessionId, brokerUrl);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : "Connection failed";
|
|
setError(msg);
|
|
} finally {
|
|
setConnecting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Show
|
|
when={!autoJoined() || error()}
|
|
fallback={
|
|
<p class="text-sm text-gray-500 text-center">
|
|
{connecting() ? "正在加入会话…" : "已加入!"}
|
|
</p>
|
|
}
|
|
>
|
|
<div class="w-full max-w-sm space-y-3">
|
|
<p class="text-sm text-gray-600 text-center">
|
|
请输入你的名字和角色来加入会话。
|
|
<br />
|
|
<span class="text-xs text-gray-400">
|
|
连接到 {window.location.host}
|
|
</span>
|
|
</p>
|
|
<input
|
|
type="text"
|
|
value={playerName()}
|
|
onInput={(e) => setPlayerName(e.currentTarget.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && handleConnect()}
|
|
placeholder="你的名字"
|
|
class="w-full border border-gray-300 rounded px-3 py-1.5 text-sm"
|
|
autofocus
|
|
/>
|
|
{/* Role selector */}
|
|
<div class="flex gap-1">
|
|
{(["gm", "player", "observer"] as const).map((r) => (
|
|
<button
|
|
onClick={() => setRole(r)}
|
|
class={`flex-1 rounded px-2 py-1.5 text-xs font-medium border transition-colors ${
|
|
role() === r
|
|
? "bg-blue-600 text-white border-blue-600"
|
|
: "bg-white text-gray-600 border-gray-300 hover:border-gray-400"
|
|
}`}
|
|
>
|
|
{r === "gm" ? "主持人" : r === "player" ? "玩家" : "观察者"}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
onClick={handleConnect}
|
|
disabled={connecting() || !playerName().trim()}
|
|
class="w-full bg-blue-600 text-white rounded px-3 py-2 text-sm font-medium
|
|
hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
{connecting() ? "连接中..." : "加入"}
|
|
</button>
|
|
<Show when={error()}>
|
|
<p class="text-red-500 text-sm text-center">{error()}</p>
|
|
</Show>
|
|
</div>
|
|
</Show>
|
|
);
|
|
};
|