Options panels refactoring (#196)

* Refactor library options to match chapter options using shared component

* Disable some eslint rules

* Cleanup SourceOptions layout to use same components as other panels
This commit is contained in:
Valter Martinek
2022-11-24 09:41:03 +01:00
committed by GitHub
parent 3972d7757f
commit 154b357c40
35 changed files with 625 additions and 685 deletions

View File

@@ -22,5 +22,7 @@ module.exports = {
// just why // just why
'react/jsx-no-bind' : 'off', 'react/jsx-no-bind' : 'off',
'react/jsx-props-no-spreading': 'off',
'react/require-default-props': 'off',
}, },
}; };

View File

@@ -0,0 +1,28 @@
/*
* 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, CheckboxProps, FormControlLabel } from '@mui/material';
import React from 'react';
interface IProps extends CheckboxProps {
label?: string
}
const CheckboxInput: React.FC<IProps> = ({
label, sx, ...rest
}) => (
<FormControlLabel
control={(
<Checkbox {...rest} />
)}
label={label}
sx={sx}
/>
);
export default CheckboxInput;

View File

@@ -18,7 +18,6 @@ const LoadingIconButton = ({
}; };
return ( return (
// eslint-disable-next-line react/jsx-props-no-spreading
<IconButton disabled={loading} {...rest} onClick={handleClick}> <IconButton disabled={loading} {...rest} onClick={handleClick}>
{loading ? (<CircularProgress size={24} />) : children} {loading ? (<CircularProgress size={24} />) : children}
</IconButton> </IconButton>

View File

@@ -0,0 +1,28 @@
/*
* 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 { FormControlLabel, Radio, RadioProps } from '@mui/material';
import React from 'react';
export interface RadioInputProps extends RadioProps {
label?: string
}
const RadioInput: React.FC<RadioInputProps> = ({
label, sx, ...rest
}) => (
<FormControlLabel
control={(
<Radio {...rest} />
)}
label={label}
sx={sx}
/>
);
export default RadioInput;

View File

@@ -0,0 +1,27 @@
/*
* 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 ArrowDownward from '@mui/icons-material/ArrowDownward';
import ArrowUpward from '@mui/icons-material/ArrowUpward';
import React from 'react';
import RadioInput, { RadioInputProps } from './RadioInput';
interface IProps extends RadioInputProps {
sortDescending?: boolean | null | undefined
}
const SortRadioInput: React.FC<IProps> = ({
sortDescending, ...rest
}) => (
<RadioInput
checkedIcon={sortDescending ? <ArrowDownward color="primary" /> : <ArrowUpward color="primary" />}
{...rest}
/>
);
export default SortRadioInput;

View File

@@ -0,0 +1,49 @@
/*
* 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 { DisabledByDefaultRounded } from '@mui/icons-material';
import { Checkbox, CheckboxProps } from '@mui/material';
import React, { useCallback } from 'react';
type CheckState = boolean | undefined | null;
function nextState(state: CheckState): CheckState {
if (state === true) return false;
if (state === false) return undefined;
return true;
}
export interface ThreeStateCheckboxProps extends Omit<CheckboxProps, 'checked' | 'onChange'> {
checked?: boolean | undefined | null
onChange?: (checked: boolean | undefined | null) => void
}
/**
* When checked is true, checkbox contains checkmark
* When checked is false, checkbox contains cross
* When checked is null or undefined, checkbox is empty
*/
const ThreeStateCheckbox: React.FC<ThreeStateCheckboxProps> = ({ checked, onChange, ...rest }) => {
const handleChange = useCallback(() => {
if (onChange) {
const newState = nextState(checked);
onChange(newState);
}
}, [onChange]);
return (
<Checkbox
indeterminateIcon={<DisabledByDefaultRounded />}
checked={checked === true}
indeterminate={checked === false}
onChange={handleChange}
{...rest}
/>
);
};
export default ThreeStateCheckbox;

View File

@@ -0,0 +1,29 @@
/*
* 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 { FormControlLabel } from '@mui/material';
import React from 'react';
import ThreeStateCheckbox, { ThreeStateCheckboxProps } from './ThreeStateCheckbox';
interface IProps extends ThreeStateCheckboxProps {
label?: string
}
const ThreeStateCheckboxInput: React.FC<IProps> = ({
label, sx, ...rest
}) => (
<FormControlLabel
control={(
<ThreeStateCheckbox {...rest} />
)}
label={label}
sx={sx}
/>
);
export default ThreeStateCheckboxInput;

View File

@@ -9,16 +9,7 @@ import React, { useContext } from 'react';
type ContextType = { type ContextType = {
options: LibraryOptions; options: LibraryOptions;
setOption: <Name extends keyof LibraryOptions>(
name: Name,
value: React.SetStateAction<LibraryOptions[Name]>
) => void;
setOptions: React.Dispatch<React.SetStateAction<LibraryOptions>>; setOptions: React.Dispatch<React.SetStateAction<LibraryOptions>>;
active: boolean
activeSort: boolean
}; };
export const DefaultLibraryOptions: LibraryOptions = { export const DefaultLibraryOptions: LibraryOptions = {
@@ -35,10 +26,7 @@ export const DefaultLibraryOptions: LibraryOptions = {
const LibraryOptionsContext = React.createContext<ContextType>({ const LibraryOptionsContext = React.createContext<ContextType>({
options: DefaultLibraryOptions, options: DefaultLibraryOptions,
setOption: () => {},
setOptions: () => {}, setOptions: () => {},
active: false,
activeSort: false,
}); });
export default LibraryOptionsContext; export default LibraryOptionsContext;

View File

@@ -83,7 +83,8 @@ export default function LibraryMangaGrid(props: IMangaGridProps) {
} = props; } = props;
const [query] = useQueryParam('query', StringParam); const [query] = useQueryParam('query', StringParam);
const { options, active } = useLibraryOptionsContext(); const { options } = useLibraryOptionsContext();
const { unread, downloaded } = options;
const filteredManga = filterManga(mangas); const filteredManga = filterManga(mangas);
const sortedManga = sortManga(filteredManga); const sortedManga = sortManga(filteredManga);
const DoneManga = sortedManga.map((ele) => { const DoneManga = sortedManga.map((ele) => {
@@ -91,7 +92,7 @@ export default function LibraryMangaGrid(props: IMangaGridProps) {
ele.inLibrary = undefined; ele.inLibrary = undefined;
return ele; return ele;
}); });
const showFilteredOutMessage = (active || query) const showFilteredOutMessage = (unread != null || downloaded != null || query)
&& filteredManga.length === 0 && mangas.length > 0; && filteredManga.length === 0 && mangas.length > 0;
return ( return (

View File

@@ -1,232 +0,0 @@
/*
* 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, { useState } from 'react';
import FilterListIcon from '@mui/icons-material/FilterList';
import {
Drawer,
FormControlLabel,
IconButton,
Tabs,
Tab,
Box,
Stack,
Checkbox,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Radio,
} from '@mui/material';
import ThreeStateCheckbox from 'components/util/ThreeStateCheckbox';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import TabPanel from 'components/util/TabPanel';
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
function filtersTab(currentTab: number) {
const { options: { unread, downloaded }, setOption } = useLibraryOptionsContext();
return (
<TabPanel index={0} currentIndex={currentTab}>
<Stack direction="column">
<FormControlLabel
control={(
<ThreeStateCheckbox
name="Unread"
checked={unread}
onChange={(change) => setOption('unread', change)}
/>
)}
label="Unread"
/>
<FormControlLabel
control={(
<ThreeStateCheckbox
name="Downloaded"
checked={downloaded}
onChange={(change) => setOption('downloaded', change)}
/>
)}
label="Downloaded"
/>
</Stack>
</TabPanel>
);
}
function sortsTab(currentTab: number) {
const { options: { sorts, sortDesc }, setOption } = useLibraryOptionsContext();
const handleChange = (event: React.MouseEvent<HTMLDivElement, MouseEvent>, index: string) => {
if (sorts === index) {
setOption('sortDesc', (sortDes) => !sortDes);
} else {
setOption('sortDesc', false);
}
setOption('sorts', index);
};
return (
<>
<TabPanel index={1} currentIndex={currentTab}>
<Stack direction="column">
{
['sortToRead', 'sortAlph', 'sortID'].map((e) => {
let icon;
if (sorts === e) {
icon = !sortDesc ? (<ArrowUpwardIcon color="primary" />)
: (<ArrowDownwardIcon color="primary" />);
}
icon = icon === undefined && sortDesc === undefined && e === 'sortID' ? (<ArrowDownwardIcon color="primary" />) : icon;
return (
<ListItem disablePadding>
<ListItemButton onClick={(event) => handleChange(event, e)}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText primary={e} />
</ListItemButton>
</ListItem>
);
})
}
</Stack>
</TabPanel>
</>
);
}
function dispalyTab(currentTab: number) {
const { options, setOptions } = useLibraryOptionsContext();
function setContextOptions(
e: React.ChangeEvent<HTMLInputElement>,
checked: boolean,
) {
setOptions((prev) => ({ ...prev, [e.target.name]: checked }));
}
function setGridContextOptions(
e: React.ChangeEvent<HTMLInputElement>,
checked: boolean,
) {
if (checked) {
setOptions((prev) => ({ ...prev, gridLayout: parseInt(e.target.name, 10) }));
}
}
return (
<TabPanel index={2} currentIndex={currentTab}>
<Stack direction="column">
DISPLAY MODE
<FormControlLabel
label="Compact grid"
control={(
<Radio
name="0"
checked={options.gridLayout === 0 || options.gridLayout === undefined}
onChange={setGridContextOptions}
/>
)}
/>
<FormControlLabel
label="Comfortable grid"
control={(
<Radio
name="1"
checked={options.gridLayout === 1}
onChange={setGridContextOptions}
/>
)}
/>
<FormControlLabel
label="list"
control={(
<Radio
name="2"
checked={options.gridLayout === 2}
onChange={setGridContextOptions}
/>
)}
/>
BADGES
<FormControlLabel
label="Unread Badges"
control={(
<Checkbox
name="showUnreadBadge"
checked={options.showUnreadBadge}
onChange={setContextOptions}
/>
)}
/>
<FormControlLabel
label="Download Badges"
control={(
<Checkbox
name="showDownloadBadge"
checked={options.showDownloadBadge}
onChange={setContextOptions}
/>
)}
/>
</Stack>
</TabPanel>
);
}
function Options() {
const [currentTab, setCurrentTab] = useState<number>(0);
return (
<Box>
<Tabs
key={currentTab}
value={currentTab}
variant="fullWidth"
onChange={(e, newTab) => setCurrentTab(newTab)}
indicatorColor="primary"
textColor="primary"
>
<Tab label="Filter" value={0} />
<Tab label="Sort" value={1} />
<Tab label="Display" value={2} />
</Tabs>
{filtersTab(currentTab)}
{sortsTab(currentTab)}
{dispalyTab(currentTab)}
</Box>
);
}
export default function LibraryOptions() {
const [filtersOpen, setFiltersOpen] = React.useState(false);
const { active } = useLibraryOptionsContext();
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>
</>
);
}

View File

@@ -0,0 +1,106 @@
/*
* 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 { FormLabel, RadioGroup } from '@mui/material';
import CheckboxInput from 'components/atoms/CheckboxInput';
import RadioInput from 'components/atoms/RadioInput';
import SortRadioInput from 'components/atoms/SortRadioInput';
import ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
import OptionsTabs from 'components/molecules/OptionsTabs';
import React from 'react';
const TITLES = {
filter: 'Filter',
sort: 'Sort',
display: 'Display',
};
const SORT_OPTIONS: [LibrarySortMode, string][] = [
['sortToRead', 'By Unread chapters'],
['sortAlph', 'Alphabetically'],
['sortID', 'By ID'],
];
interface IProps {
open: boolean,
onClose: () => void,
}
const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
const { options, setOptions } = useLibraryOptionsContext();
const handleFilterChange = <T extends keyof LibraryOptions>(
key: T,
value: LibraryOptions[T],
) => {
setOptions((v) => ({ ...v, [key]: value }));
};
return (
<OptionsTabs<'filter' | 'sort' | 'display'>
open={open}
onClose={onClose}
tabs={['filter', 'sort', 'display']}
tabTitle={(key) => TITLES[key]}
tabContent={(key) => {
if (key === 'filter') {
return (
<>
<ThreeStateCheckboxInput label="Unread" checked={options.unread} onChange={(c) => handleFilterChange('unread', c)} />
<ThreeStateCheckboxInput label="Downloaded" checked={options.downloaded} onChange={(c) => handleFilterChange('downloaded', c)} />
</>
);
}
if (key === 'sort') {
return SORT_OPTIONS.map(([mode, label]) => (
<SortRadioInput
key={mode}
label={label}
checked={options.sorts === mode}
sortDescending={options.sortDesc}
onClick={() => (mode !== options.sorts
? handleFilterChange('sorts', mode)
: handleFilterChange('sortDesc', !options.sortDesc))}
/>
));
}
if (key === 'display') {
const { gridLayout, showDownloadBadge, showUnreadBadge } = options;
return (
<>
<FormLabel>Display mode</FormLabel>
<RadioGroup
onChange={(e) => handleFilterChange('gridLayout', Number(e.target.value))}
value={gridLayout}
>
<RadioInput label="Compact grid" value={0} checked={gridLayout == null || gridLayout === 0} />
<RadioInput label="Comfortable grid" value={1} checked={gridLayout === 1} />
<RadioInput label="List" value={2} checked={gridLayout === 2} />
</RadioGroup>
<FormLabel sx={{ mt: 2 }}>Badges</FormLabel>
<CheckboxInput
label="Unread Badges"
checked={showUnreadBadge === true}
onChange={() => handleFilterChange('showUnreadBadge', !showUnreadBadge)}
/>
<CheckboxInput
label="Download Badges"
checked={showDownloadBadge === true}
onChange={() => handleFilterChange('showDownloadBadge', !showDownloadBadge)}
/>
</>
);
}
return null;
}}
/>
);
};
export default LibraryOptionsPanel;

View File

@@ -5,44 +5,22 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
import LibraryOptionsContext, { DefaultLibraryOptions } from 'components/context/LibraryOptionsContext'; import LibraryOptionsContext, { DefaultLibraryOptions } from 'components/context/LibraryOptionsContext';
import React from 'react';
import useLocalStorage from 'util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
interface IProps { interface IProps {
children: React.ReactNode; children: React.ReactNode;
} }
export default function LibraryOptionsContextProvider({ children }: IProps) { const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => {
const [options, setOptions] = useLocalStorage<LibraryOptions>('libraryOptions', DefaultLibraryOptions); const [options, setOptions] = useLocalStorage<LibraryOptions>('libraryOptions', DefaultLibraryOptions);
function setOption<Name extends keyof LibraryOptions>(
option: Name,
value: React.SetStateAction<LibraryOptions[Name]>,
) {
setOptions((opts) => ({
...opts,
[option]: typeof value === 'function' ? value(opts[option]) : value,
}));
}
// TODO remove these fields when we have a better way to handle them
// eslint-disable-next-line eqeqeq
const active = !(options.unread == undefined) || !(options.downloaded == undefined);
// eslint-disable-next-line eqeqeq
const activeSort = (options.sortDesc != undefined) || (options.sorts != undefined);
return ( return (
<LibraryOptionsContext.Provider <LibraryOptionsContext.Provider value={{ options, setOptions }}>
value={{
options,
setOption,
setOptions,
active,
activeSort,
}}
>
{children} {children}
</LibraryOptionsContext.Provider> </LibraryOptionsContext.Provider>
); );
} };
export default LibraryOptionsContextProvider;

View File

@@ -0,0 +1,32 @@
/*
* 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 FilterList from '@mui/icons-material/FilterList';
import { IconButton } from '@mui/material';
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
import React, { useState } from 'react';
import LibraryOptionsPanel from './LibraryOptionsPanel';
const LibraryToolbarMenu: React.FC = () => {
const [open, setOpen] = useState(false);
const { options } = useLibraryOptionsContext();
const active = options.downloaded != null || options.unread != null;
return (
<>
<IconButton
onClick={() => setOpen(!open)}
color={active ? 'warning' : 'default'}
>
<FilterList />
</IconButton>
<LibraryOptionsPanel open={open} onClose={() => setOpen(false)} />
</>
);
};
export default LibraryToolbarMenu;

View File

@@ -239,7 +239,6 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
totalCount={visibleChapters.length} totalCount={visibleChapters.length}
itemContent={(index:number) => ( itemContent={(index:number) => (
<ChapterCard <ChapterCard
// eslint-disable-next-line react/jsx-props-no-spreading
{...chaptersWithMeta[index]} {...chaptersWithMeta[index]}
showChapterNumber={options.showChapterNumber} showChapterNumber={options.showChapterNumber}
triggerChaptersUpdate={() => mutate()} triggerChaptersUpdate={() => mutate()}

View File

@@ -5,110 +5,70 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { ArrowDownward, ArrowUpward } from '@mui/icons-material'; import { RadioGroup } from '@mui/material';
import { import RadioInput from 'components/atoms/RadioInput';
Drawer, FormControlLabel, Radio, RadioGroup, Tab, Tabs, import SortRadioInput from 'components/atoms/SortRadioInput';
} from '@mui/material'; import ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
import { Box } from '@mui/system'; import OptionsTabs from 'components/molecules/OptionsTabs';
import TabPanel from 'components/util/TabPanel'; import React from 'react';
import ThreeStateCheckbox from 'components/util/ThreeStateCheckbox';
import React, { useCallback, useState } from 'react';
import { SORT_OPTIONS } from './util'; import { SORT_OPTIONS } from './util';
interface IProps{ interface IProps {
open: boolean open: boolean
onClose: () => void; onClose: () => void
options: ChapterListOptions options: ChapterListOptions
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction> optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>
} }
const TabContent: React.FC<{ children: React.ReactNode }> = ({ children }) => ( const TITLES = {
<Box sx={{ filter: 'Filter',
px: 3, py: 1, display: 'flex', flexDirection: 'column', minHeight: 150, sort: 'Sort',
}} display: 'Display',
> };
{children}
</Box>
);
const ChapterOptions: React.FC<IProps> = ({ const ChapterOptions: React.FC<IProps> = ({
open, onClose, options, optionsDispatch, open, onClose, options, optionsDispatch,
}) => { }) => (
const [tabNum, setTabNum] = useState(0); <OptionsTabs<'filter' | 'sort' | 'display'>
open={open}
const handleFilterChange = useCallback( onClose={onClose}
(value: NullAndUndefined<boolean>, name: string) => { minHeight={150}
optionsDispatch({ type: 'filter', filterType: name.toLowerCase(), filterValue: value }); tabs={['filter', 'sort', 'display']}
}, [], tabTitle={(key) => TITLES[key]}
); tabContent={(key) => {
if (key === 'filter') {
return ( return (
<> <>
<Drawer <ThreeStateCheckboxInput label="Unread" checked={options.unread} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'unread', filterValue: c })} />
anchor="bottom" <ThreeStateCheckboxInput label="Downloaded" checked={options.downloaded} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'downloaded', filterValue: c })} />
open={open} <ThreeStateCheckboxInput label="Bookmarked" checked={options.bookmarked} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'bookmarked', filterValue: c })} />
onClose={onClose} </>
PaperProps={{ );
style: { }
maxWidth: 600, if (key === 'sort') {
marginLeft: 'auto', return SORT_OPTIONS.map(([mode, label]) => (
marginRight: 'auto', <SortRadioInput
minHeight: '150px', key={mode}
}, label={label}
}} checked={options.sortBy === mode}
> sortDescending={options.reverse}
<Box> onClick={() => (mode !== options.sortBy
<Tabs ? optionsDispatch({ type: 'sortBy', sortBy: mode })
value={tabNum} : optionsDispatch({ type: 'sortReverse' }))}
variant="fullWidth" />
onChange={(e, newTab) => setTabNum(newTab)} ));
indicatorColor="primary" }
textColor="primary" if (key === 'display') {
> return (
<Tab value={0} label="Filter" /> <RadioGroup onChange={() => optionsDispatch({ type: 'showChapterNumber' })} value={options.showChapterNumber}>
<Tab value={1} label="Sort" /> <RadioInput label="Source Title" value={false} />
<Tab value={2} label="Display" /> <RadioInput label="Chapter Number" value />
</Tabs> </RadioGroup>
<TabPanel index={0} currentIndex={tabNum}> );
<TabContent> }
<FormControlLabel control={<ThreeStateCheckbox name="Unread" checked={options.unread} onChange={handleFilterChange} />} label="Unread" /> return null;
<FormControlLabel control={<ThreeStateCheckbox name="Downloaded" checked={options.downloaded} onChange={handleFilterChange} />} label="Downloaded" /> }}
<FormControlLabel control={<ThreeStateCheckbox name="Bookmarked" checked={options.bookmarked} onChange={handleFilterChange} />} label="Bookmarked" /> />
</TabContent> );
</TabPanel>
<TabPanel index={1} currentIndex={tabNum}>
<TabContent>
{
SORT_OPTIONS.map(([mode, label]) => (
<FormControlLabel
key={mode}
control={(
<Radio
checked={options.sortBy === mode}
checkedIcon={options.reverse ? <ArrowUpward color="primary" /> : <ArrowDownward color="primary" />}
onClick={() => (mode !== options.sortBy
? optionsDispatch({ type: 'sortBy', sortBy: mode })
: optionsDispatch({ type: 'sortReverse' }))}
/>
)}
label={label}
/>
))
}
</TabContent>
</TabPanel>
<TabPanel index={2} currentIndex={tabNum}>
<TabContent>
<RadioGroup onChange={() => optionsDispatch({ type: 'showChapterNumber' })} value={options.showChapterNumber}>
<FormControlLabel label="Source Title" value="title" control={<Radio checked={!options.showChapterNumber} />} />
<FormControlLabel label="Chapter Number" value="chapterNumber" control={<Radio checked={options.showChapterNumber} />} />
</RadioGroup>
</TabContent>
</TabPanel>
</Box>
</Drawer>
</>
);
};
export default ChapterOptions; export default ChapterOptions;

View File

@@ -0,0 +1,47 @@
/*
* 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 {
Drawer,
} from '@mui/material';
import { Box } from '@mui/system';
import React from 'react';
interface IProps {
open: boolean
onClose: () => void
children: React.ReactNode
minHeight?: number
}
const OptionsPanel: React.FC<IProps> = ({
open, onClose, children, minHeight,
}) => (
<Drawer
anchor="bottom"
open={open}
onClose={onClose}
PaperProps={{
style: {
maxWidth: 600,
marginLeft: 'auto',
marginRight: 'auto',
minHeight,
},
}}
>
<Box>
{children}
</Box>
</Drawer>
);
OptionsPanel.defaultProps = {
minHeight: undefined,
};
export default OptionsPanel;

View File

@@ -0,0 +1,59 @@
/*
* 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 { Stack, Tab, Tabs } from '@mui/material';
import TabPanel from 'components/util/TabPanel';
import React, { useState } from 'react';
import OptionsPanel from './OptionsPanel';
interface IProps<T = string>{
open: boolean
onClose: () => void
tabs: T[]
tabTitle: (key: T) => React.ReactNode
tabContent: (key: T) => React.ReactNode
minHeight?: number
}
const OptionsTabs = <T extends string = string>({
open, onClose, tabs, tabTitle, tabContent, minHeight,
}: IProps<T>) => {
const [tabNum, setTabNum] = useState(0);
return (
<OptionsPanel
open={open}
onClose={onClose}
minHeight={minHeight}
>
<Tabs
value={tabNum}
variant="fullWidth"
onChange={(e, newTab) => setTabNum(newTab)}
indicatorColor="primary"
textColor="primary"
>
{tabs.map((tab, tabIndex) => (
<Tab key={tab} value={tabIndex} label={tabTitle(tab)} />
))}
</Tabs>
{tabs.map((tab, tabIndex) => (
<TabPanel key={tab} index={tabIndex} currentIndex={tabNum}>
<Stack sx={{ px: 3, py: 1, minHeight }}>
{tabContent(tab)}
</Stack>
</TabPanel>
))}
</OptionsPanel>
);
};
OptionsTabs.defaultProps = {
minHeight: undefined,
};
export default OptionsTabs;

View File

@@ -6,22 +6,21 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import React from 'react';
import FilterListIcon from '@mui/icons-material/FilterList'; import FilterListIcon from '@mui/icons-material/FilterList';
import { import { Button, Fab, Stack } from '@mui/material';
Drawer, Button, Fab,
} from '@mui/material';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
import SelectFilter from './filters/SelectFilter'; import OptionsPanel from 'components/molecules/OptionsPanel';
import React from 'react';
import CheckBoxFilter from './filters/CheckBoxFilter'; import CheckBoxFilter from './filters/CheckBoxFilter';
import HeaderFilter from './filters/HeaderFilter'; import HeaderFilter from './filters/HeaderFilter';
import SeperatorFilter from './filters/SeparatorFilter'; import SelectFilter from './filters/SelectFilter';
import SortFilter from './filters/SortFilter'; import SortFilter from './filters/SortFilter';
import TextFilter from './filters/TextFilter'; import TextFilter from './filters/TextFilter';
import TriStateFilter from './filters/TriStateFilter'; import TriStateFilter from './filters/TriStateFilter';
// this can only cycle once, so should be fine // this can only cycle once, so should be fine
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import GroupFilter from './filters/GroupFilter'; import GroupFilter from './filters/GroupFilter';
import SeperatorFilter from './filters/SeparatorFilter';
interface IFilters { interface IFilters {
sourceFilter: ISourceFilters[] sourceFilter: ISourceFilters[]
@@ -46,7 +45,7 @@ export function Options({
update, update,
}: IFilters) { }: IFilters) {
return ( return (
<Box key={`filters ${group}`}> <Stack key={`filters ${group}`}>
{ sourceFilter.map((e: ISourceFilters, index) => { { sourceFilter.map((e: ISourceFilters, index) => {
let checkif = update.find((el: { let checkif = update.find((el: {
group: number | undefined; position: number; group: number | undefined; position: number;
@@ -145,7 +144,7 @@ export function Options({
return (<Box key={`${e.filter.name}null`} />); return (<Box key={`${e.filter.name}null`} />);
} }
})} })}
</Box> </Stack>
); );
} }
@@ -182,17 +181,11 @@ export default function SourceOptions({
Filter Filter
</Fab> </Fab>
<Drawer <OptionsPanel
anchor="bottom"
open={FilterOptions} open={FilterOptions}
onClose={() => setFilterOptions(false)} onClose={() => setFilterOptions(false)}
PaperProps={{
style: {
maxWidth: 600, padding: '1em', marginLeft: 'auto', marginRight: 'auto',
},
}}
> >
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex', p: 2, pb: 0 }}>
<Button <Button
onClick={handleReset} onClick={handleReset}
> >
@@ -206,13 +199,20 @@ export default function SourceOptions({
Submit Submit
</Button> </Button>
</Box> </Box>
<Options <Box
sourceFilter={sourceFilter} sx={{
updateFilterValue={updateFilterValue} pb: 2,
group={undefined} mx: 2,
update={update} }}
/> >
</Drawer> <Options
sourceFilter={sourceFilter}
updateFilterValue={updateFilterValue}
group={undefined}
update={update}
/>
</Box>
</OptionsPanel>
</> </>
); );
} }

View File

@@ -5,9 +5,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import CheckboxInput from 'components/atoms/CheckboxInput';
import React from 'react'; import React from 'react';
import { Box } from '@mui/system';
import { Checkbox, FormControlLabel } from '@mui/material';
interface Props { interface Props {
state: boolean state: boolean
@@ -18,7 +17,7 @@ interface Props {
update: any update: any
} }
export default function CheckBoxFilter(props: Props) { const CheckBoxFilter: React.FC<Props> = (props: Props) => {
const { const {
state, state,
name, name,
@@ -39,20 +38,10 @@ export default function CheckBoxFilter(props: Props) {
if (state !== undefined) { if (state !== undefined) {
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}> <CheckboxInput label={name} checked={val} onChange={handleChange} />
<FormControlLabel
key={name}
control={(
<Checkbox
name={name}
checked={val}
onChange={handleChange}
/>
)}
label={name}
/>
</Box>
); );
} }
return (<></>); return null;
} };
export default CheckBoxFilter;

View File

@@ -5,11 +5,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import {
Collapse, List, ListItemButton, ListItemText,
} from '@mui/material';
import React from 'react';
import { ExpandLess, ExpandMore } from '@mui/icons-material'; import { ExpandLess, ExpandMore } from '@mui/icons-material';
import {
Collapse, ListItemButton, ListItemText, Stack,
} from '@mui/material';
import { Box } from '@mui/system';
import React from 'react';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { Options } from '../SourceOptions'; import { Options } from '../SourceOptions';
@@ -21,7 +22,7 @@ interface Props {
update: any update: any
} }
export default function GroupFilter(props: Props) { const GroupFilter: React.FC<Props> = (props: Props) => {
const { const {
state, state,
name, name,
@@ -32,26 +33,25 @@ export default function GroupFilter(props: Props) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const handleClick = () => {
setOpen(!open);
};
return ( return (
<> <Box sx={{ mx: -2 }}>
<ListItemButton onClick={handleClick}> <ListItemButton onClick={() => setOpen(!open)}>
<ListItemText primary={name} /> <ListItemText primary={name} />
{open ? <ExpandLess /> : <ExpandMore />} {open ? <ExpandLess /> : <ExpandMore />}
</ListItemButton> </ListItemButton>
<Collapse in={open}> <Collapse in={open}>
<List disablePadding> {/* Container is moved outside 2, so content has to go inside 4 */}
<Stack sx={{ mx: 4 }}>
<Options <Options
sourceFilter={state} sourceFilter={state}
group={position} group={position}
updateFilterValue={updateFilterValue} updateFilterValue={updateFilterValue}
update={update} update={update}
/> />
</List> </Stack>
</Collapse> </Collapse>
</> </Box>
); );
} };
export default GroupFilter;

View File

@@ -6,14 +6,13 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { Typography } from '@mui/material';
import React from 'react'; import React from 'react';
import { Box } from '@mui/system';
interface Props { interface Props {
name: string name: string
} }
export default function HeaderFilter(props: Props) { const HeaderFilter: React.FC<Props> = ({ name }) => (<Typography key={name} sx={{ mt: 2 }} variant="subtitle2">{name}</Typography>);
const { name } = props;
return (<Box key={name}>{name}</Box>); export default HeaderFilter;
}

View File

@@ -7,7 +7,6 @@
*/ */
import React from 'react'; import React from 'react';
import { Box } from '@mui/system';
import InputLabel from '@mui/material/InputLabel'; import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl'; import FormControl from '@mui/material/FormControl';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
@@ -61,26 +60,22 @@ function hasSelect(
</MenuItem> </MenuItem>
)); ));
return ( return (
<Box key={name} sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}> <FormControl sx={{ my: 1 }} variant="standard">
<FormControl fullWidth> <InputLabel>
<InputLabel sx={{ margin: '10px 0 10px 0' }}> {name}
{name} </InputLabel>
</InputLabel> <Select
<Select name={name}
name={name} value={values[val].displayname}
value={values[val].displayname} label={name}
label={name} onChange={handleChange}
onChange={handleChange} >
autoWidth {rett}
sx={{ margin: '10px 0 10px 0' }} </Select>
> </FormControl>
{rett}
</Select>
</FormControl>
</Box>
); );
} }
return (<></>); return null;
} }
function noSelect( function noSelect(
@@ -106,29 +101,25 @@ function noSelect(
const rett = values.map((value: string) => (<MenuItem key={`${name} ${value}`} value={value}>{value}</MenuItem>)); const rett = values.map((value: string) => (<MenuItem key={`${name} ${value}`} value={value}>{value}</MenuItem>));
return ( return (
<Box key={name} sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}> <FormControl sx={{ my: 1 }} variant="standard">
<FormControl fullWidth> <InputLabel>
<InputLabel sx={{ margin: '10px 0 10px 0' }}> {name}
{name} </InputLabel>
</InputLabel> <Select
<Select name={name}
name={name} value={values[val]}
value={values[val]} label={name}
label={name} onChange={handleChange}
onChange={handleChange} >
autoWidth {rett}
sx={{ margin: '10px 0 10px 0' }} </Select>
> </FormControl>
{rett}
</Select>
</FormControl>
</Box>
); );
} }
return (<></>); return null;
} }
export default function SelectFilter({ const SelectFilter: React.FC<Props> = ({
values, values,
name, name,
state, state,
@@ -137,7 +128,7 @@ export default function SelectFilter({
updateFilterValue, updateFilterValue,
update, update,
group, group,
}: Props) { }) => {
if (selected === undefined) { if (selected === undefined) {
return noSelect( return noSelect(
values, values,
@@ -159,4 +150,6 @@ export default function SelectFilter({
update, update,
group, group,
); );
} };
export default SelectFilter;

View File

@@ -5,14 +5,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { Divider } from '@mui/material';
import React from 'react'; import React from 'react';
import { Box } from '@mui/system';
interface Props { interface Props {
name: string name: string
} }
export default function SeperatorFilter(props: Props) { const SeparatorFilter: React.FC<Props> = ({ name }) => (<Divider key={name} sx={{ my: 1 }} textAlign="center">{name}</Divider>);
const { name } = props;
return (<Box key={name}>{name}</Box>); export default SeparatorFilter;
}

View File

@@ -5,18 +5,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import React from 'react';
import { Box } from '@mui/system';
import FormControl from '@mui/material/FormControl';
import {
Collapse,
List,
ListItem,
ListItemButton, ListItemIcon, ListItemText,
} from '@mui/material';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import { ExpandLess, ExpandMore } from '@mui/icons-material'; import { ExpandLess, ExpandMore } from '@mui/icons-material';
import {
Collapse, ListItemButton, ListItemText, Stack,
} from '@mui/material';
import { Box } from '@mui/system';
import SortRadioInput from 'components/atoms/SortRadioInput';
import React from 'react';
interface Props { interface Props {
values: any values: any
@@ -28,7 +23,7 @@ interface Props {
update: any update: any
} }
export default function SortFilter(props: Props) { const SortFilter: React.FC<Props> = (props: Props) => {
const { const {
values, values,
name, name,
@@ -47,8 +42,7 @@ export default function SortFilter(props: Props) {
}; };
if (values) { if (values) {
const handleChange = (event: const handleChange = (index: number) => {
React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => {
const tmp = val; const tmp = val;
if (tmp.index === index) { if (tmp.index === index) {
tmp.ascending = !tmp.ascending; tmp.ascending = !tmp.ascending;
@@ -63,42 +57,29 @@ export default function SortFilter(props: Props) {
updateFilterValue([...upd, { position, state: JSON.stringify(tmp), group }]); updateFilterValue([...upd, { position, state: JSON.stringify(tmp), group }]);
}; };
const ret = ( return (
<FormControl fullWidth> <Box sx={{ mx: -2 }}>
<ListItemButton onClick={handleClick}> <ListItemButton onClick={handleClick}>
<ListItemText primary={name} /> <ListItemText primary={name} />
{open ? <ExpandLess /> : <ExpandMore />} {open ? <ExpandLess /> : <ExpandMore />}
</ListItemButton> </ListItemButton>
<Collapse in={open}> <Collapse in={open}>
<List> <Stack sx={{ mx: 4 }}>
{values.map((value: string, index: number) => { {values.map((value: string, index: number) => (
let icon; <SortRadioInput
if (val.index === index) { key={`${name} ${value}`}
icon = val.ascending ? (<ArrowUpwardIcon color="primary" />) label={value}
: (<ArrowDownwardIcon color="primary" />); checked={val.index === index}
} sortDescending={!val.ascending}
return ( onClick={() => handleChange(index)}
<ListItem disablePadding key={`${name} ${value}`}> />
<ListItemButton ))}
onClick={(event) => handleChange(event, index)} </Stack>
>
<ListItemIcon>
{icon}
</ListItemIcon>
<ListItemText primary={value} />
</ListItemButton>
</ListItem>
);
})}
</List>
</Collapse> </Collapse>
</FormControl>
);
return (
<Box key={name} sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}>
{ret}
</Box> </Box>
); );
} }
return (<></>); return null;
} };
export default SortFilter;

View File

@@ -5,14 +5,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import React from 'react';
import { Box } from '@mui/system';
import {
Input,
InputLabel,
FormControl,
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import {
FormControl, Input, InputAdornment, InputLabel,
} from '@mui/material';
import React from 'react';
interface Props { interface Props {
state: string state: string
@@ -23,7 +20,7 @@ interface Props {
update: any update: any
} }
export default function TextFilter(props: Props) { const TextFilter: React.FC<Props> = (props) => {
const { const {
state, state,
name, name,
@@ -51,27 +48,24 @@ export default function TextFilter(props: Props) {
if (state !== undefined) { if (state !== undefined) {
return ( return (
<Box key={`${name}`} sx={{ display: 'flex', flexDirection: 'row', minWidth: 120 }}> <FormControl sx={{ my: 1 }} variant="standard">
<> <InputLabel>
<SearchIcon {name}
sx={{ </InputLabel>
margin: 'auto', <Input
}} name={name}
/> value={Search || ''}
<FormControl fullWidth> onChange={handleChange}
<InputLabel sx={{ margin: '10px 0 10px 0' }}> endAdornment={(
{name} <InputAdornment position="end">
</InputLabel> <SearchIcon />
<Input </InputAdornment>
name={name} )}
value={Search || ''} />
onChange={handleChange} </FormControl>
sx={{ margin: '10px 0 10px 0' }}
/>
</FormControl>
</>
</Box>
); );
} }
return (<></>); return (<></>);
} };
export default TextFilter;

View File

@@ -5,10 +5,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
import React from 'react'; import React from 'react';
import { Box } from '@mui/system';
import { FormControlLabel } from '@mui/material';
import ThreeStateCheckbox from 'components/util/ThreeStateCheckbox';
interface Props { interface Props {
state: number state: number
@@ -19,7 +17,7 @@ interface Props {
update: any update: any
} }
export default function TriStateFilter(props: Props) { const TriStateFilter: React.FC<Props> = (props) => {
const { const {
state, state,
name, name,
@@ -28,48 +26,32 @@ export default function TriStateFilter(props: Props) {
updateFilterValue, updateFilterValue,
update, update,
} = props; } = props;
const [val, setval] = React.useState({ const [val, setval] = React.useState<number>(Number(state));
[name]: state,
});
const handleChange = (checked: boolean | null | undefined) => { const handleChange = (checked: boolean | null | undefined) => {
const tmp = val; // eslint-disable-next-line no-nested-ternary
if (checked !== undefined) { const newState = checked === undefined ? 0 : checked ? 1 : 2;
tmp[name] = checked ? 1 : 2; setval(newState);
} else {
delete tmp[name];
}
setval({
...tmp,
});
const upd = update.filter((e: { const upd = update.filter((e: {
position: number; group: number | undefined; position: number; group: number | undefined;
}) => !(position === e.position && group === e.group)); }) => !(position === e.position && group === e.group));
updateFilterValue([...upd, { updateFilterValue([...upd, {
position, position,
state: (tmp[name] === undefined ? 0 : tmp[name]).toString(), state: newState.toString(),
group, group,
}]); }]);
}; };
if (state !== undefined) { if (state !== undefined) {
let check;
if (val[name] !== 0) {
check = val[name] === 1;
} else {
check = undefined;
}
return ( return (
<Box sx={{ marginLeft: 3 }}> <ThreeStateCheckboxInput
<FormControlLabel label={name}
key={name} checked={[undefined, true, false][val]}
control={( onChange={(checked) => handleChange(checked)}
<ThreeStateCheckbox name="Unread" checked={check} onChange={(checked) => handleChange(checked)} /> />
)}
label={name}
/>
</Box>
); );
} }
return (<></>); return (<></>);
} };
export default TriStateFilter;

View File

@@ -48,11 +48,9 @@ function TwoSatePreference(props: TwoStatePreferenceProps) {
} }
export function CheckBoxPreference(props: CheckBoxPreferenceProps) { export function CheckBoxPreference(props: CheckBoxPreferenceProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <TwoSatePreference {...props} type="Checkbox" />; return <TwoSatePreference {...props} type="Checkbox" />;
} }
export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) { export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <TwoSatePreference {...props} type="Switch" />; return <TwoSatePreference {...props} type="Switch" />;
} }

View File

@@ -12,10 +12,8 @@ import { Link } from 'react-router-dom';
export default function ListItemLink(props: ListItemProps<Link, { directLink?: boolean }>) { export default function ListItemLink(props: ListItemProps<Link, { directLink?: boolean }>) {
const { directLink, to } = props; const { directLink, to } = props;
if (directLink) { if (directLink) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <ListItem button component="a" href={to} {...props} />; return <ListItem button component="a" href={to} {...props} />;
} }
// eslint-disable-next-line react/jsx-props-no-spreading
return <ListItem button component={Link} {...props} />; return <ListItem button component={Link} {...props} />;
} }

View File

@@ -1,5 +1,3 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable react/require-default-props */
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *

View File

@@ -1,90 +0,0 @@
/*
* 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, name: string) => 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 = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocalChecked(stateTransition(localChecked));
if (onChange) {
onChange(stateToChecked(stateTransition(localChecked)), e.currentTarget.name);
}
};
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;

View File

@@ -18,7 +18,6 @@ function removeToast(id: string) {
} }
function Transition(props: SlideProps) { function Transition(props: SlideProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <Slide {...props} direction="up" />; return <Slide {...props} direction="up" />;
} }

View File

@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable react/destructuring-assignment */ /* eslint-disable react/destructuring-assignment */
/* eslint-disable react/jsx-props-no-spreading */
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *

View File

@@ -13,7 +13,7 @@ import NavbarContext from 'components/context/NavbarContext';
import EmptyView from 'components/util/EmptyView'; import EmptyView from 'components/util/EmptyView';
import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
import TabPanel from 'components/util/TabPanel'; import TabPanel from 'components/util/TabPanel';
import LibraryOptions from 'components/library/LibraryOptions'; import LibraryToolbarMenu from 'components/library/LibraryToolbarMenu';
import LibraryMangaGrid from 'components/library/LibraryMangaGrid'; import LibraryMangaGrid from 'components/library/LibraryMangaGrid';
import AppbarSearch from 'components/util/AppbarSearch'; import AppbarSearch from 'components/util/AppbarSearch';
import { useQueryParam, NumberParam } from 'use-query-params'; import { useQueryParam, NumberParam } from 'use-query-params';
@@ -42,7 +42,7 @@ export default function Library() {
setAction( setAction(
<> <>
<AppbarSearch /> <AppbarSearch />
<LibraryOptions /> <LibraryToolbarMenu />
<UpdateChecker /> <UpdateChecker />
</>, </>,
); );

View File

@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable react/destructuring-assignment */ /* eslint-disable react/destructuring-assignment */
/* eslint-disable react/jsx-props-no-spreading */
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *

4
src/typings.d.ts vendored
View File

@@ -276,6 +276,8 @@ type ChapterOptionsReducerAction =
| { type: 'sortReverse' } | { type: 'sortReverse' }
| { type: 'showChapterNumber' }; | { type: 'showChapterNumber' };
type LibrarySortMode = 'sortToRead' | 'sortAlph' | 'sortID';
interface LibraryOptions { interface LibraryOptions {
// display options // display options
showDownloadBadge: boolean showDownloadBadge: boolean
@@ -286,7 +288,7 @@ interface LibraryOptions {
// filter options // filter options
downloaded: NullAndUndefined<boolean> downloaded: NullAndUndefined<boolean>
unread: NullAndUndefined<boolean> unread: NullAndUndefined<boolean>
sorts: NullAndUndefined<string> sorts: NullAndUndefined<LibrarySortMode>
sortDesc: NullAndUndefined<boolean> sortDesc: NullAndUndefined<boolean>
} }