diff --git a/src/components/journal/ConnectDialog.tsx b/src/components/journal/ConnectDialog.tsx index 797737a..30612ec 100644 --- a/src/components/journal/ConnectDialog.tsx +++ b/src/components/journal/ConnectDialog.tsx @@ -1,27 +1,84 @@ /** - * ConnectDialog — name + role picker to join a session + * ConnectDialog — broker connection + session lobby (list / create / join) + * + * The broker connection and the active session are decoupled: this dialog + * connects to the broker, which makes the session list available, and joining + * or creating a session is a separate step that reuses that connection. */ -import { Component, createSignal, onMount, Show } from "solid-js"; +import { Component, createSignal, For, onMount, Show } from "solid-js"; import { - connectStream, - hydrateFromServer, - useJournalStream, + connectBroker, + createSession, + joinSession, + resolveBrokerUrl, + sessions, setMyName, setMyRole, - setSessionId, + useJournalStream, } from "../stores/journalStream"; export const ConnectDialog: Component = () => { const stream = useJournalStream(); + const manifest = sessions; const [playerName, setPlayerName] = createSignal(stream.myName); const [role, setRole] = createSignal<"gm" | "player" | "observer">( stream.myRole, ); - const [connecting, setConnecting] = createSignal(false); + const [newSessionName, setNewSessionName] = createSignal(""); + const [busy, setBusy] = createSignal(false); const [error, setError] = createSignal(null); const [autoJoined, setAutoJoined] = createSignal(false); - // Auto-join when URL has both session and player params + const ensureBroker = async () => { + if (stream.connected) return; + await connectBroker(resolveBrokerUrl()); + }; + + const identity = () => ({ + name: playerName().trim() || stream.myName, + role: role(), + }); + + const applyIdentity = () => { + const { name, role: r } = identity(); + setMyName(name); + setMyRole(r); + }; + + const handleJoin = async (sessionId: string) => { + setBusy(true); + setError(null); + try { + applyIdentity(); + await ensureBroker(); + await joinSession(sessionId, identity()); + } catch (e) { + setError(e instanceof Error ? e.message : "Connection failed"); + } finally { + setBusy(false); + } + }; + + const handleCreate = async () => { + const name = newSessionName().trim(); + if (!name) return; + setBusy(true); + setError(null); + try { + applyIdentity(); + await ensureBroker(); + const id = await createSession(name); + if (id) await joinSession(id, identity()); + setNewSessionName(""); + } catch (e) { + setError(e instanceof Error ? e.message : "Connection failed"); + } finally { + setBusy(false); + } + }; + + // Auto-join when the URL carries session + player + autojoin params; + // otherwise enter the lobby so the session list is available up front. onMount(() => { const params = new URL(window.location.href).searchParams; if ( @@ -32,51 +89,26 @@ export const ConnectDialog: Component = () => { setPlayerName(params.get("player") || ""); setRole("player"); setAutoJoined(true); - handleConnect(); + handleJoin(params.get("session")!); + return; } + ensureBroker().catch((e) => { + setError(e instanceof Error ? e.message : "Connection failed"); + }); }); - 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 ( - {connecting() ? "正在加入会话…" : "已加入!"} + {busy() ? "正在加入会话…" : "已加入!"}

} >

- 请输入你的名字和角色来加入会话。 + 输入你的名字和角色,选择或新建一个会话。
连接到 {window.location.host} @@ -86,7 +118,7 @@ export const ConnectDialog: Component = () => { type="text" value={playerName()} onInput={(e) => setPlayerName(e.currentTarget.value)} - onKeyDown={(e) => e.key === "Enter" && handleConnect()} + onKeyDown={(e) => e.key === "Enter" && handleCreate()} placeholder="你的名字" class="w-full border border-gray-300 rounded px-3 py-1.5 text-sm" autofocus @@ -106,14 +138,52 @@ export const ConnectDialog: Component = () => { ))}

- + + {/* Session list */} +
+

会话

+
+ + {([id, meta]) => ( + + )} + + +

+ {stream.connected ? "还没有会话" : "正在加载会话…"} +

+
+
+
+ + {/* Create a new session and join it (GM only) */} + +
+ setNewSessionName(e.currentTarget.value)} + onKeyDown={(e) => e.key === "Enter" && handleCreate()} + placeholder="新会话名称" + class="flex-1 border border-gray-300 rounded px-2 py-1.5 text-sm" + /> + +
+
+

{error()}

diff --git a/src/components/journal/ConnectionBar.tsx b/src/components/journal/ConnectionBar.tsx deleted file mode 100644 index e9cf893..0000000 --- a/src/components/journal/ConnectionBar.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/** - * ConnectionBar — session picker, player info (connected state only) - */ - -import { Component, createSignal, Show, For } from "solid-js"; -import { - hydrateFromServer, - useJournalStream, - sessions, - createSession, - deleteSession, - disconnectStream, - setSessionId, -} from "../stores/journalStream"; -import { journalSetState } from "../stores/journalStream"; - -export const ConnectionBar: Component = () => { - const stream = useJournalStream(); - const [newSessionName, setNewSessionName] = createSignal(""); - const [showNewSession, setShowNewSession] = createSignal(false); - const [error, setError] = createSignal(null); - - const manifest = sessions; - - const handleCreateSession = () => { - const name = newSessionName().trim(); - if (!name) return; - const id = createSession(name); - if (id) { - setSessionId(id); - hydrateFromServer(id).catch(() => {}); - } - setNewSessionName(""); - setShowNewSession(false); - }; - - const handleDeleteSession = (id: string) => { - if (confirm(`确定要删除会话 "${id}"?此操作不可撤销。`)) { - deleteSession(id); - } - }; - - const handleSessionSelect = async (id: string) => { - try { - await hydrateFromServer(id); - setSessionId(id); - } catch (e) { - setError("无法加载会话"); - } - }; - - const handleDisconnect = () => { - disconnectStream(); - }; - - return ( -
- {/* Status row */} -
- - 已连接 - - - {stream.sessionName || stream.sessionId} - - -
- - 扮演 {stream.myName} - - -
- - {/* GM session management */} - -
-
- 会话列表: - -
- - -
- setNewSessionName(e.currentTarget.value)} - placeholder="会话名称" - class="flex-1 border border-gray-300 rounded px-2 py-0.5 text-xs" - onKeyDown={(e) => e.key === "Enter" && handleCreateSession()} - /> - -
-
- - - {([id, meta]) => ( -
- handleSessionSelect(id)} - > - {meta.name || id} - - -
- )} -
-
-
- - -

{error()}

-
-
- ); -}; diff --git a/src/components/journal/CreateSessionDialog.tsx b/src/components/journal/CreateSessionDialog.tsx index fb23cc4..67b30b8 100644 --- a/src/components/journal/CreateSessionDialog.tsx +++ b/src/components/journal/CreateSessionDialog.tsx @@ -2,7 +2,7 @@ * CreateSessionDialog — modal for naming a new session */ import { Component, createSignal, onMount } from "solid-js"; -import { createSession, setSessionId, hydrateFromServer } from "../stores/journalStream"; +import { createSession, joinSession } from "../stores/journalStream"; interface CreateSessionDialogProps { onClose: () => void; @@ -19,15 +19,16 @@ export const CreateSessionDialog: Component = ( const trimmed = name().trim(); if (!trimmed) return; setCreating(true); - const id = createSession(trimmed); - if (id) { - setSessionId(id); - // Hydrate the new (empty) session so the UI reflects it immediately - await hydrateFromServer(id).catch(() => {}); + try { + const id = await createSession(trimmed); + if (id) await joinSession(id); + } catch (e) { + console.error("[journal] failed to create session:", e); + } finally { + setName(""); + setCreating(false); + props.onClose(); } - setName(""); - setCreating(false); - props.onClose(); }; onMount(() => inputRef?.focus()); diff --git a/src/components/journal/JournalHeader.tsx b/src/components/journal/JournalHeader.tsx index ae37cac..0dd7330 100644 --- a/src/components/journal/JournalHeader.tsx +++ b/src/components/journal/JournalHeader.tsx @@ -1,8 +1,8 @@ /** - * JournalHeader — status dot, player name, session dropdown, disconnect & close + * JournalHeader — status dot, player name, session dropdown, leave & close */ import { Component, Show } from "solid-js"; -import { useJournalStream, disconnectStream } from "../stores/journalStream"; +import { useJournalStream, leaveSession } from "../stores/journalStream"; import { SessionDropdown } from "./SessionDropdown"; interface JournalHeaderProps { @@ -13,7 +13,7 @@ interface JournalHeaderProps { export const JournalHeader: Component = (props) => { const stream = useJournalStream(); - const handleDisconnect = () => disconnectStream(); + const handleLeave = () => leaveSession(); return (
@@ -32,7 +32,9 @@ export const JournalHeader: Component = (props) => { 未连接 +

+ {stream.connected ? "选择会话" : "未连接"} +

} >
@@ -58,13 +60,13 @@ export const JournalHeader: Component = (props) => {
- +