Add the /search surface (Pages / Chapters / Mangas tabs) backed by per-user page tags and per-page collections: schema (migration 0023), backend endpoints for page tags/collections and tagged-page aggregations (with the OCR text-search param reserved at 501), plus the frontend API clients, library Page-tags tab, collection page sections, page context menu / AddTagsSheet, and reader long-press wiring. Includes the continuous-reader navigation fixes (?page=N handling, chapter-reset timing, back-button pops history) and tag-normalization hardening accumulated on the branch. Bump version 0.60.2 -> 0.62.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
import { ApiError } from '$lib/api/client';
|
|
import {
|
|
listMyDistinctPageTags,
|
|
listMyPageTags,
|
|
listTaggedChapters,
|
|
listTaggedMangas,
|
|
type PageTagSummary,
|
|
type TaggedChapterAggregate,
|
|
type TaggedMangaAggregate,
|
|
type TaggedPageItem
|
|
} from '$lib/api/page_tags';
|
|
import type { PageLoad } from './$types';
|
|
|
|
export const ssr = false;
|
|
|
|
type View = 'pages' | 'chapters' | 'mangas';
|
|
type Order = 'desc' | 'asc';
|
|
|
|
/**
|
|
* Loader for the /search page. URL is the source of truth — refresh
|
|
* or share a link lands the user on the same view.
|
|
*
|
|
* - `?tag=` exact-match tag filter. Empty → no results, just the
|
|
* chip cloud for browsing.
|
|
* - `?view=pages|chapters|mangas` — defaults to `pages` when omitted.
|
|
* - `?order=desc|asc` — only meaningful for chapters/mangas tabs.
|
|
* `desc` (most matches first) is the default.
|
|
*
|
|
* `?text=` is reserved for the planned OCR text-search input. The
|
|
* backend rejects it with 501 + stable code
|
|
* `text_search_not_yet_supported` today; the frontend never sets it.
|
|
*/
|
|
export const load: PageLoad = async ({ url }) => {
|
|
const tag = url.searchParams.get('tag');
|
|
const viewParam = url.searchParams.get('view');
|
|
const view: View =
|
|
viewParam === 'chapters' || viewParam === 'mangas' ? viewParam : 'pages';
|
|
const order: Order = url.searchParams.get('order') === 'asc' ? 'asc' : 'desc';
|
|
|
|
const empty = {
|
|
authenticated: true,
|
|
tag,
|
|
view,
|
|
order,
|
|
distinct: [] as PageTagSummary[],
|
|
pages: [] as TaggedPageItem[],
|
|
chapters: [] as TaggedChapterAggregate[],
|
|
mangas: [] as TaggedMangaAggregate[],
|
|
total: 0,
|
|
error: null as string | null
|
|
};
|
|
|
|
try {
|
|
const distinct = await listMyDistinctPageTags(undefined, 100);
|
|
// No tag selected → just the chip cloud.
|
|
if (!tag) return { ...empty, distinct };
|
|
|
|
if (view === 'chapters') {
|
|
const r = await listTaggedChapters({ tag, order, limit: 100 });
|
|
return {
|
|
...empty,
|
|
distinct,
|
|
chapters: r.items,
|
|
total: r.page.total ?? 0
|
|
};
|
|
}
|
|
if (view === 'mangas') {
|
|
const r = await listTaggedMangas({ tag, order, limit: 100 });
|
|
return {
|
|
...empty,
|
|
distinct,
|
|
mangas: r.items,
|
|
total: r.page.total ?? 0
|
|
};
|
|
}
|
|
const r = await listMyPageTags({ tag, limit: 100 });
|
|
return {
|
|
...empty,
|
|
distinct,
|
|
pages: r.items,
|
|
total: r.page.total ?? 0
|
|
};
|
|
} catch (e) {
|
|
if (e instanceof ApiError && e.status === 401) {
|
|
return { ...empty, authenticated: false };
|
|
}
|
|
if (e instanceof ApiError) {
|
|
return { ...empty, error: e.message };
|
|
}
|
|
throw e;
|
|
}
|
|
};
|