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:
@@ -22,5 +22,7 @@ module.exports = {
|
||||
|
||||
// just why
|
||||
'react/jsx-no-bind' : 'off',
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
'react/require-default-props': 'off',
|
||||
},
|
||||
};
|
||||
|
||||
28
src/components/atoms/CheckboxInput.tsx
Normal file
28
src/components/atoms/CheckboxInput.tsx
Normal 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;
|
||||
@@ -18,7 +18,6 @@ const LoadingIconButton = ({
|
||||
};
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<IconButton disabled={loading} {...rest} onClick={handleClick}>
|
||||
{loading ? (<CircularProgress size={24} />) : children}
|
||||
</IconButton>
|
||||
|
||||
28
src/components/atoms/RadioInput.tsx
Normal file
28
src/components/atoms/RadioInput.tsx
Normal 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;
|
||||
27
src/components/atoms/SortRadioInput.tsx
Normal file
27
src/components/atoms/SortRadioInput.tsx
Normal 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;
|
||||
49
src/components/atoms/ThreeStateCheckbox.tsx
Normal file
49
src/components/atoms/ThreeStateCheckbox.tsx
Normal 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;
|
||||
29
src/components/atoms/ThreeStateCheckboxInput.tsx
Normal file
29
src/components/atoms/ThreeStateCheckboxInput.tsx
Normal 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;
|
||||
@@ -9,16 +9,7 @@ import React, { useContext } from 'react';
|
||||
|
||||
type ContextType = {
|
||||
options: LibraryOptions;
|
||||
|
||||
setOption: <Name extends keyof LibraryOptions>(
|
||||
name: Name,
|
||||
value: React.SetStateAction<LibraryOptions[Name]>
|
||||
) => void;
|
||||
|
||||
setOptions: React.Dispatch<React.SetStateAction<LibraryOptions>>;
|
||||
|
||||
active: boolean
|
||||
activeSort: boolean
|
||||
};
|
||||
|
||||
export const DefaultLibraryOptions: LibraryOptions = {
|
||||
@@ -35,10 +26,7 @@ export const DefaultLibraryOptions: LibraryOptions = {
|
||||
|
||||
const LibraryOptionsContext = React.createContext<ContextType>({
|
||||
options: DefaultLibraryOptions,
|
||||
setOption: () => {},
|
||||
setOptions: () => {},
|
||||
active: false,
|
||||
activeSort: false,
|
||||
});
|
||||
|
||||
export default LibraryOptionsContext;
|
||||
|
||||
@@ -83,7 +83,8 @@ export default function LibraryMangaGrid(props: IMangaGridProps) {
|
||||
} = props;
|
||||
|
||||
const [query] = useQueryParam('query', StringParam);
|
||||
const { options, active } = useLibraryOptionsContext();
|
||||
const { options } = useLibraryOptionsContext();
|
||||
const { unread, downloaded } = options;
|
||||
const filteredManga = filterManga(mangas);
|
||||
const sortedManga = sortManga(filteredManga);
|
||||
const DoneManga = sortedManga.map((ele) => {
|
||||
@@ -91,7 +92,7 @@ export default function LibraryMangaGrid(props: IMangaGridProps) {
|
||||
ele.inLibrary = undefined;
|
||||
return ele;
|
||||
});
|
||||
const showFilteredOutMessage = (active || query)
|
||||
const showFilteredOutMessage = (unread != null || downloaded != null || query)
|
||||
&& filteredManga.length === 0 && mangas.length > 0;
|
||||
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
106
src/components/library/LibraryOptionsPanel.tsx
Normal file
106
src/components/library/LibraryOptionsPanel.tsx
Normal 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;
|
||||
@@ -5,44 +5,22 @@
|
||||
* 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 LibraryOptionsContext, { DefaultLibraryOptions } from 'components/context/LibraryOptionsContext';
|
||||
import React from 'react';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function LibraryOptionsContextProvider({ children }: IProps) {
|
||||
const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => {
|
||||
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 (
|
||||
<LibraryOptionsContext.Provider
|
||||
value={{
|
||||
options,
|
||||
setOption,
|
||||
setOptions,
|
||||
active,
|
||||
activeSort,
|
||||
}}
|
||||
>
|
||||
<LibraryOptionsContext.Provider value={{ options, setOptions }}>
|
||||
{children}
|
||||
</LibraryOptionsContext.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default LibraryOptionsContextProvider;
|
||||
|
||||
32
src/components/library/LibraryToolbarMenu.tsx
Normal file
32
src/components/library/LibraryToolbarMenu.tsx
Normal 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;
|
||||
@@ -239,7 +239,6 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
totalCount={visibleChapters.length}
|
||||
itemContent={(index:number) => (
|
||||
<ChapterCard
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...chaptersWithMeta[index]}
|
||||
showChapterNumber={options.showChapterNumber}
|
||||
triggerChaptersUpdate={() => mutate()}
|
||||
|
||||
@@ -5,110 +5,70 @@
|
||||
* 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, ArrowUpward } from '@mui/icons-material';
|
||||
import {
|
||||
Drawer, FormControlLabel, Radio, RadioGroup, Tab, Tabs,
|
||||
} from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import TabPanel from 'components/util/TabPanel';
|
||||
import ThreeStateCheckbox from 'components/util/ThreeStateCheckbox';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { RadioGroup } from '@mui/material';
|
||||
import RadioInput from 'components/atoms/RadioInput';
|
||||
import SortRadioInput from 'components/atoms/SortRadioInput';
|
||||
import ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
|
||||
import OptionsTabs from 'components/molecules/OptionsTabs';
|
||||
import React from 'react';
|
||||
import { SORT_OPTIONS } from './util';
|
||||
|
||||
interface IProps{
|
||||
interface IProps {
|
||||
open: boolean
|
||||
onClose: () => void;
|
||||
onClose: () => void
|
||||
options: ChapterListOptions
|
||||
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>
|
||||
}
|
||||
|
||||
const TabContent: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<Box sx={{
|
||||
px: 3, py: 1, display: 'flex', flexDirection: 'column', minHeight: 150,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
const TITLES = {
|
||||
filter: 'Filter',
|
||||
sort: 'Sort',
|
||||
display: 'Display',
|
||||
};
|
||||
|
||||
const ChapterOptions: React.FC<IProps> = ({
|
||||
open, onClose, options, optionsDispatch,
|
||||
}) => {
|
||||
const [tabNum, setTabNum] = useState(0);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(value: NullAndUndefined<boolean>, name: string) => {
|
||||
optionsDispatch({ type: 'filter', filterType: name.toLowerCase(), filterValue: value });
|
||||
}, [],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
anchor="bottom"
|
||||
}) => (
|
||||
<OptionsTabs<'filter' | 'sort' | 'display'>
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
PaperProps={{
|
||||
style: {
|
||||
maxWidth: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
minHeight: '150px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Tabs
|
||||
value={tabNum}
|
||||
variant="fullWidth"
|
||||
onChange={(e, newTab) => setTabNum(newTab)}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
>
|
||||
<Tab value={0} label="Filter" />
|
||||
<Tab value={1} label="Sort" />
|
||||
<Tab value={2} label="Display" />
|
||||
</Tabs>
|
||||
<TabPanel index={0} currentIndex={tabNum}>
|
||||
<TabContent>
|
||||
<FormControlLabel control={<ThreeStateCheckbox name="Unread" checked={options.unread} onChange={handleFilterChange} />} label="Unread" />
|
||||
<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
|
||||
minHeight={150}
|
||||
tabs={['filter', 'sort', 'display']}
|
||||
tabTitle={(key) => TITLES[key]}
|
||||
tabContent={(key) => {
|
||||
if (key === 'filter') {
|
||||
return (
|
||||
<>
|
||||
<ThreeStateCheckboxInput label="Unread" checked={options.unread} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'unread', filterValue: c })} />
|
||||
<ThreeStateCheckboxInput label="Downloaded" checked={options.downloaded} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'downloaded', filterValue: c })} />
|
||||
<ThreeStateCheckboxInput label="Bookmarked" checked={options.bookmarked} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'bookmarked', filterValue: c })} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (key === 'sort') {
|
||||
return SORT_OPTIONS.map(([mode, label]) => (
|
||||
<SortRadioInput
|
||||
key={mode}
|
||||
control={(
|
||||
<Radio
|
||||
label={label}
|
||||
checked={options.sortBy === mode}
|
||||
checkedIcon={options.reverse ? <ArrowUpward color="primary" /> : <ArrowDownward color="primary" />}
|
||||
sortDescending={options.reverse}
|
||||
onClick={() => (mode !== options.sortBy
|
||||
? optionsDispatch({ type: 'sortBy', sortBy: mode })
|
||||
: optionsDispatch({ type: 'sortReverse' }))}
|
||||
/>
|
||||
)}
|
||||
label={label}
|
||||
/>
|
||||
))
|
||||
));
|
||||
}
|
||||
</TabContent>
|
||||
</TabPanel>
|
||||
<TabPanel index={2} currentIndex={tabNum}>
|
||||
<TabContent>
|
||||
if (key === 'display') {
|
||||
return (
|
||||
<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} />} />
|
||||
<RadioInput label="Source Title" value={false} />
|
||||
<RadioInput label="Chapter Number" value />
|
||||
</RadioGroup>
|
||||
</TabContent>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default ChapterOptions;
|
||||
|
||||
47
src/components/molecules/OptionsPanel.tsx
Normal file
47
src/components/molecules/OptionsPanel.tsx
Normal 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;
|
||||
59
src/components/molecules/OptionsTabs.tsx
Normal file
59
src/components/molecules/OptionsTabs.tsx
Normal 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;
|
||||
@@ -6,22 +6,21 @@
|
||||
* 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, Button, Fab,
|
||||
} from '@mui/material';
|
||||
import { Button, Fab, Stack } from '@mui/material';
|
||||
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 HeaderFilter from './filters/HeaderFilter';
|
||||
import SeperatorFilter from './filters/SeparatorFilter';
|
||||
import SelectFilter from './filters/SelectFilter';
|
||||
import SortFilter from './filters/SortFilter';
|
||||
import TextFilter from './filters/TextFilter';
|
||||
import TriStateFilter from './filters/TriStateFilter';
|
||||
// this can only cycle once, so should be fine
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import GroupFilter from './filters/GroupFilter';
|
||||
import SeperatorFilter from './filters/SeparatorFilter';
|
||||
|
||||
interface IFilters {
|
||||
sourceFilter: ISourceFilters[]
|
||||
@@ -46,7 +45,7 @@ export function Options({
|
||||
update,
|
||||
}: IFilters) {
|
||||
return (
|
||||
<Box key={`filters ${group}`}>
|
||||
<Stack key={`filters ${group}`}>
|
||||
{ sourceFilter.map((e: ISourceFilters, index) => {
|
||||
let checkif = update.find((el: {
|
||||
group: number | undefined; position: number;
|
||||
@@ -145,7 +144,7 @@ export function Options({
|
||||
return (<Box key={`${e.filter.name}null`} />);
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,17 +181,11 @@ export default function SourceOptions({
|
||||
Filter
|
||||
</Fab>
|
||||
|
||||
<Drawer
|
||||
anchor="bottom"
|
||||
<OptionsPanel
|
||||
open={FilterOptions}
|
||||
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
|
||||
onClick={handleReset}
|
||||
>
|
||||
@@ -206,13 +199,20 @@ export default function SourceOptions({
|
||||
Submit
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
pb: 2,
|
||||
mx: 2,
|
||||
}}
|
||||
>
|
||||
<Options
|
||||
sourceFilter={sourceFilter}
|
||||
updateFilterValue={updateFilterValue}
|
||||
group={undefined}
|
||||
update={update}
|
||||
/>
|
||||
</Drawer>
|
||||
</Box>
|
||||
</OptionsPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
* 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 CheckboxInput from 'components/atoms/CheckboxInput';
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/system';
|
||||
import { Checkbox, FormControlLabel } from '@mui/material';
|
||||
|
||||
interface Props {
|
||||
state: boolean
|
||||
@@ -18,7 +17,7 @@ interface Props {
|
||||
update: any
|
||||
}
|
||||
|
||||
export default function CheckBoxFilter(props: Props) {
|
||||
const CheckBoxFilter: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
state,
|
||||
name,
|
||||
@@ -39,20 +38,10 @@ export default function CheckBoxFilter(props: Props) {
|
||||
|
||||
if (state !== undefined) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}>
|
||||
<FormControlLabel
|
||||
key={name}
|
||||
control={(
|
||||
<Checkbox
|
||||
name={name}
|
||||
checked={val}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
label={name}
|
||||
/>
|
||||
</Box>
|
||||
<CheckboxInput label={name} checked={val} onChange={handleChange} />
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default CheckBoxFilter;
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
* 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 {
|
||||
Collapse, List, ListItemButton, ListItemText,
|
||||
} from '@mui/material';
|
||||
import React from 'react';
|
||||
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
|
||||
import { Options } from '../SourceOptions';
|
||||
|
||||
@@ -21,7 +22,7 @@ interface Props {
|
||||
update: any
|
||||
}
|
||||
|
||||
export default function GroupFilter(props: Props) {
|
||||
const GroupFilter: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
state,
|
||||
name,
|
||||
@@ -32,26 +33,25 @@ export default function GroupFilter(props: Props) {
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton onClick={handleClick}>
|
||||
<Box sx={{ mx: -2 }}>
|
||||
<ListItemButton onClick={() => setOpen(!open)}>
|
||||
<ListItemText primary={name} />
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemButton>
|
||||
<Collapse in={open}>
|
||||
<List disablePadding>
|
||||
{/* Container is moved outside 2, so content has to go inside 4 */}
|
||||
<Stack sx={{ mx: 4 }}>
|
||||
<Options
|
||||
sourceFilter={state}
|
||||
group={position}
|
||||
updateFilterValue={updateFilterValue}
|
||||
update={update}
|
||||
/>
|
||||
</List>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default GroupFilter;
|
||||
|
||||
@@ -6,14 +6,13 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/system';
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
}
|
||||
|
||||
export default function HeaderFilter(props: Props) {
|
||||
const { name } = props;
|
||||
return (<Box key={name}>{name}</Box>);
|
||||
}
|
||||
const HeaderFilter: React.FC<Props> = ({ name }) => (<Typography key={name} sx={{ mt: 2 }} variant="subtitle2">{name}</Typography>);
|
||||
|
||||
export default HeaderFilter;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/system';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
@@ -61,9 +60,8 @@ function hasSelect(
|
||||
</MenuItem>
|
||||
));
|
||||
return (
|
||||
<Box key={name} sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel sx={{ margin: '10px 0 10px 0' }}>
|
||||
<FormControl sx={{ my: 1 }} variant="standard">
|
||||
<InputLabel>
|
||||
{name}
|
||||
</InputLabel>
|
||||
<Select
|
||||
@@ -71,16 +69,13 @@ function hasSelect(
|
||||
value={values[val].displayname}
|
||||
label={name}
|
||||
onChange={handleChange}
|
||||
autoWidth
|
||||
sx={{ margin: '10px 0 10px 0' }}
|
||||
>
|
||||
{rett}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
return null;
|
||||
}
|
||||
|
||||
function noSelect(
|
||||
@@ -106,9 +101,8 @@ function noSelect(
|
||||
|
||||
const rett = values.map((value: string) => (<MenuItem key={`${name} ${value}`} value={value}>{value}</MenuItem>));
|
||||
return (
|
||||
<Box key={name} sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel sx={{ margin: '10px 0 10px 0' }}>
|
||||
<FormControl sx={{ my: 1 }} variant="standard">
|
||||
<InputLabel>
|
||||
{name}
|
||||
</InputLabel>
|
||||
<Select
|
||||
@@ -116,19 +110,16 @@ function noSelect(
|
||||
value={values[val]}
|
||||
label={name}
|
||||
onChange={handleChange}
|
||||
autoWidth
|
||||
sx={{ margin: '10px 0 10px 0' }}
|
||||
>
|
||||
{rett}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function SelectFilter({
|
||||
const SelectFilter: React.FC<Props> = ({
|
||||
values,
|
||||
name,
|
||||
state,
|
||||
@@ -137,7 +128,7 @@ export default function SelectFilter({
|
||||
updateFilterValue,
|
||||
update,
|
||||
group,
|
||||
}: Props) {
|
||||
}) => {
|
||||
if (selected === undefined) {
|
||||
return noSelect(
|
||||
values,
|
||||
@@ -159,4 +150,6 @@ export default function SelectFilter({
|
||||
update,
|
||||
group,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default SelectFilter;
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
* 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 { Divider } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/system';
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
}
|
||||
|
||||
export default function SeperatorFilter(props: Props) {
|
||||
const { name } = props;
|
||||
return (<Box key={name}>{name}</Box>);
|
||||
}
|
||||
const SeparatorFilter: React.FC<Props> = ({ name }) => (<Divider key={name} sx={{ my: 1 }} textAlign="center">{name}</Divider>);
|
||||
|
||||
export default SeparatorFilter;
|
||||
|
||||
@@ -5,18 +5,13 @@
|
||||
* 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 { 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 {
|
||||
Collapse, ListItemButton, ListItemText, Stack,
|
||||
} from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import SortRadioInput from 'components/atoms/SortRadioInput';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
values: any
|
||||
@@ -28,7 +23,7 @@ interface Props {
|
||||
update: any
|
||||
}
|
||||
|
||||
export default function SortFilter(props: Props) {
|
||||
const SortFilter: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
values,
|
||||
name,
|
||||
@@ -47,8 +42,7 @@ export default function SortFilter(props: Props) {
|
||||
};
|
||||
|
||||
if (values) {
|
||||
const handleChange = (event:
|
||||
React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => {
|
||||
const handleChange = (index: number) => {
|
||||
const tmp = val;
|
||||
if (tmp.index === index) {
|
||||
tmp.ascending = !tmp.ascending;
|
||||
@@ -63,42 +57,29 @@ export default function SortFilter(props: Props) {
|
||||
updateFilterValue([...upd, { position, state: JSON.stringify(tmp), group }]);
|
||||
};
|
||||
|
||||
const ret = (
|
||||
<FormControl fullWidth>
|
||||
return (
|
||||
<Box sx={{ mx: -2 }}>
|
||||
<ListItemButton onClick={handleClick}>
|
||||
<ListItemText primary={name} />
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemButton>
|
||||
<Collapse in={open}>
|
||||
<List>
|
||||
{values.map((value: string, index: number) => {
|
||||
let icon;
|
||||
if (val.index === index) {
|
||||
icon = val.ascending ? (<ArrowUpwardIcon color="primary" />)
|
||||
: (<ArrowDownwardIcon color="primary" />);
|
||||
}
|
||||
return (
|
||||
<ListItem disablePadding key={`${name} ${value}`}>
|
||||
<ListItemButton
|
||||
onClick={(event) => handleChange(event, index)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={value} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Stack sx={{ mx: 4 }}>
|
||||
{values.map((value: string, index: number) => (
|
||||
<SortRadioInput
|
||||
key={`${name} ${value}`}
|
||||
label={value}
|
||||
checked={val.index === index}
|
||||
sortDescending={!val.ascending}
|
||||
onClick={() => handleChange(index)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</FormControl>
|
||||
);
|
||||
return (
|
||||
<Box key={name} sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}>
|
||||
{ret}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SortFilter;
|
||||
|
||||
@@ -5,14 +5,11 @@
|
||||
* 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 { Box } from '@mui/system';
|
||||
import {
|
||||
Input,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
} from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import {
|
||||
FormControl, Input, InputAdornment, InputLabel,
|
||||
} from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
state: string
|
||||
@@ -23,7 +20,7 @@ interface Props {
|
||||
update: any
|
||||
}
|
||||
|
||||
export default function TextFilter(props: Props) {
|
||||
const TextFilter: React.FC<Props> = (props) => {
|
||||
const {
|
||||
state,
|
||||
name,
|
||||
@@ -51,27 +48,24 @@ export default function TextFilter(props: Props) {
|
||||
|
||||
if (state !== undefined) {
|
||||
return (
|
||||
<Box key={`${name}`} sx={{ display: 'flex', flexDirection: 'row', minWidth: 120 }}>
|
||||
<>
|
||||
<SearchIcon
|
||||
sx={{
|
||||
margin: 'auto',
|
||||
}}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel sx={{ margin: '10px 0 10px 0' }}>
|
||||
<FormControl sx={{ my: 1 }} variant="standard">
|
||||
<InputLabel>
|
||||
{name}
|
||||
</InputLabel>
|
||||
<Input
|
||||
name={name}
|
||||
value={Search || ''}
|
||||
onChange={handleChange}
|
||||
sx={{ margin: '10px 0 10px 0' }}
|
||||
endAdornment={(
|
||||
<InputAdornment position="end">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
}
|
||||
};
|
||||
|
||||
export default TextFilter;
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
* 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 ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/system';
|
||||
import { FormControlLabel } from '@mui/material';
|
||||
import ThreeStateCheckbox from 'components/util/ThreeStateCheckbox';
|
||||
|
||||
interface Props {
|
||||
state: number
|
||||
@@ -19,7 +17,7 @@ interface Props {
|
||||
update: any
|
||||
}
|
||||
|
||||
export default function TriStateFilter(props: Props) {
|
||||
const TriStateFilter: React.FC<Props> = (props) => {
|
||||
const {
|
||||
state,
|
||||
name,
|
||||
@@ -28,48 +26,32 @@ export default function TriStateFilter(props: Props) {
|
||||
updateFilterValue,
|
||||
update,
|
||||
} = props;
|
||||
const [val, setval] = React.useState({
|
||||
[name]: state,
|
||||
});
|
||||
const [val, setval] = React.useState<number>(Number(state));
|
||||
|
||||
const handleChange = (checked: boolean | null | undefined) => {
|
||||
const tmp = val;
|
||||
if (checked !== undefined) {
|
||||
tmp[name] = checked ? 1 : 2;
|
||||
} else {
|
||||
delete tmp[name];
|
||||
}
|
||||
setval({
|
||||
...tmp,
|
||||
});
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const newState = checked === undefined ? 0 : checked ? 1 : 2;
|
||||
setval(newState);
|
||||
const upd = update.filter((e: {
|
||||
position: number; group: number | undefined;
|
||||
}) => !(position === e.position && group === e.group));
|
||||
updateFilterValue([...upd, {
|
||||
position,
|
||||
state: (tmp[name] === undefined ? 0 : tmp[name]).toString(),
|
||||
state: newState.toString(),
|
||||
group,
|
||||
}]);
|
||||
};
|
||||
|
||||
if (state !== undefined) {
|
||||
let check;
|
||||
if (val[name] !== 0) {
|
||||
check = val[name] === 1;
|
||||
} else {
|
||||
check = undefined;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ marginLeft: 3 }}>
|
||||
<FormControlLabel
|
||||
key={name}
|
||||
control={(
|
||||
<ThreeStateCheckbox name="Unread" checked={check} onChange={(checked) => handleChange(checked)} />
|
||||
)}
|
||||
<ThreeStateCheckboxInput
|
||||
label={name}
|
||||
checked={[undefined, true, false][val]}
|
||||
onChange={(checked) => handleChange(checked)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
}
|
||||
};
|
||||
|
||||
export default TriStateFilter;
|
||||
|
||||
@@ -48,11 +48,9 @@ function TwoSatePreference(props: TwoStatePreferenceProps) {
|
||||
}
|
||||
|
||||
export function CheckBoxPreference(props: CheckBoxPreferenceProps) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <TwoSatePreference {...props} type="Checkbox" />;
|
||||
}
|
||||
export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <TwoSatePreference {...props} type="Switch" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,8 @@ import { Link } from 'react-router-dom';
|
||||
export default function ListItemLink(props: ListItemProps<Link, { directLink?: boolean }>) {
|
||||
const { directLink, to } = props;
|
||||
if (directLink) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <ListItem button component="a" href={to} {...props} />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <ListItem button component={Link} {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/require-default-props */
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
@@ -18,7 +18,6 @@ function removeToast(id: string) {
|
||||
}
|
||||
|
||||
function Transition(props: SlideProps) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <Slide {...props} direction="up" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
|
||||
@@ -13,7 +13,7 @@ import NavbarContext from 'components/context/NavbarContext';
|
||||
import EmptyView from 'components/util/EmptyView';
|
||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||
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 AppbarSearch from 'components/util/AppbarSearch';
|
||||
import { useQueryParam, NumberParam } from 'use-query-params';
|
||||
@@ -42,7 +42,7 @@ export default function Library() {
|
||||
setAction(
|
||||
<>
|
||||
<AppbarSearch />
|
||||
<LibraryOptions />
|
||||
<LibraryToolbarMenu />
|
||||
<UpdateChecker />
|
||||
</>,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
|
||||
4
src/typings.d.ts
vendored
4
src/typings.d.ts
vendored
@@ -276,6 +276,8 @@ type ChapterOptionsReducerAction =
|
||||
| { type: 'sortReverse' }
|
||||
| { type: 'showChapterNumber' };
|
||||
|
||||
type LibrarySortMode = 'sortToRead' | 'sortAlph' | 'sortID';
|
||||
|
||||
interface LibraryOptions {
|
||||
// display options
|
||||
showDownloadBadge: boolean
|
||||
@@ -286,7 +288,7 @@ interface LibraryOptions {
|
||||
// filter options
|
||||
downloaded: NullAndUndefined<boolean>
|
||||
unread: NullAndUndefined<boolean>
|
||||
sorts: NullAndUndefined<string>
|
||||
sorts: NullAndUndefined<LibrarySortMode>
|
||||
sortDesc: NullAndUndefined<boolean>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user