133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
import { ApiError } from '$lib/api/client';
|
|
import {
|
|
listMyDistinctPageTags,
|
|
listMyPageTags,
|
|
listTaggedChapters,
|
|
listTaggedMangas,
|
|
searchPages,
|
|
CONTENT_WARNINGS,
|
|
type ContentWarning,
|
|
type PageSearchItem,
|
|
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';
|
|
|
|
/** Parse a CSV of content warnings, keeping only valid vocabulary values. */
|
|
function parseWarnings(raw: string | null): ContentWarning[] {
|
|
if (!raw) return [];
|
|
return raw
|
|
.split(',')
|
|
.map((s) => s.trim().toLowerCase())
|
|
.filter((s): s is ContentWarning =>
|
|
(CONTENT_WARNINGS as string[]).includes(s)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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=` drives OCR full-text search against the page OCR index (the
|
|
* active OCR backend populates it). A non-empty `text` (or an included
|
|
* content-warning) switches the page into content-search mode below.
|
|
*/
|
|
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';
|
|
|
|
// Content-search inputs (OCR/scene text + content-warning filters).
|
|
const text = (url.searchParams.get('text') ?? '').trim();
|
|
const cwInclude = parseWarnings(url.searchParams.get('cw_include'));
|
|
const cwExclude = parseWarnings(url.searchParams.get('cw_exclude'));
|
|
// A positive filter (text or an included warning) is required for the
|
|
// backend; cw_exclude alone can't drive a search.
|
|
const contentSearch = text !== '' || cwInclude.length > 0;
|
|
|
|
const empty = {
|
|
authenticated: true,
|
|
tag,
|
|
view,
|
|
order,
|
|
text,
|
|
cwInclude,
|
|
cwExclude,
|
|
contentSearch,
|
|
distinct: [] as PageTagSummary[],
|
|
pages: [] as TaggedPageItem[],
|
|
chapters: [] as TaggedChapterAggregate[],
|
|
mangas: [] as TaggedMangaAggregate[],
|
|
results: [] as PageSearchItem[],
|
|
total: 0,
|
|
error: null as string | null
|
|
};
|
|
|
|
try {
|
|
const distinct = await listMyDistinctPageTags(undefined, 100);
|
|
|
|
// Content search takes precedence over tag browsing.
|
|
if (contentSearch) {
|
|
const r = await searchPages({
|
|
text: text || undefined,
|
|
cwInclude,
|
|
cwExclude,
|
|
limit: 100
|
|
});
|
|
return { ...empty, distinct, results: r.items, total: r.page.total ?? 0 };
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
};
|