diff --git a/src/App.tsx b/src/App.tsx index e620069c..367e852a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,6 @@ import About from 'screens/settings/About'; import Categories from 'screens/settings/Categories'; import Backup from 'screens/settings/Backup'; import Library from 'screens/Library'; -import SearchSingle from 'screens/SearchSingle'; import SourceConfigure from 'screens/SourceConfigure'; import Manga from 'screens/Manga'; import SourceMangas from 'screens/SourceMangas'; @@ -131,9 +130,6 @@ export default function App() { {/* Manga Routes */} - - - diff --git a/src/components/SourceCard.tsx b/src/components/SourceCard.tsx index 3a4bb1fc..6237ab14 100644 --- a/src/components/SourceCard.tsx +++ b/src/components/SourceCard.tsx @@ -9,10 +9,7 @@ import React from 'react'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import { useHistory } from 'react-router-dom'; -import SearchIcon from '@mui/icons-material/Search'; import Button from '@mui/material/Button'; -import IconButton from '@mui/material/IconButton'; -import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined'; import Avatar from '@mui/material/Avatar'; import Typography from '@mui/material/Typography'; import useLocalStorage from 'util/useLocalStorage'; @@ -108,34 +105,16 @@ export default function SourceCard(props: IProps) { <> - redirectTo(e, `/sources/${id}/search/`)} - size="large" - edge="end" - > - - {supportsLatest && ( - redirectTo(e, `/sources/${id}/latest/`)} - size="large" > - - + Latest + )} - {supportsLatest && ( + + + + + + ); +} diff --git a/src/components/source/filters/CheckBoxFilter.tsx b/src/components/source/filters/CheckBoxFilter.tsx new file mode 100644 index 00000000..00d9eba0 --- /dev/null +++ b/src/components/source/filters/CheckBoxFilter.tsx @@ -0,0 +1,53 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +import React from 'react'; +import { Box } from '@mui/system'; +import { Checkbox, FormControlLabel } from '@mui/material'; + +interface Props { + state: boolean + name: string + position: number + group: number | undefined + updateFilterValue: Function +} + +export default function CheckBoxFilter(props: Props) { + const { + state, + name, + position, + group, + updateFilterValue, + } = props; + const [val, setval] = React.useState(state); + + const handleChange = (event: { target: { name: any; checked: any; }; }) => { + setval(event.target.checked); + updateFilterValue({ position, state: event.target.checked.toString(), group }); + }; + + if (state !== undefined) { + return ( + + + )} + label={name} + /> + + ); + } + return (<>); +} diff --git a/src/components/source/filters/GroupFilter.tsx b/src/components/source/filters/GroupFilter.tsx new file mode 100644 index 00000000..a2f3fe6d --- /dev/null +++ b/src/components/source/filters/GroupFilter.tsx @@ -0,0 +1,54 @@ +/* + * 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 { + Collapse, List, ListItemButton, ListItemText, +} from '@mui/material'; +import React from 'react'; +import { ExpandLess, ExpandMore } from '@mui/icons-material'; +// eslint-disable-next-line import/no-cycle +import { Options } from '../SourceOptions'; + +interface Props { + state: ISourceFilters[] + name: string + position: number + updateFilterValue: Function +} + +export default function GroupFilter(props: Props) { + const { + state, + name, + position, + updateFilterValue, + } = props; + + const [open, setOpen] = React.useState(false); + + const handleClick = () => { + setOpen(!open); + }; + + return ( + <> + + + {open ? : } + + + + + + + + ); +} diff --git a/src/components/source/filters/HeaderFilter.tsx b/src/components/source/filters/HeaderFilter.tsx new file mode 100644 index 00000000..05b082ba --- /dev/null +++ b/src/components/source/filters/HeaderFilter.tsx @@ -0,0 +1,19 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import React from 'react'; +import { Box } from '@mui/system'; + +interface Props { + name: string +} + +export default function HeaderFilter(props: Props) { + const { name } = props; + return ({name}); +} diff --git a/src/components/source/filters/SelectFilter.tsx b/src/components/source/filters/SelectFilter.tsx new file mode 100644 index 00000000..046326fe --- /dev/null +++ b/src/components/source/filters/SelectFilter.tsx @@ -0,0 +1,150 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import React from 'react'; +import { Box } from '@mui/system'; +import InputLabel from '@mui/material/InputLabel'; +import FormControl from '@mui/material/FormControl'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; + +interface Props { + values: any + name: string + state: number + selected: Selected | undefined + position: number + updateFilterValue: Function + group: number | undefined +} + +interface Selected { + displayname: string + value: string + _value: string +} + +function hasSelect( + values: Selected[], + name: string, + state: number, + position: number, + updateFilterValue: Function, + group?: number, +) { + const [val, setval] = React.useState(state); + if (values) { + const handleChange = (event: { target: { name: any; value: any; }; }) => { + const vall = values.map((e) => e.displayname).indexOf(`${event.target.value}`); + setval(vall); + updateFilterValue({ position, state: vall.toString(), group }); + }; + + const rett = values.map((e: Selected) => ( + + { + e.displayname + } + + )); + return ( + + + + {name} + + + + + ); + } + return (<>); +} + +function noSelect( + values: string[], + name: string, + state: number, + position: number, + updateFilterValue: Function, + group?: number, +) { + const [val, setval] = React.useState(state); + + if (values) { + const handleChange = (event: { target: { name: any; value: any; }; }) => { + const vall = values.indexOf(`${event.target.value}`); + setval(vall); + updateFilterValue({ position, state: vall.toString(), group }); + }; + + const rett = values.map((value: string) => ({value})); + return ( + + + + {name} + + + + + ); + } + return (<>); +} + +export default function SelectFilter({ + values, + name, + state, + selected, + position, + updateFilterValue, + group, +}: Props) { + if (selected === undefined) { + return noSelect( + values, + name, + state, + position, + updateFilterValue, + group, + ); + } + + return hasSelect( + values, + name, + state, + position, + updateFilterValue, + group, + ); +} diff --git a/src/components/source/filters/SeparatorFilter.tsx b/src/components/source/filters/SeparatorFilter.tsx new file mode 100644 index 00000000..25cf8b78 --- /dev/null +++ b/src/components/source/filters/SeparatorFilter.tsx @@ -0,0 +1,18 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +import React from 'react'; +import { Box } from '@mui/system'; + +interface Props { + name: string +} + +export default function SeperatorFilter(props: Props) { + const { name } = props; + return ({name}); +} diff --git a/src/components/source/filters/SortFilter.tsx b/src/components/source/filters/SortFilter.tsx new file mode 100644 index 00000000..fd03d388 --- /dev/null +++ b/src/components/source/filters/SortFilter.tsx @@ -0,0 +1,95 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +import React from 'react'; +import { 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'; + +interface Props { + values: any + name: string + state: IState + position: number + group: number | undefined + updateFilterValue: Function +} + +export default function SortFilter(props: Props) { + const { + values, + name, + state, + position, + group, + updateFilterValue, + } = props; + const [val, setval] = React.useState(state); + + const [open, setOpen] = React.useState(false); + + const handleClick = () => { + setOpen(!open); + }; + + if (values) { + const handleChange = (event: + React.MouseEvent, index: number) => { + const tmp = val; + tmp.index = index; + tmp.ascending = !tmp.ascending; + setval(tmp); + updateFilterValue({ position, state: JSON.stringify(tmp), group }); + }; + + const ret = ( + + + + {open ? : } + + + + {values.map((value: string, index: number) => { + let icon; + if (val.index === index) { + icon = val.ascending ? () + : (); + } + return ( + + handleChange(event, index)} + > + + {icon} + + + + + ); + })} + + + + ); + return ( + + {ret} + + ); + } + return (<>); +} diff --git a/src/components/source/filters/TextFilter.tsx b/src/components/source/filters/TextFilter.tsx new file mode 100644 index 00000000..5b478d56 --- /dev/null +++ b/src/components/source/filters/TextFilter.tsx @@ -0,0 +1,72 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +import React from 'react'; +import { Box } from '@mui/system'; +import { + Input, + InputLabel, + FormControl, +} from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; + +interface Props { + state: string + name: string + position: number + group: number | undefined + updateFilterValue: Function +} + +export default function TextFilter(props: Props) { + const { + state, + name, + position, + group, + updateFilterValue, + } = props; + const [Search, setsearch] = React.useState(''); + let typingTimer: NodeJS.Timeout; + + function doneTyping(e: React.ChangeEvent) { + updateFilterValue({ position, state: e.target.value === '' ? '' : e.target.value.toString(), group }); + } + + function handleChange(e: React.ChangeEvent) { + setsearch(e.target.value === '' ? '' : e.target.value); + + clearTimeout(typingTimer); + typingTimer = setTimeout(() => { doneTyping(e); }, 2500); + } + + if (state !== undefined) { + return ( + + <> + + + + {name} + + + + + + ); + } + return (<>); +} diff --git a/src/components/source/filters/TriStateFilter.tsx b/src/components/source/filters/TriStateFilter.tsx new file mode 100644 index 00000000..92418ef6 --- /dev/null +++ b/src/components/source/filters/TriStateFilter.tsx @@ -0,0 +1,70 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +import React from 'react'; +import { Box } from '@mui/system'; +import { FormControlLabel } from '@mui/material'; +import ThreeStateCheckbox from 'components/util/ThreeStateCheckbox'; + +interface Props { + state: number + name: string + position: number + group: number | undefined + updateFilterValue: Function +} + +export default function TriStateFilter(props: Props) { + const { + state, + name, + position, + group, + updateFilterValue, + } = props; + const [val, setval] = React.useState({ + [name]: state, + }); + + const handleChange = (checked: boolean | null | undefined) => { + const tmp = val; + if (checked !== undefined) { + tmp[name] = checked ? 1 : 2; + } else { + delete tmp[name]; + } + setval({ + ...tmp, + }); + updateFilterValue({ + position, + state: (tmp[name] === undefined ? 0 : tmp[name]).toString(), + group, + }); + }; + + if (state !== undefined) { + let check; + if (val[name] !== 0) { + check = val[name] === 1; + } else { + check = undefined; + } + return ( + + handleChange(checked)} /> + )} + label={name} + /> + + ); + } + return (<>); +} diff --git a/src/screens/SearchSingle.tsx b/src/screens/SearchSingle.tsx deleted file mode 100644 index 1ccc86a3..00000000 --- a/src/screens/SearchSingle.tsx +++ /dev/null @@ -1,121 +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, { useContext, useEffect, useState } from 'react'; -import TextField from '@mui/material/TextField'; -import Button from '@mui/material/Button'; -import { useParams } from 'react-router-dom'; -import MangaGrid from 'components/MangaGrid'; -import NavbarContext from 'components/context/NavbarContext'; -import client from 'util/client'; -import { Box } from '@mui/system'; - -export default function SearchSingle() { - const { setTitle, setAction } = useContext(NavbarContext); - useEffect(() => { setTitle('Search'); setAction(<>); }, []); - - const { sourceId } = useParams<{ sourceId: string }>(); - const [error, setError] = useState(false); - const [mangas, setMangas] = useState([]); - const [message, setMessage] = useState(''); - const [searchTerm, setSearchTerm] = useState(''); - const [hasNextPage, setHasNextPage] = useState(false); - const [lastPageNum, setLastPageNum] = useState(1); - const [isLoading, setIsLoading] = useState(false); - - const textInput = React.createRef(); - - useEffect(() => { - client.get(`/api/v1/source/${sourceId}`) - .then((response) => response.data) - .then((data: { name: string }) => setTitle(`Search: ${data.name}`)); - }, []); - - function resetSearch() { - setIsLoading(true); - setMangas([]); - setLastPageNum(1); - } - - function processInput() { - if (textInput.current) { - const { value } = textInput.current; - if (value === '') { - setError(true); - } else { - setError(false); - setSearchTerm(value); - resetSearch(); - } - } - } - - useEffect(() => { - if (searchTerm.length > 0) { - client.get(`/api/v1/source/${sourceId}/search`, { params: { searchTerm, pageNum: lastPageNum } }) - .then((response) => response.data) - .then((data: { mangaList: IManga[], hasNextPage: boolean }) => { - if (data.mangaList.length > 0) { - setMangas([ - ...mangas, - ...data.mangaList.map((it) => ({ - title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id, - }))]); - setHasNextPage(data.hasNextPage); - } else { - setMessage('Search query returned nothing.'); - } - - setIsLoading(false); - }); - } - }, [searchTerm, lastPageNum]); - - return ( - <> - - e.key === 'Enter' && processInput()} - /> - - - {searchTerm.length > 0 - && ( - - )} - - ); -} diff --git a/src/screens/SourceMangas.tsx b/src/screens/SourceMangas.tsx index 73ca2581..0c56d1c1 100644 --- a/src/screens/SourceMangas.tsx +++ b/src/screens/SourceMangas.tsx @@ -8,10 +8,19 @@ import React, { useContext, useEffect, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import IconButton from '@mui/material/IconButton'; -import MangaGrid from 'components/MangaGrid'; +import SourceMangaGrid from 'components/source/SourceMangaGrid'; import NavbarContext from 'components/context/NavbarContext'; import client from 'util/client'; import SettingsIcon from '@mui/icons-material/Settings'; +import SourceOptions from 'components/source/SourceOptions'; +import AppbarSearch from 'components/util/AppbarSearch'; +import { useQueryParam, StringParam } from 'use-query-params'; + +interface IPos { + position: number + state: any + group?: number +} export default function SourceMangas(props: { popular: boolean }) { const { setTitle, setAction } = useContext(NavbarContext); @@ -24,28 +33,28 @@ export default function SourceMangas(props: { popular: boolean }) { const [lastPageNum, setLastPageNum] = useState(1); const [fetched, setFetched] = useState(false); + const [Search, setSearch] = useState(); + const [query, setquery] = useQueryParam('query', StringParam); + const [reset, setReset] = React.useState(2); + const [update, setUpdate] = React.useState(); + const [triggerUpdate, setTriggerUpdate] = useState(2); + const [Data, SetData] = useState(); + + const [Init, setInit] = useState(); + const [Noreset, setNoreset] = useQueryParam('R'); + + function makeFilters() { + client.get(`/api/v1/source/${sourceId}/filters`) + .then((response) => response.data) + .then((data: ISourceFilters[]) => { + SetData(data); + }); + } + useEffect(() => { setTitle('Source'); // title is later set after a fetch but we set it here once }, []); - useEffect(() => { - setAction( - <> - {isConfigurable && ( - history.push(`/sources/${sourceId}/configure/`)} - aria-label="display more actions" - edge="end" - color="inherit" - size="large" - > - - - )} - , - ); - }, [isConfigurable]); - useEffect(() => { client.get(`/api/v1/source/${sourceId}`) .then((response) => response.data) @@ -56,18 +65,113 @@ export default function SourceMangas(props: { popular: boolean }) { }, []); useEffect(() => { - const sourceType = props.popular ? 'popular' : 'latest'; - client.get(`/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`) - .then((response) => response.data) - .then((data: { mangaList: IManga[], hasNextPage: boolean }) => { - setMangas([ - ...mangas, - ...data.mangaList.map((it) => ({ - title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id, - }))]); - setHasNextPage(data.hasNextPage); - setFetched(true); - }); + if (triggerUpdate === 2) { + return; + } + if (triggerUpdate === 0) { + setTriggerUpdate(1); + return; + } + setFetched(false); + setMangas([]); + setLastPageNum(0); + if (Noreset === undefined && Search) { setNoreset(null); } + }, [triggerUpdate]); + + useEffect(() => { + if (update !== undefined) { + const { position, state, group }: IPos = update; + client.post(`/api/v1/source/${sourceId}/filters`, + JSON.stringify(group === undefined ? { + position, + state, + } : { + position: group, + state: JSON.stringify({ + position, + state, + }), + })) + .then(() => { + makeFilters(); + }); + } + }, [update]); + + useEffect(() => { + if (reset === 0) { + setquery(undefined); + setNoreset(undefined); + setReset(1); + } else if (Noreset === undefined) { + client.get(`/api/v1/source/${sourceId}/filters?reset=true`) + .then(() => { + makeFilters(); + setSearch(false); + if (reset === 1) { + setTriggerUpdate(0); + } + }); + return; + } + makeFilters(); + }, [reset]); + + useEffect(() => { + setAction( + <> + + {isConfigurable && ( + history.push(`/sources/${sourceId}/configure/`)} + aria-label="display more actions" + edge="end" + color="inherit" + size="large" + > + + + )} + + , + ); + }, [isConfigurable]); + + useEffect(() => { + if (query) { + setSearch(true); + } else { setSearch(false); } + if (Noreset === undefined) { setInit(null); } + }, [query]); + + useEffect(() => { + if (Search !== undefined && query !== undefined && Init === null) { + const delayDebounceFn = setTimeout(() => { + setTriggerUpdate(0); + }, 1000); + return () => clearTimeout(delayDebounceFn); + } + if (Search !== undefined) { setInit(null); } + return () => {}; + }, [Search, query]); + + useEffect(() => { + if (lastPageNum !== 0) { + const sourceType = props.popular ? 'popular' : 'latest'; + client.get(`/api/v1/source/${sourceId}/${query !== undefined || Search || Noreset === null ? 'search' : sourceType}${query !== undefined || Search || Noreset === null ? `?searchTerm=${query || ''}&pageNum=${lastPageNum}` : `/${lastPageNum}`}`) + .then((response) => response.data) + .then((data: { mangaList: IManga[], hasNextPage: boolean }) => { + setMangas([ + ...mangas, + ...data.mangaList.map((it) => ({ + title: it.title, + thumbnailUrl: it.thumbnailUrl, + id: it.id, + }))]); + setHasNextPage(data.hasNextPage); + setFetched(true); + }); + } else { setLastPageNum(1); } }, [lastPageNum]); let message; @@ -86,14 +190,25 @@ export default function SourceMangas(props: { popular: boolean }) { } return ( - + <> + + {Data !== undefined && ( + + )} + ); } diff --git a/src/typings.d.ts b/src/typings.d.ts index b47359cd..5a9cfaa5 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -30,6 +30,29 @@ interface ISource { displayName: string } +interface ISourceFilters { + type: string + filter: ISourceFilter +} + +interface ISourceFilter { + name: string + state: number | string | boolean | ISourceFilters[] | IState + values?: string[] + selected?: ISelected +} + +interface ISelected { + displayname: string + value: string + _value: string +} + +interface IState { + ascending: boolean + index: number +} + interface IMangaCard { id: number title: string diff --git a/src/util/useLibraryOptions.ts b/src/util/useLibraryOptions.ts index c18da98d..5bcafbe1 100644 --- a/src/util/useLibraryOptions.ts +++ b/src/util/useLibraryOptions.ts @@ -16,6 +16,7 @@ interface IUseLibraryOptions { query: NullAndUndefined setQuery: (query: NullAndUndefined) => void active: boolean + activeSort: boolean sorts: NullAndUndefined setSorts: (sorts: NullAndUndefined) => void sortDesc: NullAndUndefined @@ -31,6 +32,8 @@ export default function useLibraryOptions(): IUseLibraryOptions { // eslint-disable-next-line eqeqeq const active = !(unread == undefined) || !(downloaded == undefined); + // eslint-disable-next-line eqeqeq + const activeSort = (sortDesc != undefined) || (sorts != undefined); return { downloaded, setDownloaded, @@ -39,6 +42,7 @@ export default function useLibraryOptions(): IUseLibraryOptions { query, setQuery, active, + activeSort, sorts, setSorts, sortDesc,