handle series

This commit is contained in:
2026-05-17 15:37:00 -04:00
parent b7fb0b1043
commit 62c3deddaf
10 changed files with 346 additions and 81 deletions

View File

@@ -7,20 +7,27 @@ export const load: PageServerLoad = async ({ parent }) => {
const db = getDb();
scanLibraries(db);
const games =
loggedIn
? db
.prepare(
`SELECT id, slug, title, library, has_cover, has_wide, genre
FROM games ORDER BY title ASC`
)
.all()
: db
.prepare(
`SELECT id, slug, title, library, has_cover, has_wide, genre
FROM games WHERE library = 'public' ORDER BY title ASC`
)
.all();
const games = loggedIn
? db
.prepare(
`SELECT id, slug, title, library, has_cover, has_wide, genre
FROM games WHERE series_id IS NULL ORDER BY title ASC`
)
.all()
: db
.prepare(
`SELECT id, slug, title, library, has_cover, has_wide, genre
FROM games WHERE series_id IS NULL AND library = 'public' ORDER BY title ASC`
)
.all();
return { games };
const series = loggedIn
? db.prepare(`SELECT id, slug, title, library, has_cover FROM series ORDER BY title ASC`).all()
: db
.prepare(
`SELECT id, slug, title, library, has_cover FROM series WHERE library = 'public' ORDER BY title ASC`
)
.all();
return { games, series };
};

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { PageData } from './$types';
import GameGrid from '$lib/components/GameGrid.svelte';
import type { Game } from '$lib/types';
import type { Game, Series } from '$lib/types';
let { data }: { data: PageData } = $props();
let filter = $state('');
@@ -20,5 +20,10 @@
class="w-full max-w-md px-4 py-2 rounded-lg bg-gray-800 border border-gray-700 text-gray-100 placeholder-gray-400 focus:outline-none focus:border-purple-500 transition-colors"
/>
</div>
<GameGrid games={data.games as Game[]} loggedIn={data.loggedIn} {filter} />
<GameGrid
games={data.games as Game[]}
series={data.series as Series[]}
loggedIn={data.loggedIn}
{filter}
/>
</div>

View File

@@ -0,0 +1,34 @@
import type { RequestHandler } from './$types';
import { getDb } from '$lib/server/db';
import { getSession } from '$lib/server/auth';
import { findCoverFile } from '$lib/server/files';
import { contentType } from '$lib/utils';
import fs from 'node:fs';
import { Readable } from 'node:stream';
import type { Series } from '$lib/types';
export const GET: RequestHandler = ({ params, cookies }) => {
const db = getDb();
const series = db
.prepare('SELECT * FROM series WHERE slug = ?')
.get(params.slug) as Series | undefined;
if (!series) return new Response('Not found', { status: 404 });
if (series.library === 'private' && !getSession(cookies)) {
return new Response('Unauthorized', { status: 401 });
}
const coverPath = findCoverFile(series.folder_path);
if (!coverPath) return new Response('No cover image', { status: 404 });
const mime = contentType(coverPath);
const nodeStream = fs.createReadStream(coverPath);
const webStream = Readable.toWeb(nodeStream) as ReadableStream;
return new Response(webStream, {
headers: {
'Content-Type': mime,
'Cache-Control': 'public, max-age=86400'
}
});
};

View File

@@ -0,0 +1,25 @@
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import { getDb } from '$lib/server/db';
import { getSession } from '$lib/server/auth';
import type { Series } from '$lib/types';
export const load: PageServerLoad = async ({ params, cookies, parent }) => {
await parent();
const db = getDb();
const series = db
.prepare('SELECT * FROM series WHERE slug = ?')
.get(params.slug) as Series | undefined;
if (!series) error(404, 'Series not found');
if (series.library === 'private' && !getSession(cookies)) error(401, 'Unauthorized');
const games = db
.prepare(
`SELECT id, slug, title, library, has_cover, has_wide, genre
FROM games WHERE series_id = ? ORDER BY title ASC`
)
.all(series.id);
return { series, games };
};

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import type { PageData } from './$types';
import GameGrid from '$lib/components/GameGrid.svelte';
import type { Game } from '$lib/types';
let { data }: { data: PageData } = $props();
let filter = $state('');
</script>
<svelte:head>
<title>{data.series.title} — Game Grid</title>
</svelte:head>
<div class="max-w-screen-2xl mx-auto px-4 py-6">
<div class="mb-6">
<a href="/" class="text-gray-400 hover:text-white text-sm transition-colors">← Back</a>
<h1 class="mt-3 text-2xl font-bold text-white">{data.series.title}</h1>
</div>
<div class="mb-6">
<input
type="text"
placeholder="Search games..."
bind:value={filter}
class="w-full max-w-md px-4 py-2 rounded-lg bg-gray-800 border border-gray-700 text-gray-100 placeholder-gray-400 focus:outline-none focus:border-purple-500 transition-colors"
/>
</div>
<GameGrid games={data.games as Game[]} loggedIn={data.loggedIn} {filter} />
</div>