Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor ChapterOptions #126

Merged
merged 7 commits into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 19 additions & 95 deletions src/components/chapter/ChapterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
* 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, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { Box, styled } from '@mui/system';
import { Virtuoso } from 'react-virtuoso';
import Typography from '@mui/material/Typography';
import { CircularProgress, Fab } from '@mui/material';
import { Link } from 'react-router-dom';
import { CircularProgress, Stack } from '@mui/material';
import makeToast from 'components/util/Toast';
import PlayArrow from '@mui/icons-material/PlayArrow';
import ChapterOptions from 'components/chapter/ChapterOptions';
import ChapterCard from 'components/chapter/ChapterCard';
import useLocalStorage from 'util/useLocalStorage';
import { useReducerLocalStorage } from 'util/useLocalStorage';
import {
chapterOptionsReducer, defaultChapterOptions, findFirstUnreadChapter,
filterAndSortChapters,
} from 'components/chapter/util';
import ResumeFab from 'components/chapter/ResumeFAB';
import useFetchChapters from './useFetchChapters';

const CustomVirtuoso = styled(Virtuoso)(({ theme }) => ({
Expand All @@ -40,65 +43,15 @@ interface IProps {
id: string
}

function unreadFilter(unread: NullAndUndefined<boolean>, { read: isChapterRead }: IChapter) {
switch (unread) {
case true:
return !isChapterRead;
case false:
return isChapterRead;
default:
return true;
}
}

function downloadFilter(downloaded: NullAndUndefined<boolean>,
{ downloaded: chapterDownload }: IChapter) {
switch (downloaded) {
case true:
return chapterDownload;
case false:
return !chapterDownload;
default:
return true;
}
}

function bookmarkdFilter(bookmarked: NullAndUndefined<boolean>,
{ bookmarked: chapterBookmarked }: IChapter) {
switch (bookmarked) {
case true:
return chapterBookmarked;
case false:
return !chapterBookmarked;
default:
return true;
}
}

function findFirstUnreadChapter(chapters: IChapter[]): IChapter | undefined {
for (let index = chapters.length - 1; index >= 0; index--) {
if (!chapters[index].read) return chapters[index];
}
return undefined;
}

export default function ChapterList(props: IProps) {
const { id } = props;

const [chapters, triggerChaptersUpdate, noChaptersFound] = useFetchChapters(id);
const [firstUnreadChapter, setFirstUnreadChapter] = useState<IChapter>();
const [filteredChapters, setFilteredChapters] = useState<IChapter[]>([]);
const [options, setOptions] = useLocalStorage<ChapterListOptions>(
`${id}filterOptions`,
{
active: false,
unread: undefined,
downloaded: undefined,
bookmarked: undefined,
reverse: false,
sortBy: 'source',
showChapterNumber: false,
},
// eslint-disable-next-line max-len
const [options, optionsDispatch] = useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>(
chapterOptionsReducer, `${id}filterOptions`, defaultChapterOptions,
);

const [, setWsClient] = useState<WebSocket>();
Expand All @@ -120,7 +73,7 @@ export default function ChapterList(props: IProps) {
triggerChaptersUpdate();
}, [queue.length]);

const downloadStatusStringFor = (chapter: IChapter) => {
const downloadStatusStringFor = useCallback((chapter: IChapter) => {
let rtn = '';
if (chapter.downloaded) {
rtn = ' • Downloaded';
Expand All @@ -131,39 +84,14 @@ export default function ChapterList(props: IProps) {
}
});
return rtn;
};
}, [queue]);

useEffect(() => {
const filtered = options.active
? chapters.filter((chp) => unreadFilter(options.unread, chp)
&& downloadFilter(options.downloaded, chp)
&& bookmarkdFilter(options.bookmarked, chp))
: [...chapters];
const Sorted = options.sortBy === 'fetchedAt'
? filtered.sort((a, b) => a.fetchedAt - b.fetchedAt)
: filtered;
if (options.reverse) {
Sorted.reverse();
}
setFilteredChapters(Sorted);

const filtered = filterAndSortChapters(chapters, options);
setFilteredChapters(filtered);
setFirstUnreadChapter(findFirstUnreadChapter(filtered));
}, [options, chapters]);

const ResumeFab = () => (firstUnreadChapter === undefined ? null
: (
<Fab
sx={{ position: 'fixed', bottom: '2em', right: '3em' }}
component={Link}
variant="extended"
color="primary"
to={`/manga/${id}/chapter/${firstUnreadChapter.index}/page/${firstUnreadChapter.lastPageRead}`}
>
<PlayArrow />
{firstUnreadChapter.index === 1 ? 'Start' : 'Resume' }
</Fab>
));

useEffect(() => {
if (noChaptersFound) {
makeToast('No chapters found', 'warning');
Expand All @@ -185,19 +113,15 @@ export default function ChapterList(props: IProps) {

return (
<>
<Box sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Stack direction="column">
<Box sx={{
display: 'flex', justifyContent: 'space-between', px: 1.5, mt: 1,
}}
>
<Typography variant="h5">
{`${filteredChapters.length} Chapters`}
</Typography>
<ChapterOptions options={options} setOptions={setOptions} />
<ChapterOptions options={options} optionsDispatch={optionsDispatch} />
</Box>

<CustomVirtuoso
Expand All @@ -218,8 +142,8 @@ export default function ChapterList(props: IProps) {
useWindowScroll={window.innerWidth < 900}
overscan={window.innerHeight * 0.5}
/>
</Box>
<ResumeFab />
</Stack>
{firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={id} />}
</>
);
}
96 changes: 30 additions & 66 deletions src/components/chapter/ChapterOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* 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 React, { useState, useCallback } from 'react';
import FilterListIcon from '@mui/icons-material/FilterList';
import {
Drawer, FormControlLabel, IconButton, Typography, Tab, Tabs, Radio, RadioGroup,
Drawer, FormControlLabel, IconButton, Typography, Tab, Tabs, Radio, RadioGroup, Stack,
} from '@mui/material';
import ThreeStateCheckbox from 'components/util/ThreeStateCheckbox';
import { Box } from '@mui/system';
Expand All @@ -17,51 +17,21 @@ import TabPanel from 'components/util/TabPanel';

interface IProps{
options: ChapterListOptions
setOptions: React.Dispatch<React.SetStateAction<ChapterListOptions>>
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>
}

const SortTab: [ChapterSortMode, string][] = [['source', 'By Source'], ['fetchedAt', 'By Fetch date']];

export default function ChapterOptions(props: IProps) {
const { options, setOptions } = props;
const { options, optionsDispatch } = props;
const [filtersOpen, setFiltersOpen] = useState(false);
const [tabNum, setTabNum] = useState(0);

const setUnread = (newUnread: NullAndUndefined<boolean>) => {
const active = options.unread !== false
&& options.downloaded !== false
&& options.bookmarked !== false;
setOptions({ ...options, active, unread: newUnread });
};

const setDownloaded = (newDownloaded: NullAndUndefined<boolean>) => {
const active = options.unread !== false
&& options.downloaded !== false
&& options.bookmarked !== false;
setOptions({ ...options, active, downloaded: newDownloaded });
};

const setBookmarked = (newBookmarked: NullAndUndefined<boolean>) => {
const active = options.unread !== false
&& options.downloaded !== false
&& options.bookmarked !== false;
setOptions({ ...options, active, bookmarked: newBookmarked });
};

const setSort = (newSort: ChapterSortMode) => {
if (newSort !== options.sortBy) {
setOptions({ ...options, sortBy: newSort });
} else {
setOptions({ ...options, reverse: !options.reverse });
}
};

const handleDisplay = (e: React.ChangeEvent<HTMLInputElement>) => {
const showChapterNumber = e.target.value === 'chapterNumber';
if (showChapterNumber !== options.showChapterNumber) {
setOptions({ ...options, showChapterNumber });
}
};
const filterOptions = useCallback(
(value: NullAndUndefined<boolean>, name: string) => {
optionsDispatch({ type: 'filter', filterType: name.toLowerCase(), filterValue: value });
}, [],
);

return (
<>
Expand Down Expand Up @@ -101,51 +71,45 @@ export default function ChapterOptions(props: IProps) {
</Tabs>
<TabPanel index={0} currentIndex={tabNum}>
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '150px' }}>
<FormControlLabel control={<ThreeStateCheckbox name="Unread" checked={options.unread} onChange={setUnread} />} label="Unread" />
<FormControlLabel control={<ThreeStateCheckbox name="Downloaded" checked={options.downloaded} onChange={setDownloaded} />} label="Downloaded" />
<FormControlLabel control={<ThreeStateCheckbox name="Bookmarked" checked={options.bookmarked} onChange={setBookmarked} />} label="Bookmarked" />
<FormControlLabel control={<ThreeStateCheckbox name="Unread" checked={options.unread} onChange={filterOptions} />} label="Unread" />
<FormControlLabel control={<ThreeStateCheckbox name="Downloaded" checked={options.downloaded} onChange={filterOptions} />} label="Downloaded" />
<FormControlLabel control={<ThreeStateCheckbox name="Bookmarked" checked={options.bookmarked} onChange={filterOptions} />} label="Bookmarked" />
</Box>
</TabPanel>
<TabPanel index={1} currentIndex={tabNum}>
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '150px' }}>
{
SortTab.map((item) => (
<Box
onClick={() => setSort(item[0])}
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
height: 42,
py: 1,
}}
<Stack
direction="row"
alignItems="center"
spacing="2"
sx={{ py: 1, height: 42 }}
onClick={() => (item[0] !== options.sortBy
? optionsDispatch({ type: 'sortBy', sortBy: item[0] })
: optionsDispatch({ type: 'sortReverse' }))}
>
<Box sx={{
height: 24,
width: 24,
}}
>
{options.sortBy === item[0]
&& (options.reverse ? (
<ArrowUpward color="primary" />
) : (
<ArrowDownward color="primary" />
))}
<Box sx={{ height: 24, width: 24 }}>
{
options.sortBy === item[0]
&& (options.reverse
? (<ArrowUpward color="primary" />) : (<ArrowDownward color="primary" />))
}
</Box>
<Typography>{item[1]}</Typography>
</Box>
</Stack>

))
}
</Box>
</TabPanel>
<TabPanel index={2} currentIndex={tabNum}>
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '150px' }}>
<RadioGroup name="chapter-title-display" onChange={handleDisplay} value={options.showChapterNumber}>
<Stack flexDirection="column" sx={{ minHeight: '150px' }}>
<RadioGroup name="chapter-title-display" onChange={() => optionsDispatch({ type: 'showChapterNumber' })} value={options.showChapterNumber}>
<FormControlLabel label="By Source Title" value="title" control={<Radio checked={!options.showChapterNumber} />} />
<FormControlLabel label="By Chapter Number" value="chapterNumber" control={<Radio checked={options.showChapterNumber} />} />
</RadioGroup>
</Box>
</Stack>
</TabPanel>
</Box>
</Drawer>
Expand Down
32 changes: 32 additions & 0 deletions src/components/chapter/ResumeFAB.tsx
Original file line number Diff line number Diff line change
@@ -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 React from 'react';
import { Fab } from '@mui/material';
import { Link } from 'react-router-dom';
import { PlayArrow } from '@mui/icons-material';

interface ResumeFABProps{
chapter: IChapter
mangaId: string
}

export default function ResumeFab(props: ResumeFABProps) {
const { chapter: { index, lastPageRead }, mangaId } = props;
return (
<Fab
sx={{ position: 'fixed', bottom: '2em', right: '3em' }}
component={Link}
variant="extended"
color="primary"
to={`/manga/${mangaId}/chapter/${index}/page/${lastPageRead}`}
>
<PlayArrow />
{index === 1 ? 'Start' : 'Resume' }
</Fab>
);
}
Loading