Implement Unread Filter for Library (#54)
This commit is contained in:
90
src/components/ThreeStateCheckbox.tsx
Normal file
90
src/components/ThreeStateCheckbox.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 { Checkbox, createSvgIcon } from '@mui/material';
|
||||
import React, {
|
||||
useEffect, useState,
|
||||
} from 'react';
|
||||
|
||||
export interface IThreeStateCheckboxProps {
|
||||
name: string
|
||||
checked: boolean | undefined | null
|
||||
onChange: (change: boolean | undefined | null) => void
|
||||
}
|
||||
|
||||
enum CheckState {
|
||||
SELECTED, INTERMEDIATE, UNSELECTED,
|
||||
}
|
||||
|
||||
function checkedToState(checked: boolean | undefined | null): CheckState {
|
||||
switch (checked) {
|
||||
case true:
|
||||
return CheckState.SELECTED;
|
||||
case false:
|
||||
return CheckState.INTERMEDIATE;
|
||||
default:
|
||||
return CheckState.UNSELECTED;
|
||||
}
|
||||
}
|
||||
function stateToChecked(state: CheckState): boolean | undefined {
|
||||
switch (state) {
|
||||
case CheckState.SELECTED:
|
||||
return true;
|
||||
case CheckState.INTERMEDIATE:
|
||||
return false;
|
||||
default:
|
||||
case CheckState.UNSELECTED:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function stateTransition(state: CheckState): CheckState {
|
||||
switch (state) {
|
||||
case CheckState.SELECTED:
|
||||
return CheckState.INTERMEDIATE;
|
||||
case CheckState.INTERMEDIATE:
|
||||
return CheckState.UNSELECTED;
|
||||
case CheckState.UNSELECTED:
|
||||
default:
|
||||
return CheckState.SELECTED;
|
||||
}
|
||||
}
|
||||
|
||||
const ThreeStateCheckbox = (props: IThreeStateCheckboxProps) => {
|
||||
const {
|
||||
name, checked, onChange,
|
||||
} = props;
|
||||
const [localChecked, setLocalChecked] = useState(checkedToState(checked));
|
||||
useEffect(() => setLocalChecked(checkedToState(checked)), [checked]);
|
||||
const handleChange = () => {
|
||||
setLocalChecked(stateTransition(localChecked));
|
||||
if (onChange) {
|
||||
onChange(stateToChecked(stateTransition(localChecked)));
|
||||
}
|
||||
};
|
||||
const CancelBox = createSvgIcon(
|
||||
<>
|
||||
<path
|
||||
d="M 19 6.41 L 13.41 12 L 19 17.59 L 17.59 19 L 12 13.41 L 6.41 19 V 19 H 6.41 L 5 17.59 L 11 12 L 5 6.41 L 6.41 5 L 12 10.59 L 17.59 5 L 19 6.41 M 5 5 m 0 -2 H 5 c -1.1 0 -2 0.9 -2 2 v 14 c 0 1.1 0.9 2 2 2 h 14 c 1.1 0 2 -0.9 2 -2 V 5 c 0 -1.1 -0.9 -2 -2 -2 z "
|
||||
/>
|
||||
</>,
|
||||
'CancelBox',
|
||||
);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
name={name}
|
||||
checked={localChecked === CheckState.SELECTED}
|
||||
indeterminate={localChecked === CheckState.INTERMEDIATE}
|
||||
indeterminateIcon={<CancelBox />}
|
||||
onChange={handleChange}
|
||||
className={`${localChecked}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ThreeStateCheckbox;
|
||||
51
src/components/library/LibraryMangaGrid.tsx
Normal file
51
src/components/library/LibraryMangaGrid.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import MangaGrid, { IMangaGridProps } from '../manga/MangaGrid';
|
||||
import useLibraryOptions, { NullAndUndefined } from '../../util/useLibraryOptions';
|
||||
|
||||
const FILTERED_OUT_MESSAGE = 'There are no Manga matching this filter';
|
||||
|
||||
function unreadFilter(unread: NullAndUndefined<boolean>, { unreadCount }: IMangaCard): boolean {
|
||||
switch (unread) {
|
||||
case true:
|
||||
return !!unreadCount && unreadCount >= 1;
|
||||
case false:
|
||||
return unreadCount === 0;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function filterManga(mangas: IMangaCard[]): IMangaCard[] {
|
||||
const { unread } = useLibraryOptions();
|
||||
return mangas
|
||||
.filter((manga) => unreadFilter(unread, manga));
|
||||
}
|
||||
|
||||
export default function LibraryMangaGrid(props: IMangaGridProps) {
|
||||
const {
|
||||
mangas, isLoading, hasNextPage, lastPageNum, setLastPageNum, message,
|
||||
} = props;
|
||||
|
||||
const { active } = useLibraryOptions();
|
||||
const filteredManga = filterManga(mangas);
|
||||
const showFilteredOutMessage = active && filteredManga.length === 0 && mangas.length > 0;
|
||||
|
||||
return (
|
||||
<MangaGrid
|
||||
mangas={filteredManga}
|
||||
isLoading={isLoading}
|
||||
hasNextPage={hasNextPage}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
message={showFilteredOutMessage ? FILTERED_OUT_MESSAGE : message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
51
src/components/library/LibraryOptions.tsx
Normal file
51
src/components/library/LibraryOptions.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import { Drawer, FormControlLabel, IconButton } from '@mui/material';
|
||||
import useLibraryOptions from '../../util/useLibraryOptions';
|
||||
import ThreeStateCheckbox from '../ThreeStateCheckbox';
|
||||
|
||||
function Options() {
|
||||
const { unread, setUnread } = useLibraryOptions();
|
||||
return (
|
||||
<div>
|
||||
<FormControlLabel control={<ThreeStateCheckbox name="Unread" checked={unread} onChange={setUnread} />} label="Unread" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LibraryOptions() {
|
||||
const [filtersOpen, setFiltersOpen] = React.useState(false);
|
||||
const { active } = useLibraryOptions();
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => setFiltersOpen(!filtersOpen)}
|
||||
color={active ? 'warning' : 'default'}
|
||||
>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
|
||||
<Drawer
|
||||
anchor="bottom"
|
||||
open={filtersOpen}
|
||||
onClose={() => setFiltersOpen(false)}
|
||||
PaperProps={{
|
||||
style: {
|
||||
maxWidth: 600, padding: '1em', marginLeft: 'auto', marginRight: 'auto',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Options />
|
||||
</Drawer>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import EmptyView from 'components/EmptyView';
|
||||
import LoadingPlaceholder from 'components/LoadingPlaceholder';
|
||||
import MangaCard from './MangaCard';
|
||||
|
||||
interface IProps{
|
||||
export interface IMangaGridProps{
|
||||
mangas: IMangaCard[]
|
||||
isLoading: boolean
|
||||
message?: string
|
||||
@@ -21,7 +21,7 @@ interface IProps{
|
||||
setLastPageNum: (lastPageNum: number) => void
|
||||
}
|
||||
|
||||
export default function MangaGrid(props: IProps) {
|
||||
export default function MangaGrid(props: IMangaGridProps) {
|
||||
const {
|
||||
mangas, isLoading, message, messageExtra, hasNextPage, lastPageNum, setLastPageNum,
|
||||
} = props;
|
||||
|
||||
Reference in New Issue
Block a user