fix(filters): the year dialog fits a tablet, walks by arrow keys, names one year once

- a decade cell is a 36-high box with its count under the year (owner A)
- arrow keys walk the cells: along a decade, then to the nearest year below
- clearing the kept older year hands the focus on instead of dropping it
- More... shows while a year with titles is hidden, as a plain chip
- the decade caption is not read twice; the Plex range fields have names
- one Plex year is labelled 1999, not 1999-1999, via oneYear/singleYear
- tests for the Plex wiring and the feed year clearing the upload date
This commit is contained in:
2026-11-14 17:56:37 +01:00
parent 02714e4986
commit 0eca18bcb4
20 changed files with 393 additions and 109 deletions
+2
View File
@@ -52,6 +52,8 @@ import { plexApi } from "./api/plex";
export type * from "./api/plex";
export {
EMPTY_PLEX_FILTERS,
oneYear,
singleYear,
plexFilterCount,
coercePlexFilters,
plexFiltersSignature,
+11
View File
@@ -261,6 +261,17 @@ export interface PlexPersonDetail {
movies: PlexCard[]; // enabled-library movies whose cast OR directors include this person
shows: PlexCard[]; // enabled-library shows, same
}
/** One year as a Plex filter: a range of one, both ends set (the year picker, a year on an info page).
* `null` clears it. */
export const oneYear = (year?: number | null): Pick<PlexFilters, "yearMin" | "yearMax"> => ({
yearMin: year ?? null,
yearMax: year ?? null,
});
/** The one year a filter narrows to, when its range is a single year. */
export const singleYear = (f: Pick<PlexFilters, "yearMin" | "yearMax">): number | undefined =>
f.yearMin != null && f.yearMin === f.yearMax ? f.yearMin : undefined;
// The expanded Plex browse filters (movie libraries). Persisted per-account as JSON.
export interface PlexFilters {
genres: string[];
@@ -0,0 +1,26 @@
// @vitest-environment jsdom
import { describe, it, expect } from "vitest";
import { buildPlexActiveFilters } from "./plexActiveFilters";
import { EMPTY_PLEX_FILTERS, type PlexFilters } from "./api";
const t = (key: string) => key;
const yearLabel = (f: Partial<PlexFilters>) =>
buildPlexActiveFilters(
{ ...EMPTY_PLEX_FILTERS, ...f },
"",
() => {},
() => {},
t,
).find((c) => c.key === "year")?.label;
describe("the Plex year chip", () => {
it("names one year as the year, not a range of one", () => {
expect(yearLabel({ yearMin: 1999, yearMax: 1999 })).toBe("plex.filter.year: 1999");
});
it("names a range and an open end as before", () => {
expect(yearLabel({ yearMin: 1990, yearMax: 1999 })).toBe("plex.filter.year: 1990–1999");
expect(yearLabel({ yearMin: 1990 })).toBe("plex.filter.year: ≥1990");
expect(yearLabel({ yearMax: 1999 })).toBe("plex.filter.year: ≤1999");
});
});
+9 -6
View File
@@ -1,6 +1,6 @@
import { vocabI18nKey } from "./vocab";
import { type ActiveFilter } from "./useActiveFilters";
import { type PlexFilters } from "./api";
import { singleYear, type PlexFilters } from "./api";
type TFn = (key: string, options?: Record<string, unknown>) => string;
@@ -42,12 +42,15 @@ export function buildPlexActiveFilters(
});
}
if (has(filters.yearMin) || has(filters.yearMax)) {
const one = singleYear(filters);
const range =
has(filters.yearMin) && has(filters.yearMax)
? `${filters.yearMin}–${filters.yearMax}`
: has(filters.yearMin)
? `≥${filters.yearMin}`
: `≤${filters.yearMax}`;
one != null
? String(one)
: has(filters.yearMin) && has(filters.yearMax)
? `${filters.yearMin}–${filters.yearMax}`
: has(filters.yearMin)
? `≥${filters.yearMin}`
: `≤${filters.yearMax}`;
out.push({
key: "year",
label: `${t("plex.filter.year")}: ${range}`,