fix(plex,channels): address R13 S2 review findings
- 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.
This commit is contained in:
@@ -578,7 +578,6 @@ export default function Channels() {
|
|||||||
controlsBandClassName="px-4 max-w-384 mx-auto"
|
controlsBandClassName="px-4 max-w-384 mx-auto"
|
||||||
sort={sort}
|
sort={sort}
|
||||||
onSortChange={setSort}
|
onSortChange={setSort}
|
||||||
rowClassName={(c) => (c.hidden ? "opacity-60" : "")}
|
|
||||||
emptyText={t("channels.empty")}
|
emptyText={t("channels.empty")}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -589,7 +588,6 @@ export default function Channels() {
|
|||||||
leadKey="priority"
|
leadKey="priority"
|
||||||
aboutKey="about"
|
aboutKey="about"
|
||||||
actionsKey="actions"
|
actionsKey="actions"
|
||||||
rowClassName={(c) => (c.hidden ? "opacity-60" : "")}
|
|
||||||
emptyText={t("channels.empty")}
|
emptyText={t("channels.empty")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { healPlexSort, PLEX_SORT_FALLBACK } from "../lib/plexSort";
|
import { healPlexSort } from "../lib/plexSort";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
@@ -60,7 +60,7 @@ import PlexCollectionEditor from "./PlexCollectionEditor";
|
|||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./QueryState";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./PageShell";
|
||||||
import ActiveFilterChips from "./ActiveFilterChips";
|
import ActiveFilterChips from "./ActiveFilterChips";
|
||||||
import { usePlexActiveFilters } from "./usePlexActiveFilters";
|
import { usePlexActiveFilters, usePlexClearFilters } from "./usePlexActiveFilters";
|
||||||
import { usePlex, usePlexActions } from "./PlexProvider";
|
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||||
|
|
||||||
// Lazy: the rich player (pulls in hls.js) loads only when something is first played.
|
// 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
|
// 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.
|
// shared `playlistOpen` slot, which we consume into a history subview then clear.
|
||||||
const { q, scope, show, sort, filters, playlistOpen: openPlaylist } = usePlex();
|
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);
|
const dq = useDebounced(q.trim(), 350);
|
||||||
// Plex's active-filter chip row (above the grid), the shared feed idiom: one removable chip per
|
// 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 plexChips = usePlexActiveFilters();
|
||||||
const clearAllFilters = () => {
|
const clearAllFilters = usePlexClearFilters();
|
||||||
setFilters(EMPTY_PLEX_FILTERS);
|
|
||||||
setShow("all");
|
|
||||||
setSort(PLEX_SORT_FALLBACK);
|
|
||||||
};
|
|
||||||
// The provider holds sort scope-agnostically; heal it against THIS scope's options so a movie-only
|
// 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.
|
// key (e.g. duration) left over from before a switch to shows doesn't ask for an unavailable sort.
|
||||||
const sortState = healPlexSort(sort, scope);
|
const sortState = healPlexSort(sort, scope);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { qk } from "../lib/queryKeys";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Layers, ListMusic, Plus, SlidersHorizontal, X } from "lucide-react";
|
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 { healPlexSort, plexSortOptions, PLEX_SORT_FALLBACK } from "../lib/plexSort";
|
||||||
import { defaultLayout, type PanelLayout } from "../lib/panelLayout";
|
import { defaultLayout, type PanelLayout } from "../lib/panelLayout";
|
||||||
import { useDebounced } from "../lib/useDebounced";
|
import { useDebounced } from "../lib/useDebounced";
|
||||||
@@ -11,6 +11,7 @@ import { useScrollFade } from "../lib/useScrollFade";
|
|||||||
import SidePanel from "./SidePanel";
|
import SidePanel from "./SidePanel";
|
||||||
import PanelGroups, { type PanelItem } from "./PanelGroups";
|
import PanelGroups, { type PanelItem } from "./PanelGroups";
|
||||||
import SortControl from "./SortControl";
|
import SortControl from "./SortControl";
|
||||||
|
import { usePlexClearFilters } from "./usePlexActiveFilters";
|
||||||
import { usePlex, usePlexActions } from "./PlexProvider";
|
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||||
|
|
||||||
// The Plex module's left filter column — now the shared floating SidePanel with its filter groups
|
// 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<PlexFilters>) => setFilters({ ...filters, ...p });
|
const patch = (p: Partial<PlexFilters>) => setFilters({ ...filters, ...p });
|
||||||
const toggleIn = (arr: string[], v: string) =>
|
const toggleIn = (arr: string[], v: string) =>
|
||||||
arr.includes(v) ? arr.filter((x) => x !== v) : [...arr, v];
|
arr.includes(v) ? arr.filter((x) => x !== v) : [...arr, v];
|
||||||
const clearAll = () => {
|
const clearAll = usePlexClearFilters();
|
||||||
setFilters(EMPTY_PLEX_FILTERS);
|
|
||||||
setShow("all");
|
|
||||||
setSort(PLEX_SORT_FALLBACK);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortOptions = plexSortOptions(scope, (k) => t(`plex.sort.${k}`));
|
const sortOptions = plexSortOptions(scope, (k) => t(`plex.sort.${k}`));
|
||||||
const durBucketKey = DURATION_BUCKETS.find(
|
const durBucketKey = DURATION_BUCKETS.find(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { type ActiveFilter } from "../lib/useActiveFilters";
|
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 { healPlexSort, PLEX_SORT_FALLBACK } from "../lib/plexSort";
|
||||||
import { usePlex, usePlexActions } from "./PlexProvider";
|
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export function usePlexActiveFilters(): ActiveFilter[] {
|
|||||||
const patch = (p: Partial<PlexFilters>) => setFilters({ ...filters, ...p });
|
const patch = (p: Partial<PlexFilters>) => setFilters({ ...filters, ...p });
|
||||||
const out: ActiveFilter[] = [];
|
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) {
|
for (const g of filters.genres) {
|
||||||
out.push({
|
out.push({
|
||||||
@@ -34,11 +34,11 @@ export function usePlexActiveFilters(): ActiveFilter[] {
|
|||||||
remove: () => patch({ contentRatings: filters.contentRatings.filter((x) => x !== r) }),
|
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 =
|
const range =
|
||||||
set(filters.yearMin) && set(filters.yearMax)
|
has(filters.yearMin) && has(filters.yearMax)
|
||||||
? `${filters.yearMin}–${filters.yearMax}`
|
? `${filters.yearMin}–${filters.yearMax}`
|
||||||
: set(filters.yearMin)
|
: has(filters.yearMin)
|
||||||
? `≥${filters.yearMin}`
|
? `≥${filters.yearMin}`
|
||||||
: `≤${filters.yearMax}`;
|
: `≤${filters.yearMax}`;
|
||||||
out.push({
|
out.push({
|
||||||
@@ -47,17 +47,19 @@ export function usePlexActiveFilters(): ActiveFilter[] {
|
|||||||
remove: () => patch({ yearMin: null, yearMax: null }),
|
remove: () => patch({ yearMin: null, yearMax: null }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (set(filters.ratingMin)) {
|
if (has(filters.ratingMin)) {
|
||||||
out.push({
|
out.push({
|
||||||
key: "ratingMin",
|
key: "ratingMin",
|
||||||
label: `${t("plex.filter.rating")}: ≥${filters.ratingMin}`,
|
label: `${t("plex.filter.rating")}: ≥${filters.ratingMin}`,
|
||||||
remove: () => patch({ ratingMin: null }),
|
remove: () => patch({ ratingMin: null }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (set(filters.durationMin) || set(filters.durationMax)) {
|
if (has(filters.durationMin) || has(filters.durationMax)) {
|
||||||
const lo = set(filters.durationMin) ? Math.round(filters.durationMin! / 60) : null;
|
const lo = has(filters.durationMin) ? Math.round(filters.durationMin! / 60) : null;
|
||||||
const hi = set(filters.durationMax) ? Math.round(filters.durationMax! / 60) : null;
|
const hi = has(filters.durationMax) ? Math.round(filters.durationMax! / 60) : null;
|
||||||
const range = lo != null && hi != null ? `${lo}–${hi}m` : lo != null ? `≥${lo}m` : `≤${hi}m`;
|
const u = t("plex.filter.durationUnit");
|
||||||
|
const range =
|
||||||
|
lo != null && hi != null ? `${lo}–${hi} ${u}` : lo != null ? `≥${lo} ${u}` : `≤${hi} ${u}`;
|
||||||
out.push({
|
out.push({
|
||||||
key: "duration",
|
key: "duration",
|
||||||
label: `${t("plex.filter.duration")}: ${range}`,
|
label: `${t("plex.filter.duration")}: ${range}`,
|
||||||
@@ -95,7 +97,7 @@ export function usePlexActiveFilters(): ActiveFilter[] {
|
|||||||
if (filters.collection) {
|
if (filters.collection) {
|
||||||
out.push({
|
out.push({
|
||||||
key: "collection",
|
key: "collection",
|
||||||
label: filters.collectionTitle ?? filters.collection,
|
label: filters.collectionTitle ?? t("plex.filter.collection"),
|
||||||
remove: () => patch({ collection: null, collectionTitle: null }),
|
remove: () => patch({ collection: null, collectionTitle: null }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -116,3 +118,14 @@ export function usePlexActiveFilters(): ActiveFilter[] {
|
|||||||
}
|
}
|
||||||
return out;
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"genre": "Genre",
|
"genre": "Genre",
|
||||||
"year": "Year",
|
"year": "Year",
|
||||||
"duration": "Length",
|
"duration": "Length",
|
||||||
|
"durationUnit": "min",
|
||||||
"durationOpt": {
|
"durationOpt": {
|
||||||
"short": "Under 90m",
|
"short": "Under 90m",
|
||||||
"mid": "90–120m",
|
"mid": "90–120m",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"genre": "Műfaj",
|
"genre": "Műfaj",
|
||||||
"year": "Év",
|
"year": "Év",
|
||||||
"duration": "Hossz",
|
"duration": "Hossz",
|
||||||
|
"durationUnit": "perc",
|
||||||
"durationOpt": {
|
"durationOpt": {
|
||||||
"short": "90 perc alatt",
|
"short": "90 perc alatt",
|
||||||
"mid": "90–120 perc",
|
"mid": "90–120 perc",
|
||||||
|
|||||||
Reference in New Issue
Block a user