Source filters, move search to SourceMangas (#142)
* source genre filter much the same as the library version * undo * base base + SelectFilter + CheckBoxFilter haven't done useQueryParam yet * gui done left to do: 1.saving the input data (so it don't disappear when closing the drawer) 2.post to update 3.reload list after drawer close * done ish * done im tired * ok, actually done now worked out the breaking that was happening in the last commit im probably using useState for more than is nessessary, only really needed to do triggerUpdate and Data * key error/warning fix * issues all done * remove junk * jank back button fix i re-used the query querystring * reove single search * boi was that a pain to fix * Update App.tsx
This commit is contained in:
@@ -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 */}
|
||||
|
||||
<Route path="/sources/:sourceId/search/">
|
||||
<SearchSingle />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/popular/">
|
||||
<SourceMangas popular />
|
||||
</Route>
|
||||
|
||||
@@ -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) {
|
||||
</Box>
|
||||
<>
|
||||
<MobileWidthButtons>
|
||||
<IconButton
|
||||
sx={{ width: 59, height: 59 }}
|
||||
onClick={(e) => redirectTo(e, `/sources/${id}/search/`)}
|
||||
size="large"
|
||||
edge="end"
|
||||
>
|
||||
<SearchIcon
|
||||
fontSize="medium"
|
||||
/>
|
||||
</IconButton>
|
||||
{supportsLatest && (
|
||||
<IconButton
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={(e) => redirectTo(e, `/sources/${id}/latest/`)}
|
||||
size="large"
|
||||
>
|
||||
<FiberNewOutlinedIcon
|
||||
fontSize="large"
|
||||
/>
|
||||
</IconButton>
|
||||
Latest
|
||||
</Button>
|
||||
)}
|
||||
</MobileWidthButtons>
|
||||
<WiderWidthButtons>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={(e) => redirectTo(e, `/sources/${id}/search/`)}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
{supportsLatest && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
||||
37
src/components/source/SourceMangaGrid.tsx
Normal file
37
src/components/source/SourceMangaGrid.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import MangaGrid, { IMangaGridProps } from 'components/MangaGrid';
|
||||
|
||||
const FILTERED_OUT_MESSAGE = 'There are no Manga matching this filter';
|
||||
|
||||
function filterManga(mangas: IMangaCard[]): IMangaCard[] {
|
||||
return mangas;
|
||||
}
|
||||
|
||||
export default function SourceMangaGrid(props: IMangaGridProps) {
|
||||
const {
|
||||
mangas, isLoading, hasNextPage, lastPageNum, setLastPageNum, message, messageExtra,
|
||||
} = props;
|
||||
|
||||
const filteredManga = filterManga(mangas);
|
||||
const showFilteredOutMessage = filteredManga.length === 0 && mangas.length > 0;
|
||||
|
||||
return (
|
||||
<MangaGrid
|
||||
mangas={filteredManga}
|
||||
isLoading={isLoading}
|
||||
hasNextPage={hasNextPage}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
message={showFilteredOutMessage ? FILTERED_OUT_MESSAGE : message}
|
||||
messageExtra={messageExtra}
|
||||
/>
|
||||
);
|
||||
}
|
||||
203
src/components/source/SourceOptions.tsx
Normal file
203
src/components/source/SourceOptions.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import {
|
||||
Drawer, Button, Fab,
|
||||
} from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import SelectFilter from './filters/SelectFilter';
|
||||
import CheckBoxFilter from './filters/CheckBoxFilter';
|
||||
import HeaderFilter from './filters/HeaderFilter';
|
||||
import SeperatorFilter from './filters/SeparatorFilter';
|
||||
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';
|
||||
|
||||
interface IFilters {
|
||||
sourceFilter: ISourceFilters[]
|
||||
updateFilterValue: Function
|
||||
group: number | undefined
|
||||
}
|
||||
|
||||
interface IFilters1 {
|
||||
sourceFilter: ISourceFilters[]
|
||||
updateFilterValue: Function
|
||||
resetFilterValue: Function
|
||||
setTriggerUpdate: Function
|
||||
setSearch: Function
|
||||
}
|
||||
|
||||
export function Options({
|
||||
sourceFilter,
|
||||
group,
|
||||
updateFilterValue,
|
||||
}: IFilters) {
|
||||
return (
|
||||
<Box key={`filters ${group}`}>
|
||||
{ sourceFilter.map((e: ISourceFilters, index) => {
|
||||
switch (e.type) {
|
||||
case 'CheckBox':
|
||||
return (
|
||||
<CheckBoxFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
state={e.filter.state as boolean}
|
||||
position={index}
|
||||
group={group}
|
||||
updateFilterValue={updateFilterValue}
|
||||
/>
|
||||
);
|
||||
case 'Group':
|
||||
return (
|
||||
<GroupFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
state={e.filter.state as ISourceFilters[]}
|
||||
position={index}
|
||||
updateFilterValue={updateFilterValue}
|
||||
/>
|
||||
);
|
||||
case 'Header':
|
||||
return (
|
||||
<HeaderFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
/>
|
||||
);
|
||||
case 'Select':
|
||||
return (
|
||||
<SelectFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
values={e.filter.values}
|
||||
state={e.filter.state as number}
|
||||
selected={e.filter.selected}
|
||||
position={index}
|
||||
group={group}
|
||||
updateFilterValue={updateFilterValue}
|
||||
/>
|
||||
);
|
||||
case 'Separator':
|
||||
return (
|
||||
<SeperatorFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
/>
|
||||
);
|
||||
case 'Sort':
|
||||
return (
|
||||
<SortFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
values={e.filter.values}
|
||||
state={e.filter.state as IState}
|
||||
position={index}
|
||||
group={group}
|
||||
updateFilterValue={updateFilterValue}
|
||||
/>
|
||||
);
|
||||
case 'Text':
|
||||
return (
|
||||
<TextFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
state={e.filter.state as string}
|
||||
position={index}
|
||||
group={group}
|
||||
updateFilterValue={updateFilterValue}
|
||||
/>
|
||||
);
|
||||
case 'TriState':
|
||||
return (
|
||||
<TriStateFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
state={e.filter.state as number}
|
||||
position={index}
|
||||
group={group}
|
||||
updateFilterValue={updateFilterValue}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (<Box key={`${e.filter.name}null`} />);
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SourceOptions({
|
||||
sourceFilter,
|
||||
updateFilterValue,
|
||||
resetFilterValue,
|
||||
setTriggerUpdate,
|
||||
setSearch,
|
||||
}: IFilters1) {
|
||||
const [FilterOptions, setFilterOptions] = React.useState(false);
|
||||
|
||||
function handleReset() {
|
||||
resetFilterValue(0);
|
||||
setFilterOptions(false);
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
setTriggerUpdate(0);
|
||||
setSearch(true);
|
||||
setFilterOptions(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Fab
|
||||
sx={{ position: 'fixed', bottom: '2em', right: '3em' }}
|
||||
onClick={() => setFilterOptions(!FilterOptions)}
|
||||
variant="extended"
|
||||
color="primary"
|
||||
>
|
||||
<FilterListIcon />
|
||||
Filter
|
||||
</Fab>
|
||||
|
||||
<Drawer
|
||||
anchor="bottom"
|
||||
open={FilterOptions}
|
||||
onClose={() => setFilterOptions(false)}
|
||||
PaperProps={{
|
||||
style: {
|
||||
maxWidth: 600, padding: '1em', marginLeft: 'auto', marginRight: 'auto',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ marginLeft: 'auto' }}
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Box>
|
||||
<Options
|
||||
sourceFilter={sourceFilter}
|
||||
updateFilterValue={updateFilterValue}
|
||||
group={undefined}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
53
src/components/source/filters/CheckBoxFilter.tsx
Normal file
53
src/components/source/filters/CheckBoxFilter.tsx
Normal file
@@ -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 (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}>
|
||||
<FormControlLabel
|
||||
key={name}
|
||||
control={(
|
||||
<Checkbox
|
||||
name={name}
|
||||
checked={val}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
label={name}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
}
|
||||
54
src/components/source/filters/GroupFilter.tsx
Normal file
54
src/components/source/filters/GroupFilter.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<ListItemButton onClick={handleClick}>
|
||||
<ListItemText primary={name} />
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemButton>
|
||||
<Collapse in={open}>
|
||||
<List disablePadding>
|
||||
<Options
|
||||
sourceFilter={state}
|
||||
group={position}
|
||||
updateFilterValue={updateFilterValue}
|
||||
/>
|
||||
</List>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
}
|
||||
19
src/components/source/filters/HeaderFilter.tsx
Normal file
19
src/components/source/filters/HeaderFilter.tsx
Normal file
@@ -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 (<Box key={name}>{name}</Box>);
|
||||
}
|
||||
150
src/components/source/filters/SelectFilter.tsx
Normal file
150
src/components/source/filters/SelectFilter.tsx
Normal file
@@ -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) => (
|
||||
<MenuItem
|
||||
key={`${name} ${e.displayname}`}
|
||||
value={e.displayname}
|
||||
>
|
||||
{
|
||||
e.displayname
|
||||
}
|
||||
</MenuItem>
|
||||
));
|
||||
return (
|
||||
<Box key={name} sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel sx={{ margin: '10px 0 10px 0' }}>
|
||||
{name}
|
||||
</InputLabel>
|
||||
<Select
|
||||
name={name}
|
||||
value={values[val].displayname}
|
||||
label={name}
|
||||
onChange={handleChange}
|
||||
autoWidth
|
||||
sx={{ margin: '10px 0 10px 0' }}
|
||||
>
|
||||
{rett}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
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) => (<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' }}>
|
||||
{name}
|
||||
</InputLabel>
|
||||
<Select
|
||||
name={name}
|
||||
value={values[val]}
|
||||
label={name}
|
||||
onChange={handleChange}
|
||||
autoWidth
|
||||
sx={{ margin: '10px 0 10px 0' }}
|
||||
>
|
||||
{rett}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
18
src/components/source/filters/SeparatorFilter.tsx
Normal file
18
src/components/source/filters/SeparatorFilter.tsx
Normal file
@@ -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 (<Box key={name}>{name}</Box>);
|
||||
}
|
||||
95
src/components/source/filters/SortFilter.tsx
Normal file
95
src/components/source/filters/SortFilter.tsx
Normal file
@@ -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<HTMLDivElement, MouseEvent>, index: number) => {
|
||||
const tmp = val;
|
||||
tmp.index = index;
|
||||
tmp.ascending = !tmp.ascending;
|
||||
setval(tmp);
|
||||
updateFilterValue({ position, state: JSON.stringify(tmp), group });
|
||||
};
|
||||
|
||||
const ret = (
|
||||
<FormControl fullWidth>
|
||||
<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>
|
||||
</Collapse>
|
||||
</FormControl>
|
||||
);
|
||||
return (
|
||||
<Box key={name} sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}>
|
||||
{ret}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
}
|
||||
72
src/components/source/filters/TextFilter.tsx
Normal file
72
src/components/source/filters/TextFilter.tsx
Normal file
@@ -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<HTMLInputElement>) {
|
||||
updateFilterValue({ position, state: e.target.value === '' ? '' : e.target.value.toString(), group });
|
||||
}
|
||||
|
||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
setsearch(e.target.value === '' ? '' : e.target.value);
|
||||
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(() => { doneTyping(e); }, 2500);
|
||||
}
|
||||
|
||||
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' }}>
|
||||
{name}
|
||||
</InputLabel>
|
||||
<Input
|
||||
name={name}
|
||||
value={Search || ''}
|
||||
onChange={handleChange}
|
||||
sx={{ margin: '10px 0 10px 0' }}
|
||||
/>
|
||||
</FormControl>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
}
|
||||
70
src/components/source/filters/TriStateFilter.tsx
Normal file
70
src/components/source/filters/TriStateFilter.tsx
Normal file
@@ -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 (
|
||||
<Box sx={{ marginLeft: 3 }}>
|
||||
<FormControlLabel
|
||||
key={name}
|
||||
control={(
|
||||
<ThreeStateCheckbox name="Unread" checked={check} onChange={(checked) => handleChange(checked)} />
|
||||
)}
|
||||
label={name}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
}
|
||||
@@ -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<boolean>(false);
|
||||
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const textInput = React.createRef<HTMLInputElement>();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Box sx={{
|
||||
margin: '20px 10px',
|
||||
display: 'flex',
|
||||
width: '400px',
|
||||
maxWidth: 'calc(100% - 20px)',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
inputRef={textInput}
|
||||
error={error}
|
||||
sx={{
|
||||
mx: 1,
|
||||
flexGrow: 1,
|
||||
}}
|
||||
id="filled-basic"
|
||||
variant="filled"
|
||||
size="small"
|
||||
label="Search text.."
|
||||
onKeyDown={(e) => e.key === 'Enter' && processInput()}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => processInput()}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</Box>
|
||||
{searchTerm.length > 0
|
||||
&& (
|
||||
<MangaGrid
|
||||
mangas={mangas}
|
||||
message={message}
|
||||
hasNextPage={hasNextPage}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<number>(1);
|
||||
const [fetched, setFetched] = useState<boolean>(false);
|
||||
|
||||
const [Search, setSearch] = useState<boolean>();
|
||||
const [query, setquery] = useQueryParam('query', StringParam);
|
||||
const [reset, setReset] = React.useState(2);
|
||||
const [update, setUpdate] = React.useState();
|
||||
const [triggerUpdate, setTriggerUpdate] = useState<number>(2);
|
||||
const [Data, SetData] = useState<ISourceFilters[]>();
|
||||
|
||||
const [Init, setInit] = useState<undefined | null>();
|
||||
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 && (
|
||||
<IconButton
|
||||
onClick={() => history.push(`/sources/${sourceId}/configure/`)}
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
size="large"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</>,
|
||||
);
|
||||
}, [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(
|
||||
<>
|
||||
<AppbarSearch />
|
||||
{isConfigurable && (
|
||||
<IconButton
|
||||
onClick={() => history.push(`/sources/${sourceId}/configure/`)}
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
size="large"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
,
|
||||
);
|
||||
}, [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 (
|
||||
<MangaGrid
|
||||
mangas={mangas}
|
||||
hasNextPage={hasNextPage}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
message={message}
|
||||
messageExtra={messageExtra}
|
||||
isLoading={!fetched}
|
||||
/>
|
||||
<>
|
||||
<SourceMangaGrid
|
||||
mangas={mangas}
|
||||
hasNextPage={hasNextPage}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
message={message}
|
||||
messageExtra={messageExtra}
|
||||
isLoading={!fetched}
|
||||
/>
|
||||
{Data !== undefined && (
|
||||
<SourceOptions
|
||||
sourceFilter={Data}
|
||||
updateFilterValue={setUpdate}
|
||||
resetFilterValue={setReset}
|
||||
setTriggerUpdate={setTriggerUpdate}
|
||||
setSearch={setSearch}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
23
src/typings.d.ts
vendored
23
src/typings.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -16,6 +16,7 @@ interface IUseLibraryOptions {
|
||||
query: NullAndUndefined<string>
|
||||
setQuery: (query: NullAndUndefined<string>) => void
|
||||
active: boolean
|
||||
activeSort: boolean
|
||||
sorts: NullAndUndefined<string>
|
||||
setSorts: (sorts: NullAndUndefined<string>) => void
|
||||
sortDesc: NullAndUndefined<boolean>
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user