From 665bc6f7b5a54894475e7228e06acd965995d3e5 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 14 Aug 2026 01:34:48 +0200 Subject: [PATCH] fix(plex,channels): address R13 S2 review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shared usePlexClearFilters hook replaces the duplicated clearAll (was copied in PlexBrowse and PlexSidebar) — one "reset every Plex narrowing" both call. - The Plex duration chip's minutes unit is localized (plex.filter.durationUnit: "min"/"perc") instead of a hardcoded "m". - The Plex collection chip falls back to the "Collection" label, not the raw rating_key id, when collectionTitle is missing. - Dropped the dead rowClassName that dimmed hidden rows in the subscribed table (hidden channels are excluded there now — they live in the Hidden tab). - Renamed the misleading `set` null-check predicate to `has`. Gate green: tsc/eslint-0/prettier/knip clean, vitest 106. --- frontend/src/components/Channels.tsx | 2 -- frontend/src/components/PlexBrowse.tsx | 14 +++----- frontend/src/components/PlexSidebar.tsx | 9 ++--- .../src/components/usePlexActiveFilters.ts | 35 +++++++++++++------ frontend/src/i18n/locales/en/plex.json | 1 + frontend/src/i18n/locales/hu/plex.json | 1 + 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx index 63fb9c1b..6267e15f 100644 --- a/frontend/src/components/Channels.tsx +++ b/frontend/src/components/Channels.tsx @@ -578,7 +578,6 @@ export default function Channels() { controlsBandClassName="px-4 max-w-384 mx-auto" sort={sort} onSortChange={setSort} - rowClassName={(c) => (c.hidden ? "opacity-60" : "")} emptyText={t("channels.empty")} /> ) : ( @@ -589,7 +588,6 @@ export default function Channels() { leadKey="priority" aboutKey="about" actionsKey="actions" - rowClassName={(c) => (c.hidden ? "opacity-60" : "")} emptyText={t("channels.empty")} /> )} diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx index 459aa827..83566f46 100644 --- a/frontend/src/components/PlexBrowse.tsx +++ b/frontend/src/components/PlexBrowse.tsx @@ -9,7 +9,7 @@ import { type ReactNode, } from "react"; import { qk } from "../lib/queryKeys"; -import { healPlexSort, PLEX_SORT_FALLBACK } from "../lib/plexSort"; +import { healPlexSort } from "../lib/plexSort"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { @@ -60,7 +60,7 @@ import PlexCollectionEditor from "./PlexCollectionEditor"; import { LoadingState, StateMessage } from "./QueryState"; import { PageToolbar } from "./PageShell"; import ActiveFilterChips from "./ActiveFilterChips"; -import { usePlexActiveFilters } from "./usePlexActiveFilters"; +import { usePlexActiveFilters, usePlexClearFilters } from "./usePlexActiveFilters"; import { usePlex, usePlexActions } from "./PlexProvider"; // Lazy: the rich player (pulls in hls.js) loads only when something is first played. @@ -91,16 +91,12 @@ export default function PlexBrowse() { // search term (q) comes from the shared header box; the rail hands off a playlist to open via the // shared `playlistOpen` slot, which we consume into a history subview then clear. const { q, scope, show, sort, filters, playlistOpen: openPlaylist } = usePlex(); - const { setScope, setShow, setSort, setFilters, setPlaylistOpen } = usePlexActions(); + const { setScope, setFilters, setPlaylistOpen } = usePlexActions(); const dq = useDebounced(q.trim(), 350); // Plex's active-filter chip row (above the grid), the shared feed idiom: one removable chip per - // narrowing, plus a Clear-all that resets filters + watch-state + sort to their defaults. + // narrowing, plus a Clear-all (shared with the sidebar) that resets filters + watch-state + sort. const plexChips = usePlexActiveFilters(); - const clearAllFilters = () => { - setFilters(EMPTY_PLEX_FILTERS); - setShow("all"); - setSort(PLEX_SORT_FALLBACK); - }; + const clearAllFilters = usePlexClearFilters(); // The provider holds sort scope-agnostically; heal it against THIS scope's options so a movie-only // key (e.g. duration) left over from before a switch to shows doesn't ask for an unavailable sort. const sortState = healPlexSort(sort, scope); diff --git a/frontend/src/components/PlexSidebar.tsx b/frontend/src/components/PlexSidebar.tsx index 8cd41b77..c239802b 100644 --- a/frontend/src/components/PlexSidebar.tsx +++ b/frontend/src/components/PlexSidebar.tsx @@ -3,7 +3,7 @@ import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Layers, ListMusic, Plus, SlidersHorizontal, X } from "lucide-react"; -import { api, EMPTY_PLEX_FILTERS, plexFilterCount, type PlexFilters } from "../lib/api"; +import { api, plexFilterCount, type PlexFilters } from "../lib/api"; import { healPlexSort, plexSortOptions, PLEX_SORT_FALLBACK } from "../lib/plexSort"; import { defaultLayout, type PanelLayout } from "../lib/panelLayout"; import { useDebounced } from "../lib/useDebounced"; @@ -11,6 +11,7 @@ import { useScrollFade } from "../lib/useScrollFade"; import SidePanel from "./SidePanel"; import PanelGroups, { type PanelItem } from "./PanelGroups"; import SortControl from "./SortControl"; +import { usePlexClearFilters } from "./usePlexActiveFilters"; import { usePlex, usePlexActions } from "./PlexProvider"; // The Plex module's left filter column — now the shared floating SidePanel with its filter groups @@ -123,11 +124,7 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl const patch = (p: Partial) => setFilters({ ...filters, ...p }); const toggleIn = (arr: string[], v: string) => arr.includes(v) ? arr.filter((x) => x !== v) : [...arr, v]; - const clearAll = () => { - setFilters(EMPTY_PLEX_FILTERS); - setShow("all"); - setSort(PLEX_SORT_FALLBACK); - }; + const clearAll = usePlexClearFilters(); const sortOptions = plexSortOptions(scope, (k) => t(`plex.sort.${k}`)); const durBucketKey = DURATION_BUCKETS.find( diff --git a/frontend/src/components/usePlexActiveFilters.ts b/frontend/src/components/usePlexActiveFilters.ts index 4f5586f3..4b12e019 100644 --- a/frontend/src/components/usePlexActiveFilters.ts +++ b/frontend/src/components/usePlexActiveFilters.ts @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { type ActiveFilter } from "../lib/useActiveFilters"; -import { type PlexFilters } from "../lib/api"; +import { EMPTY_PLEX_FILTERS, type PlexFilters } from "../lib/api"; import { healPlexSort, PLEX_SORT_FALLBACK } from "../lib/plexSort"; import { usePlex, usePlexActions } from "./PlexProvider"; @@ -18,7 +18,7 @@ export function usePlexActiveFilters(): ActiveFilter[] { const patch = (p: Partial) => setFilters({ ...filters, ...p }); const out: ActiveFilter[] = []; - const set = (x: number | null | undefined) => x != null; + const has = (x: number | null | undefined) => x != null; for (const g of filters.genres) { out.push({ @@ -34,11 +34,11 @@ export function usePlexActiveFilters(): ActiveFilter[] { remove: () => patch({ contentRatings: filters.contentRatings.filter((x) => x !== r) }), }); } - if (set(filters.yearMin) || set(filters.yearMax)) { + if (has(filters.yearMin) || has(filters.yearMax)) { const range = - set(filters.yearMin) && set(filters.yearMax) + has(filters.yearMin) && has(filters.yearMax) ? `${filters.yearMin}–${filters.yearMax}` - : set(filters.yearMin) + : has(filters.yearMin) ? `≥${filters.yearMin}` : `≤${filters.yearMax}`; out.push({ @@ -47,17 +47,19 @@ export function usePlexActiveFilters(): ActiveFilter[] { remove: () => patch({ yearMin: null, yearMax: null }), }); } - if (set(filters.ratingMin)) { + if (has(filters.ratingMin)) { out.push({ key: "ratingMin", label: `${t("plex.filter.rating")}: ≥${filters.ratingMin}`, remove: () => patch({ ratingMin: null }), }); } - if (set(filters.durationMin) || set(filters.durationMax)) { - const lo = set(filters.durationMin) ? Math.round(filters.durationMin! / 60) : null; - const hi = set(filters.durationMax) ? Math.round(filters.durationMax! / 60) : null; - const range = lo != null && hi != null ? `${lo}–${hi}m` : lo != null ? `≥${lo}m` : `≤${hi}m`; + if (has(filters.durationMin) || has(filters.durationMax)) { + const lo = has(filters.durationMin) ? Math.round(filters.durationMin! / 60) : null; + const hi = has(filters.durationMax) ? Math.round(filters.durationMax! / 60) : null; + const u = t("plex.filter.durationUnit"); + const range = + lo != null && hi != null ? `${lo}–${hi} ${u}` : lo != null ? `≥${lo} ${u}` : `≤${hi} ${u}`; out.push({ key: "duration", label: `${t("plex.filter.duration")}: ${range}`, @@ -95,7 +97,7 @@ export function usePlexActiveFilters(): ActiveFilter[] { if (filters.collection) { out.push({ key: "collection", - label: filters.collectionTitle ?? filters.collection, + label: filters.collectionTitle ?? t("plex.filter.collection"), remove: () => patch({ collection: null, collectionTitle: null }), }); } @@ -116,3 +118,14 @@ export function usePlexActiveFilters(): ActiveFilter[] { } return out; } + +// The one "reset every Plex narrowing" callback, shared by the sidebar's Clear-filters and the chip +// row's Clear-all so the two can't drift: expanded filters + watch-state + sort all go back to default. +export function usePlexClearFilters(): () => void { + const { setShow, setSort, setFilters } = usePlexActions(); + return () => { + setFilters(EMPTY_PLEX_FILTERS); + setShow("all"); + setSort(PLEX_SORT_FALLBACK); + }; +} diff --git a/frontend/src/i18n/locales/en/plex.json b/frontend/src/i18n/locales/en/plex.json index 301c0998..03fabeec 100644 --- a/frontend/src/i18n/locales/en/plex.json +++ b/frontend/src/i18n/locales/en/plex.json @@ -33,6 +33,7 @@ "genre": "Genre", "year": "Year", "duration": "Length", + "durationUnit": "min", "durationOpt": { "short": "Under 90m", "mid": "90–120m", diff --git a/frontend/src/i18n/locales/hu/plex.json b/frontend/src/i18n/locales/hu/plex.json index 78c40a67..95c9e6c8 100644 --- a/frontend/src/i18n/locales/hu/plex.json +++ b/frontend/src/i18n/locales/hu/plex.json @@ -33,6 +33,7 @@ "genre": "Műfaj", "year": "Év", "duration": "Hossz", + "durationUnit": "perc", "durationOpt": { "short": "90 perc alatt", "mid": "90–120 perc",