Skip to content

feat: add solo mode#7

Merged
GilbN merged 14 commits into
mainfrom
claude/issue-5-20260409-2200
Apr 12, 2026
Merged

feat: add solo mode#7
GilbN merged 14 commits into
mainfrom
claude/issue-5-20260409-2200

Conversation

@GilbN
Copy link
Copy Markdown
Owner

@GilbN GilbN commented Apr 9, 2026

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

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>
Copilot AI review requested due to automatic review settings April 9, 2026 22:13
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
opk-timer Ready Ready Preview, Comment Apr 12, 2026 9:57am

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 isSolo in roomState and persists it via saveRoomState/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-memory roomState. Because SocketHost/SocketClient later call roomState.update((s) => ({ ...s, ... })), a stale isSolo: true can leak into newly created/joined multiplayer rooms (showing the Solo UI and affecting conditional logic). Reset roomState to defaults (including isSolo: 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.

Comment on lines 53 to 61
<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>
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 180 to 186
<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}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
github-actions Bot and others added 11 commits April 9, 2026 22:21
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.
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>
@GilbN GilbN merged commit 2060e9e into main Apr 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add solo mode

2 participants