add prettier for auto formatting (#231)
* [Prettier] Add "prettier" Makes the formatting of the code consistent. By using the eslint-plugin-prettier formatting issues will be highlighted as a lint error. These errors will be auto fixed by running "lint --fix" (which can be done automatically on file save) * [Prettier] Add "prettier" - Fix formatting
This commit is contained in:
@@ -15,9 +15,7 @@ import DoneAll from '@mui/icons-material/DoneAll';
|
||||
import Download from '@mui/icons-material/Download';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import RemoveDone from '@mui/icons-material/RemoveDone';
|
||||
import {
|
||||
CardActionArea, Checkbox, ListItemIcon, ListItemText, Stack,
|
||||
} from '@mui/material';
|
||||
import { CardActionArea, Checkbox, ListItemIcon, ListItemText, Stack } from '@mui/material';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
@@ -33,19 +31,24 @@ import { BACK } from 'util/useBackTo';
|
||||
import { getUploadDateString } from 'util/date';
|
||||
|
||||
interface IProps {
|
||||
chapter: IChapter
|
||||
triggerChaptersUpdate: () => void
|
||||
downloadChapter: IDownloadChapter | undefined
|
||||
showChapterNumber: boolean
|
||||
onSelect: (selected: boolean) => void
|
||||
selected: boolean | null
|
||||
chapter: IChapter;
|
||||
triggerChaptersUpdate: () => void;
|
||||
downloadChapter: IDownloadChapter | undefined;
|
||||
showChapterNumber: boolean;
|
||||
onSelect: (selected: boolean) => void;
|
||||
selected: boolean | null;
|
||||
}
|
||||
|
||||
const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
chapter, triggerChaptersUpdate, downloadChapter: dc, showChapterNumber, onSelect, selected,
|
||||
chapter,
|
||||
triggerChaptersUpdate,
|
||||
downloadChapter: dc,
|
||||
showChapterNumber,
|
||||
onSelect,
|
||||
selected,
|
||||
} = props;
|
||||
const isSelecting = selected !== null;
|
||||
|
||||
@@ -71,7 +74,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
if (key === 'read') {
|
||||
formData.append('lastPageRead', '1');
|
||||
}
|
||||
client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData)
|
||||
client
|
||||
.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData)
|
||||
.then(() => triggerChaptersUpdate());
|
||||
};
|
||||
|
||||
@@ -81,7 +85,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
};
|
||||
|
||||
const deleteChapter = () => {
|
||||
client.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`)
|
||||
client
|
||||
.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`)
|
||||
.then(() => triggerChaptersUpdate());
|
||||
handleClose();
|
||||
};
|
||||
@@ -112,7 +117,10 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
>
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={{ pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`, state: { backLink: BACK } }}
|
||||
to={{
|
||||
pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`,
|
||||
state: { backLink: BACK },
|
||||
}}
|
||||
style={{
|
||||
color: theme.palette.text[chapter.read ? 'disabled' : 'primary'],
|
||||
}}
|
||||
@@ -130,13 +138,16 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
<Stack direction="column" flex={1}>
|
||||
<Typography variant="h5" component="h2">
|
||||
{chapter.bookmarked && (
|
||||
<BookmarkIcon color="primary" sx={{ mr: 0.5, position: 'relative', top: '0.15em' }} />
|
||||
<BookmarkIcon
|
||||
color="primary"
|
||||
sx={{ mr: 0.5, position: 'relative', top: '0.15em' }}
|
||||
/>
|
||||
)}
|
||||
{showChapterNumber ? `Chapter ${chapter.chapterNumber}` : chapter.name}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
{chapter.scanlator}
|
||||
{showChapterNumber
|
||||
? `Chapter ${chapter.chapterNumber}`
|
||||
: chapter.name}
|
||||
</Typography>
|
||||
<Typography variant="caption">{chapter.scanlator}</Typography>
|
||||
<Typography variant="caption">
|
||||
{getUploadDateString(chapter.uploadDate)}
|
||||
{isDownloaded && ' • Downloaded'}
|
||||
@@ -164,18 +175,14 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
<ListItemIcon>
|
||||
<CheckBoxOutlineBlank fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Select
|
||||
</ListItemText>
|
||||
<ListItemText>Select</ListItemText>
|
||||
</MenuItem>
|
||||
{isDownloaded && (
|
||||
<MenuItem onClick={deleteChapter}>
|
||||
<ListItemIcon>
|
||||
<Delete fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Delete
|
||||
</ListItemText>
|
||||
<ListItemText>Delete</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{canBeDownloaded && (
|
||||
@@ -183,9 +190,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
<ListItemIcon>
|
||||
<Download fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Download
|
||||
</ListItemText>
|
||||
<ListItemText>Download</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
||||
@@ -212,9 +217,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
<ListItemIcon>
|
||||
<DoneAll fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Mark previous as Read
|
||||
</ListItemText>
|
||||
<ListItemText>Mark previous as Read</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Card>
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
* 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 {
|
||||
Button, CircularProgress, Stack,
|
||||
} from '@mui/material';
|
||||
import { Button, CircularProgress, Stack } from '@mui/material';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { styled } from '@mui/system';
|
||||
import useSubscription from 'components/library/useSubscription';
|
||||
@@ -17,10 +15,7 @@ import { filterAndSortChapters, useChapterOptions } from 'components/manga/util'
|
||||
import EmptyView from 'components/util/EmptyView';
|
||||
import { interpolate } from 'components/util/helpers';
|
||||
import makeToast from 'components/util/Toast';
|
||||
import React, {
|
||||
ComponentProps,
|
||||
useEffect, useMemo, useRef, useState,
|
||||
} from 'react';
|
||||
import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import client, { useQuery } from 'util/client';
|
||||
import ChaptersToolbarMenu from 'components/manga/ChaptersToolbarMenu';
|
||||
@@ -66,13 +61,13 @@ const actionsStrings = {
|
||||
};
|
||||
|
||||
export interface IChapterWithMeta {
|
||||
chapter: IChapter
|
||||
downloadChapter: IDownloadChapter | undefined
|
||||
selected: boolean | null
|
||||
chapter: IChapter;
|
||||
downloadChapter: IDownloadChapter | undefined;
|
||||
selected: boolean | null;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
mangaId: string
|
||||
mangaId: string;
|
||||
}
|
||||
|
||||
const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
@@ -92,9 +87,9 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
if (prevQueueRef.current && queue) {
|
||||
const prevQueue = prevQueueRef.current;
|
||||
const changedDownloads = queue.filter((cd) => {
|
||||
const prevChapterDownload = prevQueue
|
||||
.find((pcd) => cd.chapterIndex === pcd.chapterIndex
|
||||
&& cd.mangaId === pcd.mangaId);
|
||||
const prevChapterDownload = prevQueue.find(
|
||||
(pcd) => cd.chapterIndex === pcd.chapterIndex && cd.mangaId === pcd.mangaId,
|
||||
);
|
||||
if (!prevChapterDownload) return true;
|
||||
return cd.state !== prevChapterDownload.state;
|
||||
});
|
||||
@@ -107,13 +102,19 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
prevQueueRef.current = queue;
|
||||
}, [queue]);
|
||||
|
||||
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), //
|
||||
[chapters, options]);
|
||||
const visibleChapters = useMemo(
|
||||
() => filterAndSortChapters(chapters, options), //
|
||||
[chapters, options],
|
||||
);
|
||||
|
||||
const firstUnreadChapter = useMemo(() => visibleChapters.slice()
|
||||
.reverse()
|
||||
.find((c) => c.read === false),
|
||||
[visibleChapters]);
|
||||
const firstUnreadChapter = useMemo(
|
||||
() =>
|
||||
visibleChapters
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((c) => c.read === false),
|
||||
[visibleChapters],
|
||||
);
|
||||
|
||||
const handleSelection = (index: number) => {
|
||||
const chapter = visibleChapters[index];
|
||||
@@ -139,7 +140,10 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
setSelection(null);
|
||||
};
|
||||
|
||||
const handleFabAction: ComponentProps<typeof SelectionFAB>['onAction'] = (action, actionChapters) => {
|
||||
const handleFabAction: ComponentProps<typeof SelectionFAB>['onAction'] = (
|
||||
action,
|
||||
actionChapters,
|
||||
) => {
|
||||
if (actionChapters.length === 0) return;
|
||||
const chapterIds = actionChapters.map(({ chapter }) => chapter.id);
|
||||
|
||||
@@ -160,18 +164,26 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
}
|
||||
|
||||
actionPromise
|
||||
.then(() => makeToast(interpolate(chapterIds.length, actionsStrings[action].success), 'success'))
|
||||
.then(() =>
|
||||
makeToast(
|
||||
interpolate(chapterIds.length, actionsStrings[action].success),
|
||||
'success',
|
||||
),
|
||||
)
|
||||
.then(() => mutate())
|
||||
.catch(() => makeToast(interpolate(chapterIds.length, actionsStrings[action].error), 'error'));
|
||||
.catch(() =>
|
||||
makeToast(interpolate(chapterIds.length, actionsStrings[action].error), 'error'),
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{
|
||||
margin: '10px auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
<div
|
||||
style={{
|
||||
margin: '10px auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<CircularProgress thickness={5} />
|
||||
</div>
|
||||
@@ -183,8 +195,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
|
||||
const chaptersWithMeta: IChapterWithMeta[] = visibleChapters.map((chapter) => {
|
||||
const downloadChapter = queue?.find(
|
||||
(cd) => cd.chapterIndex === chapter.index
|
||||
&& cd.mangaId === chapter.mangaId,
|
||||
(cd) => cd.chapterIndex === chapter.index && cd.mangaId === chapter.mangaId,
|
||||
);
|
||||
const selected = selection?.includes(chapter.id) ?? null;
|
||||
return {
|
||||
@@ -194,9 +205,10 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
};
|
||||
});
|
||||
|
||||
const selectedChapters = (selection === null)
|
||||
? null
|
||||
: chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id));
|
||||
const selectedChapters =
|
||||
selection === null
|
||||
? null
|
||||
: chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id));
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -206,38 +218,44 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
sx={{
|
||||
m: 1, mb: 0, mr: 2, minHeight: 40,
|
||||
m: 1,
|
||||
mb: 0,
|
||||
mr: 2,
|
||||
minHeight: 40,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{`${visibleChapters.length} Chapter${visibleChapters.length === 1 ? '' : 's'}`}
|
||||
{`${visibleChapters.length} Chapter${
|
||||
visibleChapters.length === 1 ? '' : 's'
|
||||
}`}
|
||||
</Typography>
|
||||
|
||||
{selection === null ? (
|
||||
<ChaptersToolbarMenu options={options} optionsDispatch={dispatch} />
|
||||
) : (
|
||||
<Stack direction="row">
|
||||
<Button size="small" onClick={handleSelectAll}>Select all</Button>
|
||||
<Button size="small" onClick={handleClear}>Clear</Button>
|
||||
<Button size="small" onClick={handleSelectAll}>
|
||||
Select all
|
||||
</Button>
|
||||
<Button size="small" onClick={handleClear}>
|
||||
Clear
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{noChaptersFound && (
|
||||
<EmptyView message="No chapters found" />
|
||||
)}
|
||||
{noChaptersMatchingFilter && (
|
||||
<EmptyView message="No chapters matching filter" />
|
||||
)}
|
||||
{noChaptersFound && <EmptyView message="No chapters found" />}
|
||||
{noChaptersMatchingFilter && <EmptyView message="No chapters matching filter" />}
|
||||
|
||||
<StyledVirtuoso
|
||||
style={{ // override Virtuoso default values and set them with class
|
||||
style={{
|
||||
// override Virtuoso default values and set them with class
|
||||
height: 'undefined',
|
||||
// 900 is the md breakpoint in MUI
|
||||
overflowY: window.innerWidth < 900 ? 'visible' : 'auto',
|
||||
}}
|
||||
totalCount={visibleChapters.length}
|
||||
itemContent={(index:number) => (
|
||||
itemContent={(index: number) => (
|
||||
<ChapterCard
|
||||
{...chaptersWithMeta[index]}
|
||||
showChapterNumber={options.showChapterNumber}
|
||||
@@ -250,10 +268,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
/>
|
||||
</Stack>
|
||||
{selectedChapters !== null ? (
|
||||
<SelectionFAB
|
||||
selectedChapters={selectedChapters}
|
||||
onAction={handleFabAction}
|
||||
/>
|
||||
<SelectionFAB selectedChapters={selectedChapters} onAction={handleFabAction} />
|
||||
) : (
|
||||
firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={mangaId} />
|
||||
)}
|
||||
|
||||
@@ -14,10 +14,10 @@ import React from 'react';
|
||||
import { SORT_OPTIONS } from 'components/manga/util';
|
||||
|
||||
interface IProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
options: ChapterListOptions
|
||||
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
options: ChapterListOptions;
|
||||
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
|
||||
}
|
||||
|
||||
const TITLES = {
|
||||
@@ -26,9 +26,7 @@ const TITLES = {
|
||||
display: 'Display',
|
||||
};
|
||||
|
||||
const ChapterOptions: React.FC<IProps> = ({
|
||||
open, onClose, options, optionsDispatch,
|
||||
}) => (
|
||||
const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispatch }) => (
|
||||
<OptionsTabs<'filter' | 'sort' | 'display'>
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
@@ -39,9 +37,39 @@ const ChapterOptions: React.FC<IProps> = ({
|
||||
if (key === 'filter') {
|
||||
return (
|
||||
<>
|
||||
<ThreeStateCheckboxInput label="Unread" checked={options.unread} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'unread', filterValue: c })} />
|
||||
<ThreeStateCheckboxInput label="Downloaded" checked={options.downloaded} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'downloaded', filterValue: c })} />
|
||||
<ThreeStateCheckboxInput label="Bookmarked" checked={options.bookmarked} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'bookmarked', filterValue: c })} />
|
||||
<ThreeStateCheckboxInput
|
||||
label="Unread"
|
||||
checked={options.unread}
|
||||
onChange={(c) =>
|
||||
optionsDispatch({
|
||||
type: 'filter',
|
||||
filterType: 'unread',
|
||||
filterValue: c,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label="Downloaded"
|
||||
checked={options.downloaded}
|
||||
onChange={(c) =>
|
||||
optionsDispatch({
|
||||
type: 'filter',
|
||||
filterType: 'downloaded',
|
||||
filterValue: c,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label="Bookmarked"
|
||||
checked={options.bookmarked}
|
||||
onChange={(c) =>
|
||||
optionsDispatch({
|
||||
type: 'filter',
|
||||
filterType: 'bookmarked',
|
||||
filterValue: c,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -52,15 +80,20 @@ const ChapterOptions: React.FC<IProps> = ({
|
||||
label={label}
|
||||
checked={options.sortBy === mode}
|
||||
sortDescending={options.reverse}
|
||||
onClick={() => (mode !== options.sortBy
|
||||
? optionsDispatch({ type: 'sortBy', sortBy: mode })
|
||||
: optionsDispatch({ type: 'sortReverse' }))}
|
||||
onClick={() =>
|
||||
mode !== options.sortBy
|
||||
? optionsDispatch({ type: 'sortBy', sortBy: mode })
|
||||
: optionsDispatch({ type: 'sortReverse' })
|
||||
}
|
||||
/>
|
||||
));
|
||||
}
|
||||
if (key === 'display') {
|
||||
return (
|
||||
<RadioGroup onChange={() => optionsDispatch({ type: 'showChapterNumber' })} value={options.showChapterNumber}>
|
||||
<RadioGroup
|
||||
onChange={() => optionsDispatch({ type: 'showChapterNumber' })}
|
||||
value={options.showChapterNumber}
|
||||
>
|
||||
<RadioInput label="Source Title" value={false} />
|
||||
<RadioInput label="Chapter Number" value />
|
||||
</RadioGroup>
|
||||
|
||||
@@ -12,8 +12,8 @@ import ChapterOptions from 'components/manga/ChapterOptions';
|
||||
import { isFilterActive } from 'components/manga/util';
|
||||
|
||||
interface IProps {
|
||||
options: ChapterListOptions
|
||||
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>
|
||||
options: ChapterListOptions;
|
||||
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
|
||||
}
|
||||
|
||||
const ChaptersToolbarMenu = ({ options, optionsDispatch }: IProps) => {
|
||||
|
||||
@@ -17,102 +17,103 @@ import { mutate } from 'swr';
|
||||
import client from 'util/client';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
|
||||
const useStyles = (inLibrary: boolean) => makeStyles((theme: Theme) => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
position: 'sticky',
|
||||
top: '64px',
|
||||
left: '0px',
|
||||
width: '50vw',
|
||||
height: 'calc(100vh - 64px)',
|
||||
alignSelf: 'flex-start',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
top: {
|
||||
padding: '10px',
|
||||
// [theme.breakpoints.up('md')]: {
|
||||
// minWidth: '50%',
|
||||
// },
|
||||
},
|
||||
leftRight: {
|
||||
display: 'flex',
|
||||
},
|
||||
leftSide: {
|
||||
'& img': {
|
||||
borderRadius: 4,
|
||||
maxWidth: '100%',
|
||||
minWidth: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
maxWidth: '50%',
|
||||
// [theme.breakpoints.up('md')]: {
|
||||
// minWidth: '100px',
|
||||
// },
|
||||
},
|
||||
rightSide: {
|
||||
marginLeft: 15,
|
||||
maxWidth: '100%',
|
||||
'& span': {
|
||||
fontWeight: '400',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
fontSize: '1.3em',
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
'& button': {
|
||||
color: inLibrary ? '#2196f3' : 'inherit',
|
||||
},
|
||||
'& a': {
|
||||
textDecoration: 'none',
|
||||
color: '#858585',
|
||||
'& button': {
|
||||
color: 'inherit',
|
||||
const useStyles = (inLibrary: boolean) =>
|
||||
makeStyles((theme: Theme) => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
position: 'sticky',
|
||||
top: '64px',
|
||||
left: '0px',
|
||||
width: '50vw',
|
||||
height: 'calc(100vh - 64px)',
|
||||
alignSelf: 'flex-start',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
},
|
||||
bottom: {
|
||||
paddingLeft: '10px',
|
||||
paddingRight: '10px',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
fontSize: '1.2em',
|
||||
// maxWidth: '50%',
|
||||
top: {
|
||||
padding: '10px',
|
||||
// [theme.breakpoints.up('md')]: {
|
||||
// minWidth: '50%',
|
||||
// },
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
fontSize: '1.3em',
|
||||
leftRight: {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
'& h4': {
|
||||
marginTop: '1em',
|
||||
marginBottom: 0,
|
||||
leftSide: {
|
||||
'& img': {
|
||||
borderRadius: 4,
|
||||
maxWidth: '100%',
|
||||
minWidth: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
maxWidth: '50%',
|
||||
// [theme.breakpoints.up('md')]: {
|
||||
// minWidth: '100px',
|
||||
// },
|
||||
},
|
||||
'& p': {
|
||||
textAlign: 'justify',
|
||||
textJustify: 'inter-word',
|
||||
rightSide: {
|
||||
marginLeft: 15,
|
||||
maxWidth: '100%',
|
||||
'& span': {
|
||||
fontWeight: '400',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
fontSize: '1.3em',
|
||||
},
|
||||
},
|
||||
},
|
||||
genre: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
'& h5': {
|
||||
border: '2px solid #2196f3',
|
||||
borderRadius: '1.13em',
|
||||
marginRight: '1em',
|
||||
marginTop: 0,
|
||||
marginBottom: '10px',
|
||||
padding: '0.3em',
|
||||
color: '#2196f3',
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
'& button': {
|
||||
color: inLibrary ? '#2196f3' : 'inherit',
|
||||
},
|
||||
'& a': {
|
||||
textDecoration: 'none',
|
||||
color: '#858585',
|
||||
'& button': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
bottom: {
|
||||
paddingLeft: '10px',
|
||||
paddingRight: '10px',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
fontSize: '1.2em',
|
||||
// maxWidth: '50%',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
fontSize: '1.3em',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
'& h4': {
|
||||
marginTop: '1em',
|
||||
marginBottom: 0,
|
||||
},
|
||||
'& p': {
|
||||
textAlign: 'justify',
|
||||
textJustify: 'inter-word',
|
||||
},
|
||||
},
|
||||
genre: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
'& h5': {
|
||||
border: '2px solid #2196f3',
|
||||
borderRadius: '1.13em',
|
||||
marginRight: '1em',
|
||||
marginTop: 0,
|
||||
marginBottom: '10px',
|
||||
padding: '0.3em',
|
||||
color: '#2196f3',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps{
|
||||
manga: IManga
|
||||
interface IProps {
|
||||
manga: IManga;
|
||||
}
|
||||
|
||||
function getSourceName(source: ISource) {
|
||||
@@ -133,14 +134,24 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
const classes = useStyles(manga.inLibrary)();
|
||||
|
||||
const addToLibrary = () => {
|
||||
mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`, { ...manga, inLibrary: true }, { revalidate: false });
|
||||
client.get(`/api/v1/manga/${manga.id}/library/`)
|
||||
mutate(
|
||||
`/api/v1/manga/${manga.id}/?onlineFetch=false`,
|
||||
{ ...manga, inLibrary: true },
|
||||
{ revalidate: false },
|
||||
);
|
||||
client
|
||||
.get(`/api/v1/manga/${manga.id}/library/`)
|
||||
.then(() => mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`));
|
||||
};
|
||||
|
||||
const removeFromLibrary = () => {
|
||||
mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`, { ...manga, inLibrary: false }, { revalidate: false });
|
||||
client.delete(`/api/v1/manga/${manga.id}/library/`)
|
||||
mutate(
|
||||
`/api/v1/manga/${manga.id}/?onlineFetch=false`,
|
||||
{ ...manga, inLibrary: false },
|
||||
{ revalidate: false },
|
||||
);
|
||||
client
|
||||
.delete(`/api/v1/manga/${manga.id}/library/`)
|
||||
.then(() => mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`));
|
||||
};
|
||||
|
||||
@@ -149,12 +160,13 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
<div className={classes.top}>
|
||||
<div className={classes.leftRight}>
|
||||
<div className={classes.leftSide}>
|
||||
<img src={`${serverAddress}${manga.thumbnailUrl}?useCache=${useCache}`} alt="Manga Thumbnail" />
|
||||
<img
|
||||
src={`${serverAddress}${manga.thumbnailUrl}?useCache=${useCache}`}
|
||||
alt="Manga Thumbnail"
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.rightSide}>
|
||||
<h1>
|
||||
{manga.title}
|
||||
</h1>
|
||||
<h1>{manga.title}</h1>
|
||||
<h3>
|
||||
{'Author: '}
|
||||
<span>{getValueOrUnknown(manga.author)}</span>
|
||||
@@ -163,20 +175,21 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
{'Artist: '}
|
||||
<span>{getValueOrUnknown(manga.artist)}</span>
|
||||
</h3>
|
||||
<h3>
|
||||
{`Status: ${manga.status}`}
|
||||
</h3>
|
||||
<h3>
|
||||
{`Source: ${getSourceName(manga.source)}`}
|
||||
</h3>
|
||||
<h3>{`Status: ${manga.status}`}</h3>
|
||||
<h3>{`Source: ${getSourceName(manga.source)}`}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.buttons}>
|
||||
<div>
|
||||
<IconButton onClick={manga.inLibrary ? removeFromLibrary : addToLibrary} size="large">
|
||||
{manga.inLibrary
|
||||
? <FavoriteIcon sx={{ mr: 1 }} />
|
||||
: <FavoriteBorderIcon sx={{ mr: 1 }} />}
|
||||
<IconButton
|
||||
onClick={manga.inLibrary ? removeFromLibrary : addToLibrary}
|
||||
size="large"
|
||||
>
|
||||
{manga.inLibrary ? (
|
||||
<FavoriteIcon sx={{ mr: 1 }} />
|
||||
) : (
|
||||
<FavoriteBorderIcon sx={{ mr: 1 }} />
|
||||
)}
|
||||
<Typography sx={{ fontSize: { xs: '0.75em', sm: '0.85em' } }}>
|
||||
{manga.inLibrary ? 'In Library' : 'Add To Library'}
|
||||
</Typography>
|
||||
@@ -198,7 +211,9 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
<p>{manga.description}</p>
|
||||
</div>
|
||||
<div className={classes.genre}>
|
||||
{manga.genre.map((g) => <h5 key={g}>{g}</h5>)}
|
||||
{manga.genre.map((g) => (
|
||||
<h5 key={g}>{g}</h5>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,14 @@ import Label from '@mui/icons-material/Label';
|
||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||
import Refresh from '@mui/icons-material/Refresh';
|
||||
import {
|
||||
IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip, useMediaQuery, useTheme,
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import CategorySelect from 'components/navbar/action/CategorySelect';
|
||||
import React, { useState } from 'react';
|
||||
@@ -37,13 +44,22 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
{isLargeScreen && (
|
||||
<>
|
||||
<Tooltip title="Reload data from source">
|
||||
<IconButton onClick={() => { onRefresh(); }} disabled={refreshing}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onRefresh();
|
||||
}}
|
||||
disabled={refreshing}
|
||||
>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{manga.inLibrary && (
|
||||
<Tooltip title="Edit manga categories">
|
||||
<IconButton onClick={() => { setEditCategories(true); }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setEditCategories(true);
|
||||
}}
|
||||
>
|
||||
<Label />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -71,37 +87,35 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => { onRefresh(); handleClose(); }}
|
||||
onClick={() => {
|
||||
onRefresh();
|
||||
handleClose();
|
||||
}}
|
||||
disabled={refreshing}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Refresh fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Reload data from source
|
||||
</ListItemText>
|
||||
<ListItemText>Reload data from source</ListItemText>
|
||||
</MenuItem>
|
||||
{manga.inLibrary && (
|
||||
<MenuItem
|
||||
onClick={() => { setEditCategories(true); handleClose(); }}
|
||||
onClick={() => {
|
||||
setEditCategories(true);
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Label fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Edit manga categories
|
||||
</ListItemText>
|
||||
<ListItemText>Edit manga categories</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CategorySelect
|
||||
open={editCategories}
|
||||
setOpen={setEditCategories}
|
||||
mangaId={manga.id}
|
||||
/>
|
||||
<CategorySelect open={editCategories} setOpen={setEditCategories} mangaId={manga.id} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,23 +11,29 @@ import { Link } from 'react-router-dom';
|
||||
import { PlayArrow } from '@mui/icons-material';
|
||||
import { BACK } from 'util/useBackTo';
|
||||
|
||||
interface ResumeFABProps{
|
||||
chapter: IChapter
|
||||
mangaId: string
|
||||
interface ResumeFABProps {
|
||||
chapter: IChapter;
|
||||
mangaId: string;
|
||||
}
|
||||
|
||||
export default function ResumeFab(props: ResumeFABProps) {
|
||||
const { chapter: { index, lastPageRead }, mangaId } = props;
|
||||
const {
|
||||
chapter: { index, lastPageRead },
|
||||
mangaId,
|
||||
} = props;
|
||||
return (
|
||||
<Fab
|
||||
sx={{ position: 'fixed', bottom: '2em', right: '3em' }}
|
||||
component={Link}
|
||||
variant="extended"
|
||||
color="primary"
|
||||
to={{ pathname: `/manga/${mangaId}/chapter/${index}/page/${lastPageRead}`, state: { backLink: BACK } }}
|
||||
to={{
|
||||
pathname: `/manga/${mangaId}/chapter/${index}/page/${lastPageRead}`,
|
||||
state: { backLink: BACK },
|
||||
}}
|
||||
>
|
||||
<PlayArrow />
|
||||
{index === 1 ? 'Start' : 'Resume' }
|
||||
{index === 1 ? 'Start' : 'Resume'}
|
||||
</Fab>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,20 +6,24 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||
import {
|
||||
Fab, Menu,
|
||||
} from '@mui/material';
|
||||
import { Fab, Menu } from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import { pluralize } from 'components/util/helpers';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import type { IChapterWithMeta } from 'components/manga/ChapterList';
|
||||
import SelectionFABActionItem from 'components/manga/SelectionFABActionItem';
|
||||
|
||||
export type SelectionAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread';
|
||||
export type SelectionAction =
|
||||
| 'download'
|
||||
| 'delete'
|
||||
| 'bookmark'
|
||||
| 'unbookmark'
|
||||
| 'mark_as_read'
|
||||
| 'mark_as_unread';
|
||||
|
||||
interface SelectionFABProps{
|
||||
selectedChapters: IChapterWithMeta[]
|
||||
onAction: (action: SelectionAction, chapters: IChapterWithMeta[]) => void
|
||||
interface SelectionFABProps {
|
||||
selectedChapters: IChapterWithMeta[];
|
||||
onAction: (action: SelectionAction, chapters: IChapterWithMeta[]) => void;
|
||||
}
|
||||
|
||||
const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
||||
@@ -38,7 +42,10 @@ const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed', bottom: '2em', right: '3em', pt: 1,
|
||||
position: 'fixed',
|
||||
bottom: '2em',
|
||||
right: '3em',
|
||||
pt: 1,
|
||||
}}
|
||||
ref={anchorEl}
|
||||
>
|
||||
|
||||
@@ -17,10 +17,10 @@ import type { IChapterWithMeta } from 'components/manga/ChapterList';
|
||||
import type { SelectionAction } from 'components/manga/SelectionFAB';
|
||||
|
||||
interface IProps {
|
||||
action: SelectionAction
|
||||
matchingChapters: IChapterWithMeta[]
|
||||
title: string
|
||||
onClick: (action: SelectionAction, chapters: IChapterWithMeta[]) => void
|
||||
action: SelectionAction;
|
||||
matchingChapters: IChapterWithMeta[];
|
||||
title: string;
|
||||
onClick: (action: SelectionAction, chapters: IChapterWithMeta[]) => void;
|
||||
}
|
||||
|
||||
const ICONS = {
|
||||
@@ -32,16 +32,11 @@ const ICONS = {
|
||||
mark_as_unread: RemoveDone,
|
||||
};
|
||||
|
||||
const SelectionFABActionItem: React.FC<IProps> = ({
|
||||
action, matchingChapters, onClick, title,
|
||||
}) => {
|
||||
const SelectionFABActionItem: React.FC<IProps> = ({ action, matchingChapters, onClick, title }) => {
|
||||
const count = matchingChapters.length;
|
||||
const Icon = ICONS[action];
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={() => onClick(action, matchingChapters)}
|
||||
disabled={count === 0}
|
||||
>
|
||||
<MenuItem onClick={() => onClick(action, matchingChapters)} disabled={count === 0}>
|
||||
<ListItemIcon>
|
||||
<Icon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
|
||||
@@ -16,10 +16,14 @@ export const useRefreshManga = (mangaId: string) => {
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setFetchingOnline(true);
|
||||
await Promise.all([
|
||||
fetcher(`/api/v1/manga/${mangaId}/?onlineFetch=true`)
|
||||
.then((res) => mutate(`/api/v1/manga/${mangaId}/?onlineFetch=false`, res, { revalidate: false })),
|
||||
fetcher(`/api/v1/manga/${mangaId}/chapters?onlineFetch=true`)
|
||||
.then((res) => mutate(`/api/v1/manga/${mangaId}/chapters?onlineFetch=false`, res, { revalidate: false })),
|
||||
fetcher(`/api/v1/manga/${mangaId}/?onlineFetch=true`).then((res) =>
|
||||
mutate(`/api/v1/manga/${mangaId}/?onlineFetch=false`, res, { revalidate: false }),
|
||||
),
|
||||
fetcher(`/api/v1/manga/${mangaId}/chapters?onlineFetch=true`).then((res) =>
|
||||
mutate(`/api/v1/manga/${mangaId}/chapters?onlineFetch=false`, res, {
|
||||
revalidate: false,
|
||||
}),
|
||||
),
|
||||
]).finally(() => setFetchingOnline(false));
|
||||
}, [mangaId]);
|
||||
|
||||
|
||||
@@ -17,15 +17,15 @@ const defaultChapterOptions: ChapterListOptions = {
|
||||
showChapterNumber: false,
|
||||
};
|
||||
|
||||
function chapterOptionsReducer(state: ChapterListOptions,
|
||||
actions: ChapterOptionsReducerAction)
|
||||
: ChapterListOptions {
|
||||
function chapterOptionsReducer(
|
||||
state: ChapterListOptions,
|
||||
actions: ChapterOptionsReducerAction,
|
||||
): ChapterListOptions {
|
||||
switch (actions.type) {
|
||||
case 'filter':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const active = state.unread !== false
|
||||
&& state.downloaded !== false
|
||||
&& state.bookmarked !== false;
|
||||
const active =
|
||||
state.unread !== false && state.downloaded !== false && state.bookmarked !== false;
|
||||
return {
|
||||
...state,
|
||||
active,
|
||||
@@ -53,8 +53,10 @@ export function unreadFilter(unread: NullAndUndefined<boolean>, { read: isChapte
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFilter(downloaded: NullAndUndefined<boolean>,
|
||||
{ downloaded: chapterDownload }: IChapter) {
|
||||
function downloadFilter(
|
||||
downloaded: NullAndUndefined<boolean>,
|
||||
{ downloaded: chapterDownload }: IChapter,
|
||||
) {
|
||||
switch (downloaded) {
|
||||
case true:
|
||||
return chapterDownload;
|
||||
@@ -65,8 +67,10 @@ function downloadFilter(downloaded: NullAndUndefined<boolean>,
|
||||
}
|
||||
}
|
||||
|
||||
function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>,
|
||||
{ bookmarked: chapterBookmarked }: IChapter) {
|
||||
function bookmarkedFilter(
|
||||
bookmarked: NullAndUndefined<boolean>,
|
||||
{ bookmarked: chapterBookmarked }: IChapter,
|
||||
) {
|
||||
switch (bookmarked) {
|
||||
case true:
|
||||
return chapterBookmarked;
|
||||
@@ -77,29 +81,34 @@ function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>,
|
||||
}
|
||||
}
|
||||
|
||||
export function filterAndSortChapters(chapters: IChapter[], options: ChapterListOptions)
|
||||
: IChapter[] {
|
||||
export function filterAndSortChapters(
|
||||
chapters: IChapter[],
|
||||
options: ChapterListOptions,
|
||||
): IChapter[] {
|
||||
const filtered = options.active
|
||||
? chapters.filter((chp) => unreadFilter(options.unread, chp)
|
||||
&& downloadFilter(options.downloaded, chp)
|
||||
&& bookmarkedFilter(options.bookmarked, chp))
|
||||
? chapters.filter(
|
||||
(chp) =>
|
||||
unreadFilter(options.unread, chp) &&
|
||||
downloadFilter(options.downloaded, chp) &&
|
||||
bookmarkedFilter(options.bookmarked, chp),
|
||||
)
|
||||
: [...chapters];
|
||||
const Sorted = options.sortBy === 'fetchedAt'
|
||||
? filtered.sort((a, b) => a.fetchedAt - b.fetchedAt)
|
||||
: filtered;
|
||||
const Sorted =
|
||||
options.sortBy === 'fetchedAt'
|
||||
? filtered.sort((a, b) => a.fetchedAt - b.fetchedAt)
|
||||
: filtered;
|
||||
if (options.reverse) {
|
||||
Sorted.reverse();
|
||||
}
|
||||
return Sorted;
|
||||
}
|
||||
|
||||
export const useChapterOptions = (mangaId: string) => useReducerLocalStorage<
|
||||
ChapterListOptions,
|
||||
ChapterOptionsReducerAction
|
||||
>(
|
||||
chapterOptionsReducer,
|
||||
`${mangaId}filterOptions`, defaultChapterOptions,
|
||||
);
|
||||
export const useChapterOptions = (mangaId: string) =>
|
||||
useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>(
|
||||
chapterOptionsReducer,
|
||||
`${mangaId}filterOptions`,
|
||||
defaultChapterOptions,
|
||||
);
|
||||
|
||||
export const SORT_OPTIONS: [ChapterSortMode, string][] = [
|
||||
['source', 'By Source'],
|
||||
|
||||
Reference in New Issue
Block a user