Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/electron-desktop-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ electron/
- Editor window: loads the AppShell SPA over a local loopback HTTP server (`src/ElectronApp/src/static-server.ts`) — not `file://` (fetch of `.wasm`/`.dll` assets over `file://` hits CORS/mime-type issues in Chromium) and not a custom protocol. The server binds `127.0.0.1:0` (random free port, same trick as LocalPlayer's own port selection below) and serves the same three-directory layout `deploy-play.yml` already produces for play.questviva.com — `editor/` (AppShell build) at `/`, `AppBundle/` (WasmEditor) at `/AppBundle/`, `player/` (WasmPlayer, Phase 1) or LocalPlayer (Phase 2) at `/player/`.
- Player windows: Phase 1 navigates a second `BrowserWindow` to `http://127.0.0.1:{port}/player/` (bundled WasmPlayer); Phase 2 switches this to LocalPlayer's `http://localhost:{port}/?game={encodedPath}`.
- Phase 1's Preview button needs no IPC round-trip at all: `previewInWasmPlayer()` (`editor-store.ts`) already does a plain `window.open('/player/?source=editor', ...)` and talks to it over `BroadcastChannel('quest-preview')` — both work unchanged against the loopback origin. `editorWindow.webContents.setWindowOpenHandler` in `main.ts` just needs to `{ action: "allow" }` same-origin `/player/` popups (and send anything else to `shell.openExternal`) for that `window.open` call to become the player `BrowserWindow`.
- **File associations**: `package.json`'s `build.fileAssociations` registers `.aslx` (role `Editor`) and `.quest`/`.asl`/`.cas` (role `Viewer`) with the OS, so double-clicking one of these launches or focuses the app. `.aslx` routes to the editor (`/open?action=open-recent&...`, reusing the same query shape as the native "Open Recent" submenu); `.quest`/`.asl`/`.cas` route to `PlayCatalog.svelte`'s Play flow (`/?action=play-file&...`). `app.requestSingleInstanceLock()` plus a `second-instance` handler in `main.ts` make a second double-click while the app is already running focus the existing window instead of spawning another — Windows/Linux pass the file path via argv (cold start: `process.argv`; already-running: the `second-instance` event's argv), macOS via the `open-file` app event (registered before `whenReady()`, since it can fire before `ready`). A cold-start open is folded straight into the editor window's initial URL rather than sent over IPC, since a `webContents.send()` right after constructing the window would race the page's own `onMount` listeners and get dropped; an already-running instance uses IPC (`open-recent-game` / the new `open-play-file` channel) since those listeners are already wired up.
- **File associations**: `package.json`'s `build.fileAssociations` registers `.aslx` (role `Editor`) and `.quest`/`.asl`/`.cas` (role `Viewer`) with the OS, so double-clicking one of these launches or focuses the app. `.aslx` routes to the editor (`/open?action=open-recent&...`, reusing the same query shape as the native "Open Recent" submenu); `.quest`/`.asl`/`.cas` route to `play/local/+page.svelte`'s Play flow (`/play/local?action=play-file&...`). `app.requestSingleInstanceLock()` plus a `second-instance` handler in `main.ts` make a second double-click while the app is already running focus the existing window instead of spawning another — Windows/Linux pass the file path via argv (cold start: `process.argv`; already-running: the `second-instance` event's argv), macOS via the `open-file` app event (registered before `whenReady()`, since it can fire before `ready`). A cold-start open is folded straight into the editor window's initial URL rather than sent over IPC, since a `webContents.send()` right after constructing the window would race the page's own `onMount` listeners and get dropped; an already-running instance uses IPC (`open-recent-game` / the new `open-play-file` channel) since those listeners are already wired up.

### contextBridge API (`preload.ts`)

Expand Down
41 changes: 41 additions & 0 deletions src/AppShell/src/components/GameCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import { base } from "$app/paths";
import { languageName, type CatalogGame } from "$lib/home-catalog";

let { game }: { game: CatalogGame } = $props();

function ratingStars(rating: number): string {
const rounded = Math.max(0, Math.min(5, Math.round(rating)));
return "★".repeat(rounded) + "☆".repeat(5 - rounded);
}
</script>

<a
href="{base}/play/{game.id}"
class="flex flex-col rounded-lg border border-surface-800 overflow-hidden hover:border-primary-500 transition-colors"
>
<div class="aspect-[3/4] bg-surface-800 flex items-center justify-center overflow-hidden">
{#if game.cover || game.thumbnail}
<img src={game.cover ?? game.thumbnail} alt="" loading="lazy" class="w-full h-full object-cover" />
{/if}
</div>
<div class="p-2 flex flex-col gap-1">
<div class="text-sm font-semibold truncate">{game.name}</div>
{#if game.author}
<div class="text-xs text-surface-400 truncate">by {game.author}</div>
{/if}
<div class="flex flex-wrap gap-1">
<span class="text-[10px] leading-none px-1.5 py-1 rounded-full bg-surface-800 text-surface-300">
{game.isGamebook ? "Gamebook" : "Text Adventure"}
</span>
{#if game.language !== "en"}
<span class="text-[10px] leading-none px-1.5 py-1 rounded-full bg-surface-800 text-surface-300">
{languageName(game.language)}
</span>
{/if}
</div>
{#if game.rating > 0}
<div class="text-xs text-primary-500">{ratingStars(game.rating)}</div>
{/if}
</div>
</a>
21 changes: 21 additions & 0 deletions src/AppShell/src/components/GamesPager.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
let { page, pageCount, onPage }: { page: number; pageCount: number; onPage: (page: number) => void } = $props();
</script>

{#if pageCount > 1}
<div class="flex items-center justify-center gap-3">
<button
type="button"
class="btn btn-sm preset-outlined-surface-500"
disabled={page <= 1}
onclick={() => onPage(page - 1)}
>&larr; Prev</button>
<span class="text-sm text-surface-400">Page {page} of {pageCount}</span>
<button
type="button"
class="btn btn-sm preset-outlined-surface-500"
disabled={page >= pageCount}
onclick={() => onPage(page + 1)}
>Next &rarr;</button>
</div>
{/if}
Loading
Loading