feat(plex): apply the sidebar filters to the Discover rails too
Discover now behaves like the Browse grid — the active metadata filters (genre / rating / year / collection / studio / …) narrow every rail: Continue watching, Recently added, Surprise me, the genre tiles, and Because you watched. Same LibraryFilters / meta_filter_conds the grid uses, applied to each rail's movie + show query (movie duration included); a rail may come back empty, which the client already hides. Genre-language matches the grid. Because Discover honours filters itself, adding a filter no longer force-flips the Discover landing to the grid (only a header SEARCH does, since Discover has no search rail) — so you stay on Discover with narrowed rails. Frontend threads `filters` into api.plexDiscover/plexSurprise + their query keys (plexSurprise is variadic now) and the watched-toggle setQueryData; PlexDiscover reads them from usePlex. Tests: two discover filter-narrowing cases; PlexDiscover dom test updated for the provider default + 3-arg call.
This commit is contained in:
@@ -105,11 +105,12 @@ export default function PlexBrowse() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [jumpTo]);
|
||||
|
||||
// Discover → grid on a browse intent: when a header search or a filter FIRST becomes active while on
|
||||
// the Discover landing, switch to the flat grid (which consumes q/filters). Only on the off→on edge,
|
||||
// so a pre-existing (persisted) filter doesn't yank a returning user off Discover on mount, and a
|
||||
// Back to Discover with the search still in the box doesn't immediately re-flip.
|
||||
const browseIntent = !!dq || plexFilterCount(filters) > 0;
|
||||
// Discover → grid on a SEARCH: a header search has no rail on Discover, so entering one FIRST while on
|
||||
// the Discover landing switches to the flat grid (which consumes q). Only on the off→on edge, so a Back
|
||||
// to Discover with the term still in the box doesn't immediately re-flip. Filters are NOT a flip
|
||||
// trigger anymore — Discover honours the active filters itself (its rails narrow like the grid), so
|
||||
// adding a filter leaves you on Discover with narrowed rails instead of yanking you to the grid.
|
||||
const browseIntent = !!dq;
|
||||
const prevIntent = useRef(browseIntent);
|
||||
useEffect(() => {
|
||||
if (sub.view.kind === "discover" && browseIntent && !prevIntent.current) {
|
||||
|
||||
@@ -42,6 +42,17 @@ vi.mock("../lib/api", () => ({
|
||||
plexSurprise: (...a: unknown[]) => plexSurprise(...a),
|
||||
plexSetState: (...a: unknown[]) => plexSetState(...a),
|
||||
},
|
||||
// PlexDiscover now reads the active filters via usePlex (PlexProvider); its context default needs
|
||||
// EMPTY_PLEX_FILTERS at import time. No provider is rendered, so the default (empty) filters apply.
|
||||
EMPTY_PLEX_FILTERS: {
|
||||
genres: [],
|
||||
contentRatings: [],
|
||||
directors: [],
|
||||
actors: [],
|
||||
studios: [],
|
||||
keywords: [],
|
||||
collections: [],
|
||||
},
|
||||
}));
|
||||
|
||||
import PlexDiscover from "./PlexDiscover";
|
||||
@@ -102,7 +113,8 @@ describe("PlexDiscover", () => {
|
||||
expect(screen.getAllByText("Just Added").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("Random Pick").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("Rec One").length).toBeGreaterThan(0);
|
||||
expect(plexDiscover).toHaveBeenCalledWith("both", "en");
|
||||
// Now called with the active filters too (empty default here, no provider mounted).
|
||||
expect(plexDiscover).toHaveBeenCalledWith("both", "en", expect.objectContaining({ genres: [] }));
|
||||
});
|
||||
|
||||
it("fires onGenre with the tile's value", async () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { qk } from "../lib/queryKeys";
|
||||
import { api, type PlexCard, type PlexDiscover as PlexDiscoverData } from "../lib/api";
|
||||
import { plexLang } from "../i18n";
|
||||
import { toggleWatchState } from "../lib/plexWatch";
|
||||
import { usePlex } from "./PlexProvider";
|
||||
import { Carousel } from "./Carousel";
|
||||
import { PlexPosterCard } from "./plexCards";
|
||||
import { LoadingState, StateMessage } from "./QueryState";
|
||||
@@ -89,14 +90,17 @@ export default function PlexDiscover({
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const lang = plexLang();
|
||||
// The active sidebar filters narrow every rail, so Discover matches the Browse grid. They're in the
|
||||
// query keys so a filter change refetches (and the watched-toggle setQueryData below keys the same).
|
||||
const { filters } = usePlex();
|
||||
const q = useQuery({
|
||||
queryKey: qk.plexDiscover(scope, lang),
|
||||
queryFn: () => api.plexDiscover(scope, lang),
|
||||
queryKey: qk.plexDiscover(scope, lang, filters),
|
||||
queryFn: () => api.plexDiscover(scope, lang, filters),
|
||||
});
|
||||
// Surprise-me is its own query so the ↻ button can redraw it without refetching the whole surface.
|
||||
const surpriseQ = useQuery({
|
||||
queryKey: qk.plexSurprise(scope),
|
||||
queryFn: () => api.plexSurprise(scope),
|
||||
queryKey: qk.plexSurprise(scope, filters),
|
||||
queryFn: () => api.plexSurprise(scope, undefined, filters, lang),
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
@@ -107,7 +111,7 @@ export default function PlexDiscover({
|
||||
optimistic: (next) => {
|
||||
const flip = (cards: PlexCard[]) =>
|
||||
cards.map((c) => (c.id === card.id ? { ...c, status: next } : c));
|
||||
qc.setQueryData<PlexDiscoverData>(qk.plexDiscover(scope, lang), (old) =>
|
||||
qc.setQueryData<PlexDiscoverData>(qk.plexDiscover(scope, lang, filters), (old) =>
|
||||
old
|
||||
? {
|
||||
...old,
|
||||
@@ -119,8 +123,9 @@ export default function PlexDiscover({
|
||||
}
|
||||
: old,
|
||||
);
|
||||
qc.setQueryData<{ scope: string; items: PlexCard[] }>(qk.plexSurprise(scope), (old) =>
|
||||
old ? { ...old, items: flip(old.items) } : old,
|
||||
qc.setQueryData<{ scope: string; items: PlexCard[] }>(
|
||||
qk.plexSurprise(scope, filters),
|
||||
(old) => (old ? { ...old, items: flip(old.items) } : old),
|
||||
);
|
||||
},
|
||||
resync: () => {
|
||||
|
||||
@@ -463,16 +463,26 @@ export const plexApi = {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ item_rating_keys: itemRks }),
|
||||
}),
|
||||
// Discover landing rails in one request (serve-from-DB). `lang` picks the genre-tile vocabulary.
|
||||
plexDiscover: (scope: string, lang?: string): Promise<PlexDiscover> => {
|
||||
// Discover landing rails in one request (serve-from-DB). `lang` picks the genre-tile vocabulary; the
|
||||
// active sidebar `filters` narrow every rail (so Discover behaves like the Browse grid).
|
||||
plexDiscover: (scope: string, lang?: string, filters?: PlexFilters): Promise<PlexDiscover> => {
|
||||
const u = new URLSearchParams({ scope });
|
||||
if (lang === "hu") u.set("lang", "hu");
|
||||
if (filters) appendPlexFilters(u, filters);
|
||||
return req(`/api/plex/discover?${u.toString()}`);
|
||||
},
|
||||
// A fresh random sample for the Surprise-me rail — a new draw every call (the ↻ refetch).
|
||||
plexSurprise: (scope: string, limit?: number): Promise<{ scope: string; items: PlexCard[] }> => {
|
||||
// A fresh random sample for the Surprise-me rail — a new draw every call (the ↻ refetch), narrowed by
|
||||
// the active filters like the other rails.
|
||||
plexSurprise: (
|
||||
scope: string,
|
||||
limit?: number,
|
||||
filters?: PlexFilters,
|
||||
lang?: string,
|
||||
): Promise<{ scope: string; items: PlexCard[] }> => {
|
||||
const u = new URLSearchParams({ scope });
|
||||
if (limit) u.set("limit", String(limit));
|
||||
if (lang === "hu") u.set("lang", "hu");
|
||||
if (filters) appendPlexFilters(u, filters);
|
||||
return req(`/api/plex/surprise?${u.toString()}`);
|
||||
},
|
||||
plexShow: (id: string, lang?: string): Promise<PlexShowDetail> =>
|
||||
|
||||
@@ -97,8 +97,9 @@ export const qk = {
|
||||
// both for a prefix invalidation after a watched-toggle. Surprise is its own key so the ↻ reshuffle
|
||||
// refetches it alone.
|
||||
plexDiscover: (...rest: unknown[]) => ["plex-discover", ...rest],
|
||||
plexSurprise: (scope?: string) =>
|
||||
scope !== undefined ? (["plex-surprise", scope] as const) : (["plex-surprise"] as const),
|
||||
// Variadic like plexDiscover so it can key by scope + the active filters; `plexSurprise()` (no args)
|
||||
// stays a prefix for the reshuffle/watched-toggle invalidations.
|
||||
plexSurprise: (...rest: unknown[]) => ["plex-surprise", ...rest],
|
||||
plexSuggest: (...rest: unknown[]) => ["plex-suggest", ...rest],
|
||||
// Optional trailing `lang` (E5 S6): a language switch remounts the detail with a new key so HU/EN
|
||||
// metadata is cached separately. Omitting it (e.g. for a prefix invalidation) still matches both.
|
||||
|
||||
Reference in New Issue
Block a user