From 9ed178342f13087d48d58ef32e4153728b1834cf Mon Sep 17 00:00:00 2001
From: schroda <50052685+schroda@users.noreply.github.com>
Date: Tue, 21 Jul 2026 00:51:14 +0200
Subject: [PATCH] Collapse library filters
---
CHANGELOG.md | 3 +-
src/base/components/Collapsable.tsx | 75 +++++++++++++
.../components/LibraryOptionsPanel.tsx | 100 ++++++++++++------
3 files changed, 145 insertions(+), 33 deletions(-)
create mode 100644 src/base/components/Collapsable.tsx
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 637648b6..31010d3f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added
-- (**Library**) Add source filter to library filtering options
- (**Migration**) Add a search option to ignore outdated matches
- (**Migration**) Add a search option to ignore matches with missing chapters
- (**Migration**) Add "local source" as a possible destination source
@@ -19,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- (**Reader**) Add setting to keep the screen on while reading
- (**Extension**) Support installing external JARs
- (**Reader**) Add auto background color setting
+- (**Library**) Add source filter
- (**Library**) Add random sort option to library
### Changed
@@ -49,6 +49,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- (**History**) Show only the last read chapter per manga
- (**Updates**) Show only the first unread chapter per manga per day
- (**Manga**) Hide details page title copy button in unsupported environments (secure context (https, localhost) required)
+- (**Library**) Collapse `Status`, `Tracked` and `Source` filters
### Fixed
diff --git a/src/base/components/Collapsable.tsx b/src/base/components/Collapsable.tsx
new file mode 100644
index 00000000..0b3b658c
--- /dev/null
+++ b/src/base/components/Collapsable.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) Contributors to the Suwayomi project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+import type { CollapseProps } from '@mui/material/Collapse';
+import Collapse from '@mui/material/Collapse';
+import type { StackProps } from '@mui/material/Stack';
+import Stack from '@mui/material/Stack';
+import type { ReactNode } from 'react';
+import { useState } from 'react';
+import ExpandLess from '@mui/icons-material/ExpandLess';
+import ExpandMore from '@mui/icons-material/ExpandMore';
+
+export const Collapsable = ({
+ header,
+ collapse,
+ initialState,
+ slots,
+}: {
+ header: ReactNode;
+ collapse: ReactNode;
+ initialState?: boolean;
+ slots?: {
+ headerWrapper?: StackProps;
+ collapse?: CollapseProps;
+ };
+}) => {
+ const [isOpen, setIsOpen] = useState(initialState);
+
+ return (
+ <>
+ setIsOpen(!isOpen)}
+ >
+ {header}
+ {isOpen ? : }
+
+ {
+ const wrapperInner = slots?.collapse?.slotProps?.wrapperInner;
+ const wrapperInnerProps =
+ typeof wrapperInner === 'function' ? wrapperInner(ownerState) : wrapperInner;
+
+ return {
+ ...wrapperInnerProps,
+ sx: {
+ display: 'flex',
+ flexDirection: 'column',
+ ...wrapperInnerProps?.sx,
+ },
+ };
+ },
+ }}
+ in={isOpen}
+ >
+ {collapse}
+
+ >
+ );
+};
diff --git a/src/features/library/components/LibraryOptionsPanel.tsx b/src/features/library/components/LibraryOptionsPanel.tsx
index 95c3f803..f071784b 100644
--- a/src/features/library/components/LibraryOptionsPanel.tsx
+++ b/src/features/library/components/LibraryOptionsPanel.tsx
@@ -11,6 +11,7 @@ import FormLabel from '@mui/material/FormLabel';
import RadioGroup from '@mui/material/RadioGroup';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
+import type { ReactNode } from 'react';
import { useMemo } from 'react';
import uniqBy from 'lodash/fp/uniqBy';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
@@ -36,6 +37,7 @@ import type { CategoryMetadataInfo } from '@/features/category/Category.types.ts
import { MANGA_STATUS_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts';
import { GridLayout } from '@/base/Base.types';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
+import { Collapsable } from '@/base/components/Collapsable.tsx';
const TITLES: { [key in 'filter' | 'sort' | 'display']: MessageDescriptor } = {
filter: msg`Filter`,
@@ -54,6 +56,23 @@ const SORT_OPTIONS: [LibrarySortMode, MessageDescriptor][] = [
['random', msg`Random`],
];
+const CollapsableFilter = ({ title, items, isActive }: { title: string; items: ReactNode[]; isActive: boolean }) => (
+
+);
+
export const LibraryOptionsPanel = ({
category,
open,
@@ -90,6 +109,14 @@ export const LibraryOptionsPanel = ({
makeToast(t`Could not save the default search settings to the server`, 'error', getErrorMessage(e)),
);
+ const isStatusFilterActive = Object.values(MangaStatus).some(
+ (status) => categoryLibraryOptions.hasStatus[status] != null,
+ );
+ const isTrackerFilterActive = loggedInTrackers.some(
+ (tracker) => categoryLibraryOptions.hasTrackerBinding[tracker.id] != null,
+ );
+ const isSourceFilterActive = librarySources.some((source) => categoryLibraryOptions.hasSource[source.id] != null);
+
return (
open={open}
@@ -125,38 +152,46 @@ export const LibraryOptionsPanel = ({
checked={categoryLibraryOptions.hasDuplicateChapters}
onChange={(c) => updateCategoryLibraryOptions('hasDuplicateChapters', c)}
/>
- {t`Status`}
- {Object.values(MangaStatus).map((status) => (
-
- updateCategoryLibraryOptions('hasStatus', {
- ...categoryLibraryOptions.hasStatus,
- [status]: checked,
- })
- }
+ (
+
+ updateCategoryLibraryOptions('hasStatus', {
+ ...categoryLibraryOptions.hasStatus,
+ [status]: checked,
+ })
+ }
+ />
+ ))}
+ isActive={isStatusFilterActive}
+ />
+ {!!loggedInTrackers.length && (
+ (
+
+ updateCategoryLibraryOptions('hasTrackerBinding', {
+ ...categoryLibraryOptions.hasTrackerBinding,
+ [tracker.id]: checked,
+ })
+ }
+ />
+ ))}
+ isActive={isTrackerFilterActive}
/>
- ))}
- {t`Tracked`}
- {loggedInTrackers.map((tracker) => (
-
- updateCategoryLibraryOptions('hasTrackerBinding', {
- ...categoryLibraryOptions.hasTrackerBinding,
- [tracker.id]: checked,
- })
- }
- />
- ))}
- {librarySources.length > 0 && (
- <>
- {t`Source`}
- {librarySources.map((source) => (
+ )}
+ {!!librarySources.length && (
+ (
))}
- >
+ isActive={isSourceFilterActive}
+ />
)}
>
);