Collapse library filters

This commit is contained in:
schroda
2026-07-21 00:51:14 +02:00
parent 08dcd932dc
commit 9ed178342f
3 changed files with 145 additions and 33 deletions

View File

@@ -8,7 +8,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added ### 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 outdated matches
- (**Migration**) Add a search option to ignore matches with missing chapters - (**Migration**) Add a search option to ignore matches with missing chapters
- (**Migration**) Add "local source" as a possible destination source - (**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 - (**Reader**) Add setting to keep the screen on while reading
- (**Extension**) Support installing external JARs - (**Extension**) Support installing external JARs
- (**Reader**) Add auto background color setting - (**Reader**) Add auto background color setting
- (**Library**) Add source filter
- (**Library**) Add random sort option to library - (**Library**) Add random sort option to library
### Changed ### 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 - (**History**) Show only the last read chapter per manga
- (**Updates**) Show only the first unread chapter per manga per day - (**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) - (**Manga**) Hide details page title copy button in unsupported environments (secure context (https, localhost) required)
- (**Library**) Collapse `Status`, `Tracked` and `Source` filters
### Fixed ### Fixed

View File

@@ -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 (
<>
<Stack
{...slots?.headerWrapper}
sx={{
flexDirection: 'row',
alignItems: 'flex-end',
gap: 1,
cursor: 'pointer',
...slots?.headerWrapper?.sx,
}}
onClick={() => setIsOpen(!isOpen)}
>
{header}
{isOpen ? <ExpandLess /> : <ExpandMore />}
</Stack>
<Collapse
{...slots?.collapse}
slotProps={{
...slots?.collapse?.slotProps,
wrapperInner: (ownerState) => {
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}
</Collapse>
</>
);
};

View File

@@ -11,6 +11,7 @@ import FormLabel from '@mui/material/FormLabel';
import RadioGroup from '@mui/material/RadioGroup'; import RadioGroup from '@mui/material/RadioGroup';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import type { ReactNode } from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import uniqBy from 'lodash/fp/uniqBy'; import uniqBy from 'lodash/fp/uniqBy';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx'; 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 { MANGA_STATUS_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts';
import { GridLayout } from '@/base/Base.types'; import { GridLayout } from '@/base/Base.types';
import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { Collapsable } from '@/base/components/Collapsable.tsx';
const TITLES: { [key in 'filter' | 'sort' | 'display']: MessageDescriptor } = { const TITLES: { [key in 'filter' | 'sort' | 'display']: MessageDescriptor } = {
filter: msg`Filter`, filter: msg`Filter`,
@@ -54,6 +56,23 @@ const SORT_OPTIONS: [LibrarySortMode, MessageDescriptor][] = [
['random', msg`Random`], ['random', msg`Random`],
]; ];
const CollapsableFilter = ({ title, items, isActive }: { title: string; items: ReactNode[]; isActive: boolean }) => (
<Collapsable
header={title}
collapse={items}
initialState={isActive}
slots={{
headerWrapper: {
component: FormLabel,
sx: {
mt: 2,
color: isActive ? 'warning.main' : undefined,
},
},
}}
/>
);
export const LibraryOptionsPanel = ({ export const LibraryOptionsPanel = ({
category, category,
open, open,
@@ -90,6 +109,14 @@ export const LibraryOptionsPanel = ({
makeToast(t`Could not save the default search settings to the server`, 'error', getErrorMessage(e)), 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 ( return (
<OptionsTabs<'filter' | 'sort' | 'display'> <OptionsTabs<'filter' | 'sort' | 'display'>
open={open} open={open}
@@ -125,8 +152,9 @@ export const LibraryOptionsPanel = ({
checked={categoryLibraryOptions.hasDuplicateChapters} checked={categoryLibraryOptions.hasDuplicateChapters}
onChange={(c) => updateCategoryLibraryOptions('hasDuplicateChapters', c)} onChange={(c) => updateCategoryLibraryOptions('hasDuplicateChapters', c)}
/> />
<FormLabel sx={{ mt: 2 }}>{t`Status`}</FormLabel> <CollapsableFilter
{Object.values(MangaStatus).map((status) => ( title={t`Status`}
items={Object.values(MangaStatus).map((status) => (
<ThreeStateCheckboxInput <ThreeStateCheckboxInput
key={status} key={status}
label={t(MANGA_STATUS_TO_TRANSLATION[status])} label={t(MANGA_STATUS_TO_TRANSLATION[status])}
@@ -139,8 +167,12 @@ export const LibraryOptionsPanel = ({
} }
/> />
))} ))}
<FormLabel sx={{ mt: 2 }}>{t`Tracked`}</FormLabel> isActive={isStatusFilterActive}
{loggedInTrackers.map((tracker) => ( />
{!!loggedInTrackers.length && (
<CollapsableFilter
title={t`Tracked`}
items={loggedInTrackers.map((tracker) => (
<ThreeStateCheckboxInput <ThreeStateCheckboxInput
key={tracker.id} key={tracker.id}
label={tracker.name} label={tracker.name}
@@ -153,10 +185,13 @@ export const LibraryOptionsPanel = ({
} }
/> />
))} ))}
{librarySources.length > 0 && ( isActive={isTrackerFilterActive}
<> />
<FormLabel sx={{ mt: 2 }}>{t`Source`}</FormLabel> )}
{librarySources.map((source) => ( {!!librarySources.length && (
<CollapsableFilter
title={t`Source`}
items={librarySources.map((source) => (
<ThreeStateCheckboxInput <ThreeStateCheckboxInput
key={source.id} key={source.id}
label={source.displayName} label={source.displayName}
@@ -169,7 +204,8 @@ export const LibraryOptionsPanel = ({
} }
/> />
))} ))}
</> isActive={isSourceFilterActive}
/>
)} )}
</> </>
); );