feat(filters): the any/all switch sits on the panel above its picker
ui/MatchToggle replaces the Combobox popover footer (Feed tags, topics, languages; Plex collections) and PlexSidebar's own toggle (genres, actors): shown from two picks, one wording. A closed picker had hidden that "all" was on.
This commit is contained in:
@@ -12,7 +12,7 @@ import { type MediaSourceKind } from "../lib/mediaSource";
|
||||
import { useDebounced } from "../lib/useDebounced";
|
||||
import FilterRail from "./FilterRail";
|
||||
import { type PanelItem } from "./ui/PanelGroups";
|
||||
import SegmentedToggle from "./ui/SegmentedToggle";
|
||||
import MatchToggle from "./ui/MatchToggle";
|
||||
import FilterChipGroup from "./ui/FilterChipGroup";
|
||||
import Combobox, { type ComboboxOption } from "./ui/Combobox";
|
||||
import SavedViewsPopover from "./SavedViewsPopover";
|
||||
@@ -43,31 +43,6 @@ const DURATION_BUCKETS: { key: string; min: number | null; max: number | null }[
|
||||
{ key: "long", min: 120 * 60, max: null },
|
||||
];
|
||||
|
||||
// The any/all combine toggle for a multi-value filter (genres, actors) — a thin binding of the shared
|
||||
// SegmentedToggle to the Plex match vocabulary (same look as the feed rail's tag MATCH toggle).
|
||||
function MatchToggle({
|
||||
mode,
|
||||
onChange,
|
||||
}: {
|
||||
mode: "any" | "all";
|
||||
onChange: (m: "any" | "all") => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<SegmentedToggle
|
||||
size="sm"
|
||||
label={t("plex.filter.matchLabel")}
|
||||
ariaLabel={t("plex.filter.matchLabel")}
|
||||
options={[
|
||||
{ value: "any", label: t("plex.filter.match.any") },
|
||||
{ value: "all", label: t("plex.filter.match.all") },
|
||||
]}
|
||||
value={mode}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PlexSidebar({ layout, setLayout, collapsed, onToggleCollapse }: Props) {
|
||||
const { t } = useTranslation();
|
||||
// Scope / watch-state / sort / expanded filters live in PlexProvider (shared with PlexBrowse and
|
||||
@@ -131,14 +106,12 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
onRemove={() => patch({ directors: filters.directors.filter((x) => x !== d) })}
|
||||
/>
|
||||
))}
|
||||
{filters.actors.length > 1 && (
|
||||
<div className="w-full">
|
||||
<MatchToggle
|
||||
mode={filters.actorMode ?? "any"}
|
||||
onChange={(m) => patch({ actorMode: m })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<MatchToggle
|
||||
picked={filters.actors.length}
|
||||
className="w-full"
|
||||
value={filters.actorMode ?? "any"}
|
||||
onChange={(m) => patch({ actorMode: m })}
|
||||
/>
|
||||
{filters.actors.map((a) => (
|
||||
<RemovableChip
|
||||
key={`a:${a}`}
|
||||
@@ -186,14 +159,11 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
title: t("plex.filter.genre"),
|
||||
body: (
|
||||
<>
|
||||
{filters.genres.length > 1 && (
|
||||
<div className="mb-1.5">
|
||||
<MatchToggle
|
||||
mode={filters.genreMode ?? "any"}
|
||||
onChange={(m) => patch({ genreMode: m })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<MatchToggle
|
||||
picked={filters.genres.length}
|
||||
value={filters.genreMode ?? "any"}
|
||||
onChange={(m) => patch({ genreMode: m })}
|
||||
/>
|
||||
<Combobox
|
||||
label={t("plex.filter.genre")}
|
||||
options={facets.genres}
|
||||
@@ -472,29 +442,30 @@ function CollectionPicker({
|
||||
for (const o of options) if (o.label) labelMap[o.value] = o.label;
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
label={t("plex.filter.collection")}
|
||||
clientFilter={false} // the server already narrows to `search`
|
||||
options={options}
|
||||
selected={values}
|
||||
selectedLabels={resolved}
|
||||
query={search}
|
||||
onQueryChange={setSearch}
|
||||
onOpenChange={setOpen}
|
||||
loading={q.isFetching}
|
||||
placeholder={t("plex.filter.collectionSearch")}
|
||||
emptyText={t("plex.filter.collectionEmpty")}
|
||||
matchMode={mode === "all" ? "and" : "or"}
|
||||
onMatchModeChange={(m) => onModeChange(m === "and" ? "all" : "any")}
|
||||
onChange={(next) => {
|
||||
// Carry only the selected ids' titles forward (drop titles for de-selected ones). Collapse an
|
||||
// empty map to undefined so a clear-all key-matches the pristine state (an empty {} still hashes
|
||||
// differently from an absent key), mirroring the chip-row remove handler.
|
||||
const nextTitles: Record<string, string> = {};
|
||||
for (const id of next) nextTitles[id] = labelMap[id] ?? titles?.[id] ?? id;
|
||||
onChange(next, next.length ? nextTitles : undefined);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<MatchToggle picked={values.length} value={mode} onChange={onModeChange} />
|
||||
<Combobox
|
||||
label={t("plex.filter.collection")}
|
||||
clientFilter={false} // the server already narrows to `search`
|
||||
options={options}
|
||||
selected={values}
|
||||
selectedLabels={resolved}
|
||||
query={search}
|
||||
onQueryChange={setSearch}
|
||||
onOpenChange={setOpen}
|
||||
loading={q.isFetching}
|
||||
placeholder={t("plex.filter.collectionSearch")}
|
||||
emptyText={t("plex.filter.collectionEmpty")}
|
||||
onChange={(next) => {
|
||||
// Carry only the selected ids' titles forward (drop titles for de-selected ones). Collapse an
|
||||
// empty map to undefined so a clear-all key-matches the pristine state (an empty {} still hashes
|
||||
// differently from an absent key), mirroring the chip-row remove handler.
|
||||
const nextTitles: Record<string, string> = {};
|
||||
for (const id of next) nextTitles[id] = labelMap[id] ?? titles?.[id] ?? id;
|
||||
onChange(next, next.length ? nextTitles : undefined);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user