feat: add solo mode#7
Conversation
Adds a Solo button on the home screen that launches the full competition timer (with program selection, all stage types, and host controls) without requiring a WebSocket connection or room code. Solo sessions persist across page reloads just like host sessions. Co-authored-by: GilbN <GilbN@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds a “Solo” mode that lets users run the full competition timer locally (no WebSocket / room code), with session persistence across reloads.
Changes:
- Introduces
isSoloinroomStateand persists it viasaveRoomState/loadRoomState. - Adds a Solo entry point from the Home screen and displays a “Solo” badge in Lobby/Timer top bars.
- Extends session restore logic to restore Solo sessions without creating socket connections.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/views/TimerView.svelte |
Persists isSolo on program change and shows a Solo badge in the top bar. |
src/views/LobbyView.svelte |
Persists isSolo, adjusts disconnect/history behavior for Solo, and shows a Solo badge. |
src/views/HomeView.svelte |
Adds a Solo button and initializes a Solo session (no room code). |
src/lib/stores.js |
Adds isSolo to the roomState store shape. |
src/lib/i18n.js |
Adds soloMode translations. |
src/App.svelte |
Restores Solo sessions by initializing scheduler/timer state without WebSockets. |
Comments suppressed due to low confidence (1)
src/views/LobbyView.svelte:48
- In solo mode,
disconnect()clears localStorage but never resets the in-memoryroomState. BecauseSocketHost/SocketClientlater callroomState.update((s) => ({ ...s, ... })), a staleisSolo: truecan leak into newly created/joined multiplayer rooms (showing the Solo UI and affecting conditional logic). ResetroomStateto defaults (includingisSolo: false) when disconnecting (ideally unconditionally, after tearing down any host/client).
function disconnect() {
if (!confirm(get(t)('confirmDisconnect'))) return
if ($roomState.code && !$roomState.isSolo) {
addRoomToHistory({ code: $roomState.code, isHost: true })
}
if (window.__opkHost) {
window.__opkHost.destroy()
window.__opkHost = null
}
clearRoomState()
currentView.set('home')
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <div class="top-bar"> | ||
| <RoomCode code={$roomState.code} /> | ||
| <div class="top-bar-right"> | ||
| {#if $roomState.isSolo} | ||
| <div class="solo-badge">{$t('soloMode')}</div> | ||
| {:else} | ||
| <RoomCode code={$roomState.code} /> | ||
| <ConnectionStatus status="connected" /> | ||
| <SettingsMenu /> | ||
| </div> | ||
| {/if} | ||
| <SettingsMenu /> | ||
| </div> |
There was a problem hiding this comment.
top-bar uses justify-content: space-between, but in the non-solo branch it now renders three siblings (RoomCode, ConnectionStatus, SettingsMenu). This will visually push ConnectionStatus into the middle instead of keeping it grouped on the right. Wrap the right-side controls in a container (as before) or apply margin-left: auto to a right-side group so alignment stays consistent.
| <div class="top-bar"> | ||
| {#if $roomState.code} | ||
| {#if $roomState.isSolo} | ||
| <div class="solo-badge">{$t('soloMode')}</div> | ||
| {:else if $roomState.code} | ||
| <RoomCode code={$roomState.code} /> | ||
| {/if} | ||
| {#if initialIsClient} |
There was a problem hiding this comment.
The Solo badge rendering depends on roomState.isSolo, but disconnect() only clears persisted room state; it does not guarantee the in-memory roomState is reset when exiting Solo. Since the socket implementations update roomState via object spread, any stale isSolo: true can carry into later sessions. Ensure the disconnect flow resets roomState to defaults (including isSolo: false) when returning to Home.
Adds a dedicated full-screen display view designed for tablets and monitors at the range. Joining as a display screen connects read-only with no name or lane required, and renders a landscape-optimised layout with a massive countdown, target status indicator, and stage/series info — no controls. - server: accept `role: 'spectator'` in JOIN_ROOM; skip PEER_JOINED/PEER_LEFT notifications to host so display screens are invisible to shooters list - SocketClient: pass `role` in JOIN_ROOM; set `isSpectator` on roomState - stores: add `isSpectator` to roomState; add 'display' to currentView - DisplayView.svelte: new full-screen view with portrait + landscape layouts; uses clamp() for giant digits that scale from phones to 4K monitors; always holds wake lock; shows phase, target, reshoot, and stage info - HomeView: add "Display Screen" toggle that expands a code-only join form; rejoin history shows "Spectator" label and restores to display view - App.svelte: route 'display' view; restore display sessions on page reload - i18n: add spectator/display mode translation keys (no + en) Closes #4 Co-authored-by: GilbN <GilbN@users.noreply.github.com>
- Server: spectators no longer bypass lane enforcement — isLaneTaken
skips spectator entries and handleJoinRoom forces lane='' for
spectators so a spoofed client can't reserve a lane.
- Server: handleRelay drops client→host traffic from connections
flagged ws._isSpectator, keeping spectators strictly read-only.
- SocketClient.destroy() now resets roomState with isSpectator: false
so the store shape stays consistent with its initial value.
- HomeView: joinDisplay, joinRoom, and rejoinRoom catch paths call
destroy() on the host/client before nulling the globals, preventing
leaked half-open WebSockets when a connection attempt fails.
…nnect. Adjust specator mode text size.
…plete and phase banners
Adds a Solo button on the home screen that launches the full competition timer (with program selection, all stage types, and host controls) without requiring a WebSocket connection or room code. Solo sessions persist across page reloads just like host sessions. Co-authored-by: GilbN <GilbN@users.noreply.github.com>
…bN/opk-timer into claude/issue-5-20260409-2200
…r environment handling
Adds a Solo button on the home screen that launches the full competition timer (with program selection, all stage types, and host controls) without requiring a WebSocket connection or room code.
Solo sessions persist across page reloads. No peer list, no room code, just the full timer for single-device practice.
Closes #5
Generated with Claude Code