fix(library): hide video-only filter sections on audiobook libraries

Plumb the active libraryType from LibraryBrowse →
CatalogFiltersPanel → CatalogFilterSheet → CollectionGuidedRulesEditor
so the guided filter sheet can react to it. When the type is
"audiobook[s]", hide:

- Min IMDb Rating / Content Rating row
- Actor / Director row
- Writer / Producer row
- Studio / Network row
- Video Quality (4K / HDR / DOVI) badges

Country, Genres, Year/Decade, Original Language, Match Status, and
the time-since rows stay visible — they're meaningful for audiobooks
and shipping new audiobook-native filter fields (Author / Narrator /
Series) is its own change (plan at
docs/superpowers/plans/2026-05-27-audiobook-catalog-filter-fields.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
RXWatcher
2026-05-27 18:44:26 +02:00
co-authored by Claude Opus 4.7
parent 1c291935c5
commit 69c221fe39
3 changed files with 139 additions and 112 deletions
@@ -62,6 +62,7 @@ interface CatalogFilterSheetProps {
onQueryDefinitionChange: (qd: QueryDefinition) => void;
filters?: CatalogFiltersResponse;
filtersLoading?: boolean;
libraryType?: string;
}
export default function CatalogFilterSheet({
@@ -80,6 +81,7 @@ export default function CatalogFilterSheet({
onQueryDefinitionChange,
filters,
filtersLoading = false,
libraryType,
}: CatalogFilterSheetProps) {
// Portal container inside the Sheet's DOM so that react-remove-scroll
// (activated by the Dialog/Sheet) does not block scroll events inside
@@ -152,6 +154,7 @@ export default function CatalogFilterSheet({
showSortControls={false}
filters={filters}
filtersLoading={filtersLoading}
libraryType={libraryType}
/>
)}
</div>
@@ -26,6 +26,7 @@ export interface CatalogFiltersPanelProps {
sortRelevanceScope?: QuerySortRelevanceScope;
resultCountLabel?: string;
resultCountLoading?: boolean;
libraryType?: string;
}
export default function CatalogFiltersPanel({
@@ -39,6 +40,7 @@ export default function CatalogFiltersPanel({
sortRelevanceScope,
resultCountLabel,
resultCountLoading = false,
libraryType,
}: CatalogFiltersPanelProps) {
const [sheetOpen, setSheetOpen] = useState(false);
const [editorMode, setEditorMode] = useState<"guided" | "advanced">("guided");
@@ -107,6 +109,7 @@ export default function CatalogFiltersPanel({
onQueryDefinitionChange={(nextQd) =>
onStateChange({ ...state, query_definition: nextQd })
}
libraryType={libraryType}
/>
) : null}
</div>
@@ -129,6 +132,7 @@ export function CatalogFilterSheetContainer({
onEditorModeChange,
queryDefinition,
onQueryDefinitionChange,
libraryType,
}: {
state: CatalogSearchState;
open: boolean;
@@ -145,6 +149,7 @@ export function CatalogFilterSheetContainer({
onEditorModeChange: (mode: "guided" | "advanced") => void;
queryDefinition: QueryDefinition;
onQueryDefinitionChange: (nextQd: QueryDefinition) => void;
libraryType?: string;
}) {
const filtersQuery = useCatalogFilters(state, {
enabled: editorMode === "guided",
@@ -169,6 +174,7 @@ export function CatalogFilterSheetContainer({
onQueryDefinitionChange={onQueryDefinitionChange}
filters={filtersQuery.data}
filtersLoading={filtersQuery.isLoading}
libraryType={libraryType}
/>
);
}
@@ -358,6 +358,11 @@ interface CollectionGuidedRulesEditorProps {
showSortControls?: boolean;
filters?: CatalogFiltersResponse;
filtersLoading?: boolean;
// When the editor is scoped to a single library (e.g. on the library
// browse page) pass its type so video-only filter sections (Director,
// Studio, Network, Content Rating, Video Quality, etc.) can be hidden
// for audiobook libraries.
libraryType?: string;
}
export default function CollectionGuidedRulesEditor({
@@ -373,11 +378,16 @@ export default function CollectionGuidedRulesEditor({
showSortControls = true,
filters: providedFilters,
filtersLoading: providedFiltersLoading,
libraryType,
}: CollectionGuidedRulesEditorProps) {
const state = useMemo(() => queryDefinitionToGuidedState(value), [value]);
const metadataFiltersQuery = useCatalogMetadataFilters();
const filters = providedFilters ?? metadataFiltersQuery.data;
const filtersLoading = providedFiltersLoading ?? metadataFiltersQuery.isLoading;
// Audiobook-only libraries should hide video-only filter sections.
// Backend stores type as the plural "audiobooks"; the singular form
// shows up in some API surfaces — accept either.
const isAudiobookLibrary = libraryType === "audiobook" || libraryType === "audiobooks";
const sortOptions = getCollectionSortOptions(allowPersonalizedSorts, sortRelevanceScope);
const selectedSort = normalizeQuerySortForScope(
{ field: state.sortField, order: state.sortOrder },
@@ -518,32 +528,34 @@ export default function CollectionGuidedRulesEditor({
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Minimum IMDb Rating</Label>
<Input
type="number"
min={0}
max={10}
step={0.1}
value={state.minRating}
onChange={(e) => update({ minRating: e.target.value })}
placeholder="e.g. 7.0"
disabled={readOnly}
/>
{isAudiobookLibrary ? null : (
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Minimum IMDb Rating</Label>
<Input
type="number"
min={0}
max={10}
step={0.1}
value={state.minRating}
onChange={(e) => update({ minRating: e.target.value })}
placeholder="e.g. 7.0"
disabled={readOnly}
/>
</div>
<div className="space-y-2">
<Label>Content Rating</Label>
<SearchableSelect
options={filters?.content_ratings ?? []}
value={state.contentRating}
onChange={(contentRating) => update({ contentRating })}
placeholder="Select rating..."
disabled={readOnly}
isLoading={filtersLoading}
/>
</div>
</div>
<div className="space-y-2">
<Label>Content Rating</Label>
<SearchableSelect
options={filters?.content_ratings ?? []}
value={state.contentRating}
onChange={(contentRating) => update({ contentRating })}
placeholder="Select rating..."
disabled={readOnly}
isLoading={filtersLoading}
/>
</div>
</div>
)}
<div className="space-y-2">
<Label>Original Language</Label>
@@ -558,68 +570,72 @@ export default function CollectionGuidedRulesEditor({
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Actor</Label>
<PersonSearchSelect
value={state.actor}
onChange={(actor) => update({ actor })}
disabled={readOnly}
/>
</div>
<div className="space-y-2">
<Label>Director</Label>
<PersonSearchSelect
value={state.director}
onChange={(director) => update({ director })}
disabled={readOnly}
/>
</div>
</div>
{isAudiobookLibrary ? null : (
<>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Actor</Label>
<PersonSearchSelect
value={state.actor}
onChange={(actor) => update({ actor })}
disabled={readOnly}
/>
</div>
<div className="space-y-2">
<Label>Director</Label>
<PersonSearchSelect
value={state.director}
onChange={(director) => update({ director })}
disabled={readOnly}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Writer</Label>
<PersonSearchSelect
value={state.writer}
onChange={(writer) => update({ writer })}
disabled={readOnly}
/>
</div>
<div className="space-y-2">
<Label>Producer</Label>
<PersonSearchSelect
value={state.producer}
onChange={(producer) => update({ producer })}
disabled={readOnly}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Writer</Label>
<PersonSearchSelect
value={state.writer}
onChange={(writer) => update({ writer })}
disabled={readOnly}
/>
</div>
<div className="space-y-2">
<Label>Producer</Label>
<PersonSearchSelect
value={state.producer}
onChange={(producer) => update({ producer })}
disabled={readOnly}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Studio</Label>
<SearchableSelect
options={filters?.studios ?? []}
value={state.studio}
onChange={(studio) => update({ studio })}
placeholder="Select studio..."
disabled={readOnly}
isLoading={filtersLoading}
/>
</div>
<div className="space-y-2">
<Label>Network</Label>
<SearchableSelect
options={filters?.networks ?? []}
value={state.network}
onChange={(network) => update({ network })}
placeholder="Select network..."
disabled={readOnly}
isLoading={filtersLoading}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Studio</Label>
<SearchableSelect
options={filters?.studios ?? []}
value={state.studio}
onChange={(studio) => update({ studio })}
placeholder="Select studio..."
disabled={readOnly}
isLoading={filtersLoading}
/>
</div>
<div className="space-y-2">
<Label>Network</Label>
<SearchableSelect
options={filters?.networks ?? []}
value={state.network}
onChange={(network) => update({ network })}
placeholder="Select network..."
disabled={readOnly}
isLoading={filtersLoading}
/>
</div>
</div>
</>
)}
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
@@ -681,34 +697,36 @@ export default function CollectionGuidedRulesEditor({
) : null}
</div>
<div className="space-y-2">
<Label>Video Quality</Label>
<div className="flex flex-wrap gap-2">
{[
{ key: "fourK", label: "4K" },
{ key: "hdr", label: "HDR" },
{ key: "dolbyVision", label: "DOVI" },
].map((option) => {
const selected = state[option.key as keyof GuidedFormState] === true;
return (
<Button
key={option.key}
type="button"
variant={selected ? "default" : "outline"}
size="sm"
onClick={() =>
update({
[option.key]: !selected,
} as Partial<GuidedFormState>)
}
disabled={readOnly}
>
{option.label}
</Button>
);
})}
{isAudiobookLibrary ? null : (
<div className="space-y-2">
<Label>Video Quality</Label>
<div className="flex flex-wrap gap-2">
{[
{ key: "fourK", label: "4K" },
{ key: "hdr", label: "HDR" },
{ key: "dolbyVision", label: "DOVI" },
].map((option) => {
const selected = state[option.key as keyof GuidedFormState] === true;
return (
<Button
key={option.key}
type="button"
variant={selected ? "default" : "outline"}
size="sm"
onClick={() =>
update({
[option.key]: !selected,
} as Partial<GuidedFormState>)
}
disabled={readOnly}
>
{option.label}
</Button>
);
})}
</div>
</div>
</div>
)}
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">