feat(search): frontend content search (text + warnings) + manga warning banner

Surfaces the analysis pipeline in the UI:

- api clients: searchPages() + PageSearchItem/ContentWarning types;
  listMangas cwInclude/cwExclude; MangaDetail.content_warnings.
- /search gains a content-search mode: free-text over OCR+scene and
  tri-state content-warning toggles (include/exclude), driven by URL
  params (text, cw_include, cw_exclude) and the new /me/page-search
  endpoint. Tag browsing is preserved when no content filter is active.
- manga detail renders a deduped content-warning banner.

Tests: vitest param serialization (searchPages CSV/text/cw, listMangas
cw); Playwright text-search + cw-toggle flows (route-mocked). svelte-check
+ build clean; full vitest (258) + search e2e (7) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-13 19:12:34 +02:00
parent bd39476ac7
commit d36f24e9af
11 changed files with 394 additions and 4 deletions

View File

@@ -1,4 +1,5 @@
import { request, type Manga, type MangaStatus, type Page } from './client';
import type { ContentWarning } from './page_tags';
export type MangaSort = 'recent' | 'title';
@@ -17,6 +18,8 @@ export type MangaDetail = Manga & {
authors: AuthorRef[];
genres: GenreRef[];
tags: TagRef[];
/** Deduped union of content warnings across the manga's pages. */
content_warnings: ContentWarning[];
};
export type ListOptions = {
@@ -26,6 +29,9 @@ export type ListOptions = {
authorIds?: string[];
genreIds?: string[];
tagIds?: string[];
/** Content warnings the manga must carry (all) / must not carry (any). */
cwInclude?: ContentWarning[];
cwExclude?: ContentWarning[];
limit?: number;
offset?: number;
sort?: MangaSort;
@@ -49,6 +55,12 @@ export async function listMangas(opts: ListOptions = {}): Promise<MangasPage> {
if (opts.tagIds && opts.tagIds.length) {
params.set('tag_id', opts.tagIds.join(','));
}
if (opts.cwInclude && opts.cwInclude.length) {
params.set('cw_include', opts.cwInclude.join(','));
}
if (opts.cwExclude && opts.cwExclude.length) {
params.set('cw_exclude', opts.cwExclude.join(','));
}
if (opts.limit != null) params.set('limit', String(opts.limit));
if (opts.offset != null) params.set('offset', String(opts.offset));
if (opts.sort) params.set('sort', opts.sort);