diff --git a/package-lock.json b/package-lock.json index b8c464d..cc5b115 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "solid-js": "^1.9.3", "three": "^0.183.2", "three-3mf-exporter": "^45.1.0", + "ws": "^8.21.0", "yarn-spinner-loader": "^0.1.0", "zod": "^4.4.3" }, @@ -46,6 +47,7 @@ "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", "@types/node": "^22.19.13", + "@types/ws": "^8.18.1", "husky": "^9.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -10693,9 +10695,9 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 838b46e..3896d16 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "solid-js": "^1.9.3", "three": "^0.183.2", "three-3mf-exporter": "^45.1.0", + "ws": "^8.21.0", "yarn-spinner-loader": "^0.1.0", "zod": "^4.4.3" }, @@ -66,6 +67,7 @@ "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", "@types/node": "^22.19.13", + "@types/ws": "^8.18.1", "husky": "^9.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", diff --git a/src/App.tsx b/src/App.tsx index 34ed5bf..b45c644 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -57,13 +57,18 @@ const App: Component = () => { return (
{/* 桌面端固定侧边栏 */} - + setIsDataSourceOpen(true)} + /> {/* 移动端抽屉式侧边栏 */} setIsSidebarOpen(false)} fileTree={fileTree()} pathHeadings={pathHeadings()} + onDataSourceOpen={() => setIsDataSourceOpen(true)} />
@@ -79,19 +84,14 @@ const App: Component = () => {
- - + + + +
@@ -121,6 +132,7 @@ export const MobileSidebar: Component = (props) => { pathHeadings={pathHeadings()} currentPath={location.pathname} onClose={props.onClose} + onDataSourceOpen={props.onDataSourceOpen} /> @@ -133,6 +145,7 @@ export const MobileSidebar: Component = (props) => { export const DesktopSidebar: Component<{ fileTree?: FileNode[]; pathHeadings?: Record; + onDataSourceOpen?: () => void; }> = (props) => { const location = useLocation(); const [selfFileTree, setSelfFileTree] = createSignal([]); @@ -158,6 +171,7 @@ export const DesktopSidebar: Component<{ currentPath={location.pathname} onClose={() => {}} isDesktop + onDataSourceOpen={props.onDataSourceOpen} /> ); diff --git a/src/components/journal/ConnectionBar.tsx b/src/components/journal/ConnectionBar.tsx index 584dcff..6891e5d 100644 --- a/src/components/journal/ConnectionBar.tsx +++ b/src/components/journal/ConnectionBar.tsx @@ -1,65 +1,26 @@ /** - * ConnectionBar — connect form, session picker, player name - * - * Three states: disconnected → connecting → connected + * ConnectionBar — session picker, player info (connected state only) */ -import { Component, createSignal, Show, For, onMount } from "solid-js"; +import { Component, createSignal, Show, For } from "solid-js"; import { - connectStream, hydrateFromServer, useJournalStream, sessions, - setMyName, createSession, deleteSession, + disconnectStream, } from "../stores/journalStream"; -import type { SessionMeta } from "../stores/journalStream"; -import { createMemo } from "solid-js"; - -// Direct access to the store's setter for session switching import { journalSetState } from "../stores/journalStream"; export const ConnectionBar: Component = () => { const stream = useJournalStream(); - const [brokerUrl, setBrokerUrl] = createSignal(""); const [newSessionName, setNewSessionName] = createSignal(""); - const [playerName, setPlayerName] = createSignal(""); - const [connecting, setConnecting] = createSignal(false); - const [error, setError] = createSignal(null); const [showNewSession, setShowNewSession] = createSignal(false); - - onMount(() => { - setBrokerUrl(stream.brokerUrl ?? ""); - setPlayerName(stream.myName); - }); + const [error, setError] = createSignal(null); const manifest = sessions; - const handleConnect = async () => { - const url = brokerUrl().trim(); - if (!url) return; - - setConnecting(true); - setError(null); - - try { - // Save name first - if (playerName().trim()) { - setMyName(playerName().trim()); - } - - // Connect - const sessionId = stream.sessionId || "default"; - await hydrateFromServer(sessionId); - await connectStream(sessionId, url); - } catch (e) { - setError(e instanceof Error ? e.message : "Connection failed"); - } finally { - setConnecting(false); - } - }; - const handleCreateSession = () => { const name = newSessionName().trim(); if (!name) return; @@ -75,7 +36,6 @@ export const ConnectionBar: Component = () => { }; const handleSessionSelect = async (id: string) => { - if (!stream.connected) return; try { await hydrateFromServer(id); journalSetState("sessionId", id); @@ -84,74 +44,36 @@ export const ConnectionBar: Component = () => { } }; - const statusColor = () => - stream.connected - ? "bg-green-500" - : connecting() - ? "bg-yellow-400" - : "bg-gray-400"; + const handleDisconnect = () => { + disconnectStream(); + }; return ( -
- {/* Status + session name */} +
+ {/* Status row */}
- - - {stream.connected - ? "connected" - : connecting() - ? "connecting..." - : "disconnected"} - + + connected {stream.sessionId}
- - + + Playing as {stream.myName} +
- {/* Disconnected: connect form */} - -
- setBrokerUrl(e.currentTarget.value)} - placeholder="MQTT broker (e.g. tcp://192.168.1.5:1883)" - class="w-full border border-gray-300 rounded px-2 py-1 text-xs" - /> - setPlayerName(e.currentTarget.value)} - placeholder="Your name (GM or player)" - class="w-full border border-gray-300 rounded px-2 py-1 text-xs" - /> - - -

{error()}

-
-
-
- - {/* Connected: session management (GM only) */} - + {/* GM session management */} +
Sessions: @@ -213,12 +135,8 @@ export const ConnectionBar: Component = () => {
- {/* Connected: player info (player only) */} - -
- Playing as: - {stream.myName} -
+ +

{error()}

); diff --git a/src/components/journal/JournalPanel.tsx b/src/components/journal/JournalPanel.tsx index 1787711..9ffdc73 100644 --- a/src/components/journal/JournalPanel.tsx +++ b/src/components/journal/JournalPanel.tsx @@ -1,11 +1,17 @@ /** * JournalPanel — right panel container for the journal stream * - * Collapsible, overlays on mobile, pushes content on desktop. + * Fixed overlay. Shows stream when connected, connect dialog when not. + * Auto-connects to ws://{current host}:{current port} — no URL input needed. */ -import { Component, Show, createSignal } from "solid-js"; -import { useJournalStream } from "../stores/journalStream"; +import { Component, Show, createSignal, onMount } from "solid-js"; +import { + connectStream, + hydrateFromServer, + useJournalStream, + setMyName, +} from "../stores/journalStream"; import { ConnectionBar } from "./ConnectionBar"; import { StreamView } from "./StreamView"; import { ComposePanel } from "./ComposePanel"; @@ -20,22 +26,25 @@ export const JournalPanel: Component = (props) => { return ( - {/* Mobile overlay */}
+ } + > + +
+ +
); }; + +// --------------------------------------------------------------------------- +// Connect dialog — one input: player name. Broker URL is always current host. +// --------------------------------------------------------------------------- + +const ConnectDialog: Component = () => { + const stream = useJournalStream(); + const [playerName, setPlayerName] = createSignal(stream.myName); + const [connecting, setConnecting] = createSignal(false); + const [error, setError] = createSignal(null); + + // Auto-connect if we have a name from localStorage + onMount(() => { + if (stream.myName && stream.myName !== "gm") { + handleConnect(); + } + }); + + const handleConnect = async () => { + const name = playerName().trim() || stream.myName; + if (!name) return; + + const brokerUrl = `ws://${window.location.host}`; + + setConnecting(true); + setError(null); + + try { + setMyName(name); + const sessionId = stream.sessionId || "default"; + await hydrateFromServer(sessionId); + await connectStream(sessionId, brokerUrl); + } catch (e) { + setError(e instanceof Error ? e.message : "Connection failed"); + } finally { + setConnecting(false); + } + }; + + return ( +
+

+ Enter your name to join the session. +
+ + Connecting to {window.location.host} + +

+ setPlayerName(e.currentTarget.value)} + onKeyDown={(e) => e.key === "Enter" && handleConnect()} + placeholder="Your name (GM or player)" + class="w-full border border-gray-300 rounded px-3 py-1.5 text-sm" + autofocus + /> + + +

{error()}

+
+
+ ); +};