feat(plex): sort direction, genre any/all, multi-person filters; fix back-nav + player stop

UAT follow-ups on the Plex filter epic:
- Sort now has an asc/desc toggle (sort_dir), applied to any sort field.
- Genre multi-select gains an Any/All mode (genre_mode: OR vs AND containment).
- Director/actor/studio become multi-value: people AND (titles featuring all selected),
  studios OR; clicking them on the info page stacks (unions) instead of replacing, and
  the sidebar shows each as a removable 'Active' chip.
- fix(history): clicking a metadata filter on the info page now pushes a fresh grid
  entry instead of history.back(), so browser Back returns to the info page rather than
  leaving the Plex module.
- fix(player): fully tear down the <video> + hls on unmount and guard late play() calls,
  so backing out of a just-started video no longer leaves audio playing in the background.
i18n en/hu/de (match any/all, sort direction).
This commit is contained in:
npeter83 2026-07-06 00:15:31 +02:00
parent eefd7e3abd
commit 1982cfa7b9
10 changed files with 160 additions and 60 deletions

View file

@ -723,6 +723,7 @@ export interface PlexCastMember {
// The expanded Plex browse filters (movie libraries). Persisted per-account as JSON.
export interface PlexFilters {
genres: string[];
genreMode?: "any" | "all"; // how multiple genres combine (default any)
contentRatings: string[];
yearMin?: number | null;
yearMax?: number | null;
@ -730,11 +731,18 @@ export interface PlexFilters {
durationMin?: number | null; // seconds
durationMax?: number | null;
addedWithin?: string; // "" | "24h" | "1w" | "1m" | "6m" | "1y"
director?: string | null;
actor?: string | null;
studio?: string | null;
directors: string[]; // AND — titles featuring ALL selected
actors: string[]; // AND — titles featuring ALL selected
studios: string[]; // OR — from any selected studio
sortDir?: "asc" | "desc"; // sort direction (default desc)
}
export const EMPTY_PLEX_FILTERS: PlexFilters = { genres: [], contentRatings: [] };
export const EMPTY_PLEX_FILTERS: PlexFilters = {
genres: [],
contentRatings: [],
directors: [],
actors: [],
studios: [],
};
export function plexFilterCount(f: PlexFilters): number {
return (
f.genres.length +
@ -743,9 +751,9 @@ export function plexFilterCount(f: PlexFilters): number {
(f.ratingMin != null ? 1 : 0) +
(f.durationMin != null || f.durationMax != null ? 1 : 0) +
(f.addedWithin ? 1 : 0) +
(f.director ? 1 : 0) +
(f.actor ? 1 : 0) +
(f.studio ? 1 : 0)
f.directors.length +
f.actors.length +
f.studios.length
);
}
export interface PlexFacets {
@ -1097,6 +1105,7 @@ export const api = {
const f = p.filters;
if (f) {
if (f.genres?.length) u.set("genres", f.genres.join(","));
if (f.genreMode === "all") u.set("genre_mode", "all");
if (f.contentRatings?.length) u.set("content_ratings", f.contentRatings.join(","));
if (f.yearMin != null) u.set("year_min", String(f.yearMin));
if (f.yearMax != null) u.set("year_max", String(f.yearMax));
@ -1104,9 +1113,10 @@ export const api = {
if (f.durationMin != null) u.set("duration_min", String(f.durationMin));
if (f.durationMax != null) u.set("duration_max", String(f.durationMax));
if (f.addedWithin) u.set("added_within", f.addedWithin);
if (f.director) u.set("director", f.director);
if (f.actor) u.set("actor", f.actor);
if (f.studio) u.set("studio", f.studio);
if (f.directors?.length) u.set("directors", f.directors.join(","));
if (f.actors?.length) u.set("actors", f.actors.join(","));
if (f.studios?.length) u.set("studios", f.studios.join(","));
if (f.sortDir === "asc") u.set("sort_dir", "asc");
}
return req(`/api/plex/browse?${u.toString()}`);
},