feat(plex): field-scoped search (S2) + instant search typeahead
Add a search scope-pill to the Plex header search box (GitHub-style, one field-scope at a time) and an instant typeahead of the top matches below it. Backend (unified_library): - New `field` param: all | title | people | summary | episode. `all` is byte-identical to the pre-scope behaviour (untouched weighted search_vector); a specific field keeps the GIN `search_vector @@ tsq` prefilter AND ANDs a per-column tsvector recheck, so scoped hits are a strict subset of Everywhere. Per-field expressions mirror the search_vector Computed def (same TS config, same left(summary,1000) truncation). - `episode` scope empties the movie/show grid and title-only-matches the grouped Episodes section; the title/people/summary scopes suppress that section. - Unknown field falls back to `all`. New DB-lane characterization tests (test_plex_field_scoped_search.py): each scope narrows correctly, field=all is identical to today, episode empties the grid and needs shows in scope. Frontend: - PlexSearchBox wraps the shared SearchBar with the scope-pill (reusing ActionMenu) and the typeahead; SearchBar gains generic leading/below/focus/ keydown slots so other modules' search boxes are untouched. - Episode scope offered only when shows are in scope (movie-only hides it), healed at the point of use like the sort state. - Typeahead reuses /api/plex/library (limit=10), honours scope+field (not the sidebar filters), shows poster+title+year, keyboard nav (arrows/Enter/Esc), and jumps straight to a movie's info page or a show's drill-down via a new provider jumpTo hand-off (mirrors the playlistOpen pattern). - `field` persisted per-account; en/hu labels added.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// The search-box scope-pill (S2): one field-scope active at a time, narrowing the Plex full-text
|
||||
// search to a single facet of the row. Kept in one module so the provider (state), the header
|
||||
// SearchBar (the pill UI), and the api layer (the `field` query param) share one source of truth.
|
||||
|
||||
export type PlexSearchField = "all" | "title" | "people" | "summary" | "episode";
|
||||
|
||||
export const PLEX_SEARCH_FIELDS: PlexSearchField[] = [
|
||||
"all",
|
||||
"title",
|
||||
"people",
|
||||
"summary",
|
||||
"episode",
|
||||
];
|
||||
|
||||
// i18n label key per scope (see plex.searchField.* in the locale files).
|
||||
export const plexFieldLabelKey = (f: PlexSearchField): string => `plex.searchField.${f}`;
|
||||
|
||||
// Episodes only exist under show libraries, so the Episode scope is offered only when shows are in
|
||||
// scope (show | both) — mirroring the backend, which returns nothing for episode+movie-only.
|
||||
export function availablePlexFields(scope: string): PlexSearchField[] {
|
||||
return scope === "movie"
|
||||
? PLEX_SEARCH_FIELDS.filter((f) => f !== "episode")
|
||||
: PLEX_SEARCH_FIELDS;
|
||||
}
|
||||
|
||||
// Heal a stored/selected field against the current scope: a movie-only scope can't hold Episode, so
|
||||
// it falls back to Everywhere (analogous to healPlexSort). Applied at the point of use.
|
||||
export function healPlexField(field: PlexSearchField, scope: string): PlexSearchField {
|
||||
return availablePlexFields(scope).includes(field) ? field : "all";
|
||||
}
|
||||
|
||||
export function parsePlexField(v: string | null | undefined): PlexSearchField {
|
||||
return v && (PLEX_SEARCH_FIELDS as string[]).includes(v) ? (v as PlexSearchField) : "all";
|
||||
}
|
||||
Reference in New Issue
Block a user