diff --git a/.eslintrc.js b/.eslintrc.js index 9c85192d..d2eace74 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,5 +22,7 @@ module.exports = { // just why 'react/jsx-no-bind' : 'off', + 'react/jsx-props-no-spreading': 'off', + 'react/require-default-props': 'off', }, }; diff --git a/src/components/atoms/CheckboxInput.tsx b/src/components/atoms/CheckboxInput.tsx new file mode 100644 index 00000000..2bf2684a --- /dev/null +++ b/src/components/atoms/CheckboxInput.tsx @@ -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 = ({ + label, sx, ...rest +}) => ( + + )} + label={label} + sx={sx} + /> +); + +export default CheckboxInput; diff --git a/src/components/atoms/LoadingIconButton.tsx b/src/components/atoms/LoadingIconButton.tsx index 22fc705f..9fa439b7 100644 --- a/src/components/atoms/LoadingIconButton.tsx +++ b/src/components/atoms/LoadingIconButton.tsx @@ -18,7 +18,6 @@ const LoadingIconButton = ({ }; return ( - // eslint-disable-next-line react/jsx-props-no-spreading {loading ? () : children} diff --git a/src/components/atoms/RadioInput.tsx b/src/components/atoms/RadioInput.tsx new file mode 100644 index 00000000..ee772f5e --- /dev/null +++ b/src/components/atoms/RadioInput.tsx @@ -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 = ({ + label, sx, ...rest +}) => ( + + )} + label={label} + sx={sx} + /> +); + +export default RadioInput; diff --git a/src/components/atoms/SortRadioInput.tsx b/src/components/atoms/SortRadioInput.tsx new file mode 100644 index 00000000..f11c03b1 --- /dev/null +++ b/src/components/atoms/SortRadioInput.tsx @@ -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 = ({ + sortDescending, ...rest +}) => ( + : } + {...rest} + /> +); + +export default SortRadioInput; diff --git a/src/components/atoms/ThreeStateCheckbox.tsx b/src/components/atoms/ThreeStateCheckbox.tsx new file mode 100644 index 00000000..b8abf119 --- /dev/null +++ b/src/components/atoms/ThreeStateCheckbox.tsx @@ -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 { + 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 = ({ checked, onChange, ...rest }) => { + const handleChange = useCallback(() => { + if (onChange) { + const newState = nextState(checked); + onChange(newState); + } + }, [onChange]); + + return ( + } + checked={checked === true} + indeterminate={checked === false} + onChange={handleChange} + {...rest} + /> + ); +}; +export default ThreeStateCheckbox; diff --git a/src/components/atoms/ThreeStateCheckboxInput.tsx b/src/components/atoms/ThreeStateCheckboxInput.tsx new file mode 100644 index 00000000..6b59ba5c --- /dev/null +++ b/src/components/atoms/ThreeStateCheckboxInput.tsx @@ -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 = ({ + label, sx, ...rest +}) => ( + + )} + label={label} + sx={sx} + /> +); + +export default ThreeStateCheckboxInput; diff --git a/src/components/context/LibraryOptionsContext.tsx b/src/components/context/LibraryOptionsContext.tsx index 9f04840b..869cc5f5 100644 --- a/src/components/context/LibraryOptionsContext.tsx +++ b/src/components/context/LibraryOptionsContext.tsx @@ -9,16 +9,7 @@ import React, { useContext } from 'react'; type ContextType = { options: LibraryOptions; - - setOption: ( - name: Name, - value: React.SetStateAction - ) => void; - setOptions: React.Dispatch>; - - active: boolean - activeSort: boolean }; export const DefaultLibraryOptions: LibraryOptions = { @@ -35,10 +26,7 @@ export const DefaultLibraryOptions: LibraryOptions = { const LibraryOptionsContext = React.createContext({ options: DefaultLibraryOptions, - setOption: () => {}, setOptions: () => {}, - active: false, - activeSort: false, }); export default LibraryOptionsContext; diff --git a/src/components/library/LibraryMangaGrid.tsx b/src/components/library/LibraryMangaGrid.tsx index 373c222b..c6c377fb 100644 --- a/src/components/library/LibraryMangaGrid.tsx +++ b/src/components/library/LibraryMangaGrid.tsx @@ -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 ( diff --git a/src/components/library/LibraryOptions.tsx b/src/components/library/LibraryOptions.tsx deleted file mode 100644 index 3f5e9239..00000000 --- a/src/components/library/LibraryOptions.tsx +++ /dev/null @@ -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 ( - - - setOption('unread', change)} - /> - )} - label="Unread" - /> - setOption('downloaded', change)} - /> - )} - label="Downloaded" - /> - - - ); -} - -function sortsTab(currentTab: number) { - const { options: { sorts, sortDesc }, setOption } = useLibraryOptionsContext(); - - const handleChange = (event: React.MouseEvent, index: string) => { - if (sorts === index) { - setOption('sortDesc', (sortDes) => !sortDes); - } else { - setOption('sortDesc', false); - } - setOption('sorts', index); - }; - - return ( - <> - - - { - ['sortToRead', 'sortAlph', 'sortID'].map((e) => { - let icon; - if (sorts === e) { - icon = !sortDesc ? () - : (); - } - icon = icon === undefined && sortDesc === undefined && e === 'sortID' ? () : icon; - return ( - - handleChange(event, e)}> - {icon} - - - - ); - }) - } - - - - ); -} - -function dispalyTab(currentTab: number) { - const { options, setOptions } = useLibraryOptionsContext(); - - function setContextOptions( - e: React.ChangeEvent, - checked: boolean, - ) { - setOptions((prev) => ({ ...prev, [e.target.name]: checked })); - } - - function setGridContextOptions( - e: React.ChangeEvent, - checked: boolean, - ) { - if (checked) { - setOptions((prev) => ({ ...prev, gridLayout: parseInt(e.target.name, 10) })); - } - } - - return ( - - - DISPLAY MODE - - )} - /> - - )} - /> - - )} - /> - BADGES - - )} - /> - - )} - /> - - - ); -} - -function Options() { - const [currentTab, setCurrentTab] = useState(0); - - return ( - - setCurrentTab(newTab)} - indicatorColor="primary" - textColor="primary" - > - - - - - {filtersTab(currentTab)} - {sortsTab(currentTab)} - {dispalyTab(currentTab)} - - ); -} - -export default function LibraryOptions() { - const [filtersOpen, setFiltersOpen] = React.useState(false); - const { active } = useLibraryOptionsContext(); - return ( - <> - setFiltersOpen(!filtersOpen)} - color={active ? 'warning' : 'default'} - > - - - - setFiltersOpen(false)} - PaperProps={{ - style: { - maxWidth: 600, padding: '1em', marginLeft: 'auto', marginRight: 'auto', - }, - }} - > - - - - ); -} diff --git a/src/components/library/LibraryOptionsPanel.tsx b/src/components/library/LibraryOptionsPanel.tsx new file mode 100644 index 00000000..87663d93 --- /dev/null +++ b/src/components/library/LibraryOptionsPanel.tsx @@ -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 = ({ open, onClose }) => { + const { options, setOptions } = useLibraryOptionsContext(); + + const handleFilterChange = ( + key: T, + value: LibraryOptions[T], + ) => { + setOptions((v) => ({ ...v, [key]: value })); + }; + + return ( + + open={open} + onClose={onClose} + tabs={['filter', 'sort', 'display']} + tabTitle={(key) => TITLES[key]} + tabContent={(key) => { + if (key === 'filter') { + return ( + <> + handleFilterChange('unread', c)} /> + handleFilterChange('downloaded', c)} /> + + ); + } + if (key === 'sort') { + return SORT_OPTIONS.map(([mode, label]) => ( + (mode !== options.sorts + ? handleFilterChange('sorts', mode) + : handleFilterChange('sortDesc', !options.sortDesc))} + /> + )); + } + if (key === 'display') { + const { gridLayout, showDownloadBadge, showUnreadBadge } = options; + return ( + <> + Display mode + handleFilterChange('gridLayout', Number(e.target.value))} + value={gridLayout} + > + + + + + + Badges + handleFilterChange('showUnreadBadge', !showUnreadBadge)} + /> + handleFilterChange('showDownloadBadge', !showDownloadBadge)} + /> + + ); + } + return null; + }} + /> + ); +}; + +export default LibraryOptionsPanel; diff --git a/src/components/library/LibraryOptionsProvider.tsx b/src/components/library/LibraryOptionsProvider.tsx index 786a3544..df2c815b 100644 --- a/src/components/library/LibraryOptionsProvider.tsx +++ b/src/components/library/LibraryOptionsProvider.tsx @@ -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 = ({ children }) => { const [options, setOptions] = useLocalStorage('libraryOptions', DefaultLibraryOptions); - function setOption( - option: Name, - value: React.SetStateAction, - ) { - 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 ( - + {children} ); -} +}; + +export default LibraryOptionsContextProvider; diff --git a/src/components/library/LibraryToolbarMenu.tsx b/src/components/library/LibraryToolbarMenu.tsx new file mode 100644 index 00000000..080bc977 --- /dev/null +++ b/src/components/library/LibraryToolbarMenu.tsx @@ -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 ( + <> + setOpen(!open)} + color={active ? 'warning' : 'default'} + > + + + setOpen(false)} /> + + ); +}; + +export default LibraryToolbarMenu; diff --git a/src/components/manga/ChapterList.tsx b/src/components/manga/ChapterList.tsx index d12c4ff6..100e1309 100644 --- a/src/components/manga/ChapterList.tsx +++ b/src/components/manga/ChapterList.tsx @@ -239,7 +239,6 @@ const ChapterList: React.FC = ({ mangaId }) => { totalCount={visibleChapters.length} itemContent={(index:number) => ( mutate()} diff --git a/src/components/manga/ChapterOptions.tsx b/src/components/manga/ChapterOptions.tsx index eeecd831..e76361ad 100644 --- a/src/components/manga/ChapterOptions.tsx +++ b/src/components/manga/ChapterOptions.tsx @@ -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 } -const TabContent: React.FC<{ children: React.ReactNode }> = ({ children }) => ( - - {children} - -); +const TITLES = { + filter: 'Filter', + sort: 'Sort', + display: 'Display', +}; const ChapterOptions: React.FC = ({ open, onClose, options, optionsDispatch, -}) => { - const [tabNum, setTabNum] = useState(0); - - const handleFilterChange = useCallback( - (value: NullAndUndefined, name: string) => { - optionsDispatch({ type: 'filter', filterType: name.toLowerCase(), filterValue: value }); - }, [], - ); - - return ( - <> - - - setTabNum(newTab)} - indicatorColor="primary" - textColor="primary" - > - - - - - - - } label="Unread" /> - } label="Downloaded" /> - } label="Bookmarked" /> - - - - - { - SORT_OPTIONS.map(([mode, label]) => ( - : } - onClick={() => (mode !== options.sortBy - ? optionsDispatch({ type: 'sortBy', sortBy: mode }) - : optionsDispatch({ type: 'sortReverse' }))} - /> - )} - label={label} - /> - )) - } - - - - - optionsDispatch({ type: 'showChapterNumber' })} value={options.showChapterNumber}> - } /> - } /> - - - - - - - ); -}; +}) => ( + + open={open} + onClose={onClose} + minHeight={150} + tabs={['filter', 'sort', 'display']} + tabTitle={(key) => TITLES[key]} + tabContent={(key) => { + if (key === 'filter') { + return ( + <> + optionsDispatch({ type: 'filter', filterType: 'unread', filterValue: c })} /> + optionsDispatch({ type: 'filter', filterType: 'downloaded', filterValue: c })} /> + optionsDispatch({ type: 'filter', filterType: 'bookmarked', filterValue: c })} /> + + ); + } + if (key === 'sort') { + return SORT_OPTIONS.map(([mode, label]) => ( + (mode !== options.sortBy + ? optionsDispatch({ type: 'sortBy', sortBy: mode }) + : optionsDispatch({ type: 'sortReverse' }))} + /> + )); + } + if (key === 'display') { + return ( + optionsDispatch({ type: 'showChapterNumber' })} value={options.showChapterNumber}> + + + + ); + } + return null; + }} + /> +); export default ChapterOptions; diff --git a/src/components/molecules/OptionsPanel.tsx b/src/components/molecules/OptionsPanel.tsx new file mode 100644 index 00000000..af424935 --- /dev/null +++ b/src/components/molecules/OptionsPanel.tsx @@ -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 = ({ + open, onClose, children, minHeight, +}) => ( + + + {children} + + +); + +OptionsPanel.defaultProps = { + minHeight: undefined, +}; + +export default OptionsPanel; diff --git a/src/components/molecules/OptionsTabs.tsx b/src/components/molecules/OptionsTabs.tsx new file mode 100644 index 00000000..c52a9377 --- /dev/null +++ b/src/components/molecules/OptionsTabs.tsx @@ -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{ + open: boolean + onClose: () => void + tabs: T[] + tabTitle: (key: T) => React.ReactNode + tabContent: (key: T) => React.ReactNode + minHeight?: number +} + +const OptionsTabs = ({ + open, onClose, tabs, tabTitle, tabContent, minHeight, +}: IProps) => { + const [tabNum, setTabNum] = useState(0); + + return ( + + setTabNum(newTab)} + indicatorColor="primary" + textColor="primary" + > + {tabs.map((tab, tabIndex) => ( + + ))} + + {tabs.map((tab, tabIndex) => ( + + + {tabContent(tab)} + + + ))} + + ); +}; + +OptionsTabs.defaultProps = { + minHeight: undefined, +}; + +export default OptionsTabs; diff --git a/src/components/source/SourceOptions.tsx b/src/components/source/SourceOptions.tsx index a6a65394..890663e8 100644 --- a/src/components/source/SourceOptions.tsx +++ b/src/components/source/SourceOptions.tsx @@ -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 ( - + { sourceFilter.map((e: ISourceFilters, index) => { let checkif = update.find((el: { group: number | undefined; position: number; @@ -145,7 +144,7 @@ export function Options({ return (); } })} - + ); } @@ -182,17 +181,11 @@ export default function SourceOptions({ Filter - setFilterOptions(false)} - PaperProps={{ - style: { - maxWidth: 600, padding: '1em', marginLeft: 'auto', marginRight: 'auto', - }, - }} > - + - - + + + + ); } diff --git a/src/components/source/filters/CheckBoxFilter.tsx b/src/components/source/filters/CheckBoxFilter.tsx index 1c405c95..933c3d31 100644 --- a/src/components/source/filters/CheckBoxFilter.tsx +++ b/src/components/source/filters/CheckBoxFilter.tsx @@ -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) => { const { state, name, @@ -39,20 +38,10 @@ export default function CheckBoxFilter(props: Props) { if (state !== undefined) { return ( - - - )} - label={name} - /> - + ); } - return (<>); -} + return null; +}; + +export default CheckBoxFilter; diff --git a/src/components/source/filters/GroupFilter.tsx b/src/components/source/filters/GroupFilter.tsx index d6c10477..a02de50a 100644 --- a/src/components/source/filters/GroupFilter.tsx +++ b/src/components/source/filters/GroupFilter.tsx @@ -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) => { const { state, name, @@ -32,26 +33,25 @@ export default function GroupFilter(props: Props) { const [open, setOpen] = React.useState(false); - const handleClick = () => { - setOpen(!open); - }; - return ( - <> - + + setOpen(!open)}> {open ? : } - + {/* Container is moved outside 2, so content has to go inside 4 */} + - + - + ); -} +}; + +export default GroupFilter; diff --git a/src/components/source/filters/HeaderFilter.tsx b/src/components/source/filters/HeaderFilter.tsx index 05b082ba..71ed004d 100644 --- a/src/components/source/filters/HeaderFilter.tsx +++ b/src/components/source/filters/HeaderFilter.tsx @@ -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 ({name}); -} +const HeaderFilter: React.FC = ({ name }) => ({name}); + +export default HeaderFilter; diff --git a/src/components/source/filters/SelectFilter.tsx b/src/components/source/filters/SelectFilter.tsx index 5e8e0c08..ad34067b 100644 --- a/src/components/source/filters/SelectFilter.tsx +++ b/src/components/source/filters/SelectFilter.tsx @@ -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,26 +60,22 @@ function hasSelect( )); return ( - - - - {name} - - - - + + + {name} + + + ); } - return (<>); + return null; } function noSelect( @@ -106,29 +101,25 @@ function noSelect( const rett = values.map((value: string) => ({value})); return ( - - - - {name} - - - - + + + {name} + + + ); } - return (<>); + return null; } -export default function SelectFilter({ +const SelectFilter: React.FC = ({ 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; diff --git a/src/components/source/filters/SeparatorFilter.tsx b/src/components/source/filters/SeparatorFilter.tsx index 25cf8b78..5e3075d4 100644 --- a/src/components/source/filters/SeparatorFilter.tsx +++ b/src/components/source/filters/SeparatorFilter.tsx @@ -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 ({name}); -} +const SeparatorFilter: React.FC = ({ name }) => ({name}); + +export default SeparatorFilter; diff --git a/src/components/source/filters/SortFilter.tsx b/src/components/source/filters/SortFilter.tsx index b9626706..c4027403 100644 --- a/src/components/source/filters/SortFilter.tsx +++ b/src/components/source/filters/SortFilter.tsx @@ -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) => { const { values, name, @@ -47,8 +42,7 @@ export default function SortFilter(props: Props) { }; if (values) { - const handleChange = (event: - React.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 = ( - + return ( + {open ? : } - - {values.map((value: string, index: number) => { - let icon; - if (val.index === index) { - icon = val.ascending ? () - : (); - } - return ( - - handleChange(event, index)} - > - - {icon} - - - - - ); - })} - + + {values.map((value: string, index: number) => ( + handleChange(index)} + /> + ))} + - - ); - return ( - - {ret} ); } - return (<>); -} + return null; +}; + +export default SortFilter; diff --git a/src/components/source/filters/TextFilter.tsx b/src/components/source/filters/TextFilter.tsx index 14d65a9f..a7e90b75 100644 --- a/src/components/source/filters/TextFilter.tsx +++ b/src/components/source/filters/TextFilter.tsx @@ -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) => { const { state, name, @@ -51,27 +48,24 @@ export default function TextFilter(props: Props) { if (state !== undefined) { return ( - - <> - - - - {name} - - - - - + + + {name} + + + + + )} + /> + ); } return (<>); -} +}; + +export default TextFilter; diff --git a/src/components/source/filters/TriStateFilter.tsx b/src/components/source/filters/TriStateFilter.tsx index 37f868af..7034e71a 100644 --- a/src/components/source/filters/TriStateFilter.tsx +++ b/src/components/source/filters/TriStateFilter.tsx @@ -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) => { 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(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 ( - - handleChange(checked)} /> - )} - label={name} - /> - + handleChange(checked)} + /> ); } return (<>); -} +}; + +export default TriStateFilter; diff --git a/src/components/sourceConfiguration/TwoStatePreference.tsx b/src/components/sourceConfiguration/TwoStatePreference.tsx index 674ad781..8d168683 100644 --- a/src/components/sourceConfiguration/TwoStatePreference.tsx +++ b/src/components/sourceConfiguration/TwoStatePreference.tsx @@ -48,11 +48,9 @@ function TwoSatePreference(props: TwoStatePreferenceProps) { } export function CheckBoxPreference(props: CheckBoxPreferenceProps) { - // eslint-disable-next-line react/jsx-props-no-spreading return ; } export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) { - // eslint-disable-next-line react/jsx-props-no-spreading return ; } diff --git a/src/components/util/ListItemLink.tsx b/src/components/util/ListItemLink.tsx index 3e08a27f..637a93bc 100644 --- a/src/components/util/ListItemLink.tsx +++ b/src/components/util/ListItemLink.tsx @@ -12,10 +12,8 @@ import { Link } from 'react-router-dom'; export default function ListItemLink(props: ListItemProps) { const { directLink, to } = props; if (directLink) { - // eslint-disable-next-line react/jsx-props-no-spreading return ; } - // eslint-disable-next-line react/jsx-props-no-spreading return ; } diff --git a/src/components/util/LoadingPlaceholder.tsx b/src/components/util/LoadingPlaceholder.tsx index 5985fa7b..e34ae833 100644 --- a/src/components/util/LoadingPlaceholder.tsx +++ b/src/components/util/LoadingPlaceholder.tsx @@ -1,5 +1,3 @@ -/* eslint-disable react/jsx-props-no-spreading */ -/* eslint-disable react/require-default-props */ /* * Copyright (C) Contributors to the Suwayomi project * diff --git a/src/components/util/ThreeStateCheckbox.tsx b/src/components/util/ThreeStateCheckbox.tsx deleted file mode 100644 index 3fc6795e..00000000 --- a/src/components/util/ThreeStateCheckbox.tsx +++ /dev/null @@ -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) => { - setLocalChecked(stateTransition(localChecked)); - if (onChange) { - onChange(stateToChecked(stateTransition(localChecked)), e.currentTarget.name); - } - }; - const CancelBox = createSvgIcon( - <> - - , - 'CancelBox', - ); - - return ( - } - onChange={handleChange} - className={`${localChecked}`} - /> - ); -}; -export default ThreeStateCheckbox; diff --git a/src/components/util/Toast.tsx b/src/components/util/Toast.tsx index 7d05eb9b..e45eba6e 100644 --- a/src/components/util/Toast.tsx +++ b/src/components/util/Toast.tsx @@ -18,7 +18,6 @@ function removeToast(id: string) { } function Transition(props: SlideProps) { - // eslint-disable-next-line react/jsx-props-no-spreading return ; } diff --git a/src/screens/DownloadQueue.tsx b/src/screens/DownloadQueue.tsx index 9b8e408a..e6818977 100644 --- a/src/screens/DownloadQueue.tsx +++ b/src/screens/DownloadQueue.tsx @@ -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 * diff --git a/src/screens/Library.tsx b/src/screens/Library.tsx index f8e9c625..75838f24 100644 --- a/src/screens/Library.tsx +++ b/src/screens/Library.tsx @@ -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( <> - + , ); diff --git a/src/screens/settings/Categories.tsx b/src/screens/settings/Categories.tsx index 149061e7..f38f9f17 100644 --- a/src/screens/settings/Categories.tsx +++ b/src/screens/settings/Categories.tsx @@ -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 * diff --git a/src/typings.d.ts b/src/typings.d.ts index cff1357d..a630d2ee 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -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 unread: NullAndUndefined - sorts: NullAndUndefined + sorts: NullAndUndefined sortDesc: NullAndUndefined }