feat(plex): S3 toolbar relocation — scope to header, sort above the cards
Move the two "what am I browsing" controls out of the filter sidebar to where they're always in reach, matching the Feed's layout: - Library scope (both/movie/show) → a compact SegmentedToggle in the header next to the search box (new PlexScopeToggle, reads the same provider state). - Sort → a SortControl in the row above the cards (into the PageToolbar band, count on the left, sort on the right), like the Feed. Sidebar cleanup: drop the `scope` and `sort` island cards; the panel badge now counts only the filters still in the panel (expanded filters + watch-state); remove the now-unused sort imports. panelLayout drops both from the default plex order (stale saved-layout keys are auto-filtered, so existing layouts degrade cleanly). usePlexActiveFilters no longer emits a sort chip — sort is an always-visible control now, not a removable narrowing (same as the Feed). FE-only, no backend/migration. typecheck + eslint clean; 171 vitest green; self-E2E verified (header scope drives the grid, sort control reorders, sidebar no longer shows the two cards, no sort chip).
This commit is contained in:
@@ -4,13 +4,11 @@ import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Layers, ListMusic, Plus, SlidersHorizontal, X } from "lucide-react";
|
||||
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";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
import SidePanel from "./SidePanel";
|
||||
import PanelGroups, { type PanelItem } from "./PanelGroups";
|
||||
import SortControl from "./SortControl";
|
||||
import SegmentedToggle from "./SegmentedToggle";
|
||||
import FilterChipGroup, { filterChipClass } from "./FilterChipGroup";
|
||||
import { usePlexClearFilters } from "./usePlexActiveFilters";
|
||||
@@ -66,14 +64,8 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
// Scope / watch-state / sort / expanded filters live in PlexProvider (shared with PlexBrowse and
|
||||
// the header search box); this rail is the primary editor of them. Opening a playlist is a
|
||||
// hand-off to PlexBrowse via the shared `playlistOpen` slot.
|
||||
const { scope, show, sort, filters } = usePlex();
|
||||
const {
|
||||
setScope,
|
||||
setShow,
|
||||
setSort,
|
||||
setFilters,
|
||||
setPlaylistOpen: onOpenPlaylist,
|
||||
} = usePlexActions();
|
||||
const { scope, show, filters } = usePlex();
|
||||
const { setShow, setFilters, setPlaylistOpen: onOpenPlaylist } = usePlexActions();
|
||||
const collFade = useScrollFade();
|
||||
const playlistsQ = useQuery({ queryKey: qk.plexPlaylists(), queryFn: () => api.plexPlaylists() });
|
||||
const [newPlaylist, setNewPlaylist] = useState("");
|
||||
@@ -106,10 +98,9 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
const collections = collectionsQ.data?.collections ?? [];
|
||||
|
||||
const fCount = plexFilterCount(filters);
|
||||
// Heal against the current scope so a stale out-of-scope key (e.g. movie "duration" after switching
|
||||
// to shows) doesn't inflate the active-filter count while the control + grid show the default.
|
||||
const healedSort = healPlexSort(sort, scope);
|
||||
const activeCount = fCount + (show !== "all" ? 1 : 0) + (healedSort.key !== "title" ? 1 : 0);
|
||||
// Scope and sort moved to the header / above-the-grid toolbar (S3); the panel badge now counts only
|
||||
// the filters that still live in this panel (expanded filters + watch-state).
|
||||
const activeCount = fCount + (show !== "all" ? 1 : 0);
|
||||
const anyActive = activeCount > 0;
|
||||
|
||||
const patch = (p: Partial<PlexFilters>) => setFilters({ ...filters, ...p });
|
||||
@@ -117,7 +108,6 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
arr.includes(v) ? arr.filter((x) => x !== v) : [...arr, v];
|
||||
const clearAll = usePlexClearFilters();
|
||||
|
||||
const sortOptions = plexSortOptions(scope, (k) => t(`plex.sort.${k}`));
|
||||
const durBucketKey = DURATION_BUCKETS.find(
|
||||
(b) => (filters.durationMin ?? null) === b.min && (filters.durationMax ?? null) === b.max,
|
||||
)?.key;
|
||||
@@ -128,23 +118,6 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
// so facet-gated / scope-gated sections drop out cleanly; PanelGroups orders them by the saved
|
||||
// layout and lets the user reorder / hide / collapse them.
|
||||
const items: PanelItem[] = [
|
||||
{
|
||||
id: "scope",
|
||||
title: t("plex.filter.scope"),
|
||||
body: (
|
||||
<SegmentedToggle
|
||||
ariaLabel={t("plex.filter.scope")}
|
||||
fullWidth
|
||||
size="md"
|
||||
options={(["both", "movie", "show"] as const).map((s) => ({
|
||||
value: s,
|
||||
label: t(`plex.filter.scopeOpt.${s}`),
|
||||
}))}
|
||||
value={scope}
|
||||
onChange={setScope}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "playlists",
|
||||
title: t("plex.playlist.section"),
|
||||
@@ -199,20 +172,6 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "sort",
|
||||
title: t("plex.filter.sort"),
|
||||
body: (
|
||||
<SortControl
|
||||
options={sortOptions}
|
||||
sort={healedSort}
|
||||
onChange={(next) => setSort(next ?? PLEX_SORT_FALLBACK)}
|
||||
label={t("plex.filter.sort")}
|
||||
ascLabel={t("plex.filter.dir.asc")}
|
||||
descLabel={t("plex.filter.dir.desc")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(peopleActive
|
||||
? [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user