+
{browseQ.isLoading
? " "
@@ -354,6 +356,18 @@ export default function PlexBrowse() {
? t("plex.searchCount", { count: total })
: t("plex.count", { count: total })}
+ {/* Sort moved out of the filter sidebar to the row above the cards (S3), like the Feed. */}
+
+ {t("plex.filter.sort")}
+ setSort(next ?? PLEX_SORT_FALLBACK)}
+ label={t("plex.filter.sort")}
+ ascLabel={t("plex.filter.dir.asc")}
+ descLabel={t("plex.filter.dir.desc")}
+ />
+
diff --git a/frontend/src/components/PlexScopeToggle.tsx b/frontend/src/components/PlexScopeToggle.tsx
new file mode 100644
index 00000000..93e58a78
--- /dev/null
+++ b/frontend/src/components/PlexScopeToggle.tsx
@@ -0,0 +1,26 @@
+import { useTranslation } from "react-i18next";
+import SegmentedToggle from "./SegmentedToggle";
+import { usePlex, usePlexActions } from "./PlexProvider";
+
+// The Plex library scope (both | movie | show) as a compact header control, sitting next to the
+// search box (S3 toolbar relocation — it used to be a sidebar island). Reads/writes the same
+// provider state the sidebar did, so scope stays in sync everywhere; reuses the shared
+// SegmentedToggle and the existing plex.filter.scopeOpt.* labels.
+const SCOPES = ["both", "movie", "show"] as const;
+
+export default function PlexScopeToggle() {
+ const { t } = useTranslation();
+ const { scope } = usePlex();
+ const { setScope } = usePlexActions();
+ return (
+
+ ({ value: s, label: t(`plex.filter.scopeOpt.${s}`) }))}
+ value={scope}
+ onChange={setScope}
+ />
+
+ );
+}
diff --git a/frontend/src/components/PlexSidebar.tsx b/frontend/src/components/PlexSidebar.tsx
index 236865cf..d4035efa 100644
--- a/frontend/src/components/PlexSidebar.tsx
+++ b/frontend/src/components/PlexSidebar.tsx
@@ -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
) => 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: (
- ({
- 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: (
- setSort(next ?? PLEX_SORT_FALLBACK)}
- label={t("plex.filter.sort")}
- ascLabel={t("plex.filter.dir.asc")}
- descLabel={t("plex.filter.dir.desc")}
- />
- ),
- },
...(peopleActive
? [
{
diff --git a/frontend/src/components/usePlexActiveFilters.ts b/frontend/src/components/usePlexActiveFilters.ts
index 4b12e019..5283cd44 100644
--- a/frontend/src/components/usePlexActiveFilters.ts
+++ b/frontend/src/components/usePlexActiveFilters.ts
@@ -1,19 +1,20 @@
import { useTranslation } from "react-i18next";
import { type ActiveFilter } from "../lib/useActiveFilters";
import { EMPTY_PLEX_FILTERS, type PlexFilters } from "../lib/api";
-import { healPlexSort, PLEX_SORT_FALLBACK } from "../lib/plexSort";
+import { PLEX_SORT_FALLBACK } from "../lib/plexSort";
import { usePlex, usePlexActions } from "./PlexProvider";
-// Plex's analogue of useActiveFilters: turns the shared Plex state (expanded filters + watch-state +
-// sort) into the source-agnostic ActiveFilter[] the shared ActiveFilterChips renders above the grid.
+// Plex's analogue of useActiveFilters: turns the shared Plex state (expanded filters + watch-state)
+// into the source-agnostic ActiveFilter[] the shared ActiveFilterChips renders above the grid.
// Plex thereby gets the same at-a-glance "what's narrowing this" row — with per-chip removal — that
// the feed has, instead of the narrowing being visible only inside the collapsed sidebar. Labels reuse
// the existing plex.* i18n keys; multi-value dimensions (genres, people, ratings) emit one chip each so
-// removing one keeps the rest (same shape as the feed's tag/channel chips). Lives here, not in lib,
-// because it reads the Plex provider (lib stays provider-free, like useActiveFilters).
+// removing one keeps the rest (same shape as the feed's tag/channel chips). Sort is NOT a chip here —
+// it's an always-visible control above the grid (S3), like the feed. Lives here, not in lib, because
+// it reads the Plex provider (lib stays provider-free, like useActiveFilters).
export function usePlexActiveFilters(): ActiveFilter[] {
- const { scope, show, sort, filters } = usePlex();
- const { setShow, setSort, setFilters } = usePlexActions();
+ const { show, filters } = usePlex();
+ const { setShow, setFilters } = usePlexActions();
const { t } = useTranslation();
const patch = (p: Partial) => setFilters({ ...filters, ...p });
const out: ActiveFilter[] = [];
@@ -108,14 +109,6 @@ export function usePlexActiveFilters(): ActiveFilter[] {
remove: () => setShow("all"),
});
}
- const healed = healPlexSort(sort, scope);
- if (healed.key !== "title") {
- out.push({
- key: "sort",
- label: t("plex.sort." + healed.key),
- remove: () => setSort(PLEX_SORT_FALLBACK),
- });
- }
return out;
}
diff --git a/frontend/src/lib/panelLayout.ts b/frontend/src/lib/panelLayout.ts
index a8aad5c9..ed443e84 100644
--- a/frontend/src/lib/panelLayout.ts
+++ b/frontend/src/lib/panelLayout.ts
@@ -18,10 +18,8 @@ export interface PanelLayout {
const PANEL_ORDER: Record = {
feed: ["savedviews", "date", "channel", "tags", "language", "topic"],
plex: [
- "scope",
"playlists",
"watch",
- "sort",
"people",
"collection",
"rating",