Collapse library filters
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
75
src/base/components/Collapsable.tsx
Normal file
75
src/base/components/Collapsable.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,38 +152,46 @@ 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`}
|
||||||
<ThreeStateCheckboxInput
|
items={Object.values(MangaStatus).map((status) => (
|
||||||
key={status}
|
<ThreeStateCheckboxInput
|
||||||
label={t(MANGA_STATUS_TO_TRANSLATION[status])}
|
key={status}
|
||||||
checked={categoryLibraryOptions.hasStatus[status]}
|
label={t(MANGA_STATUS_TO_TRANSLATION[status])}
|
||||||
onChange={(checked) =>
|
checked={categoryLibraryOptions.hasStatus[status]}
|
||||||
updateCategoryLibraryOptions('hasStatus', {
|
onChange={(checked) =>
|
||||||
...categoryLibraryOptions.hasStatus,
|
updateCategoryLibraryOptions('hasStatus', {
|
||||||
[status]: checked,
|
...categoryLibraryOptions.hasStatus,
|
||||||
})
|
[status]: checked,
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
isActive={isStatusFilterActive}
|
||||||
|
/>
|
||||||
|
{!!loggedInTrackers.length && (
|
||||||
|
<CollapsableFilter
|
||||||
|
title={t`Tracked`}
|
||||||
|
items={loggedInTrackers.map((tracker) => (
|
||||||
|
<ThreeStateCheckboxInput
|
||||||
|
key={tracker.id}
|
||||||
|
label={tracker.name}
|
||||||
|
checked={categoryLibraryOptions.hasTrackerBinding[tracker.id]}
|
||||||
|
onChange={(checked) =>
|
||||||
|
updateCategoryLibraryOptions('hasTrackerBinding', {
|
||||||
|
...categoryLibraryOptions.hasTrackerBinding,
|
||||||
|
[tracker.id]: checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
isActive={isTrackerFilterActive}
|
||||||
/>
|
/>
|
||||||
))}
|
)}
|
||||||
<FormLabel sx={{ mt: 2 }}>{t`Tracked`}</FormLabel>
|
{!!librarySources.length && (
|
||||||
{loggedInTrackers.map((tracker) => (
|
<CollapsableFilter
|
||||||
<ThreeStateCheckboxInput
|
title={t`Source`}
|
||||||
key={tracker.id}
|
items={librarySources.map((source) => (
|
||||||
label={tracker.name}
|
|
||||||
checked={categoryLibraryOptions.hasTrackerBinding[tracker.id]}
|
|
||||||
onChange={(checked) =>
|
|
||||||
updateCategoryLibraryOptions('hasTrackerBinding', {
|
|
||||||
...categoryLibraryOptions.hasTrackerBinding,
|
|
||||||
[tracker.id]: checked,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{librarySources.length > 0 && (
|
|
||||||
<>
|
|
||||||
<FormLabel sx={{ mt: 2 }}>{t`Source`}</FormLabel>
|
|
||||||
{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}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user