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:
Daniel
2023-02-06 10:06:33 +01:00
committed by GitHub
parent 2eeea41a45
commit 4e0a860dfd
101 changed files with 2065 additions and 1886 deletions

View File

@@ -1,24 +1,13 @@
module.exports = { module.exports = {
extends: [ extends: ['airbnb', 'airbnb-typescript', 'prettier'],
'airbnb', plugins: ['@typescript-eslint', 'no-relative-import-paths', 'prettier'],
'airbnb-typescript'
],
plugins: ['@typescript-eslint', 'no-relative-import-paths'],
parserOptions: { parserOptions: {
project: './tsconfig.json', project: './tsconfig.json',
}, },
rules: { rules: {
// Indent with 4 spaces 'prettier/prettier': 'error',
'@typescript-eslint/indent': ['error', 4],
// Indent JSX with 4 spaces
'react/jsx-indent': ['error', 4],
// Indent props with 4 spaces
'react/jsx-indent-props': ['error', 4],
'no-plusplus': ['error', { 'allowForLoopAfterthoughts': true }],
'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
// just why // just why
'react/jsx-no-bind': 'off', 'react/jsx-no-bind': 'off',

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"tabWidth": 4,
"singleQuote": true,
"printWidth": 100,
"semi": true,
"trailingComma": "all"
}

View File

@@ -58,11 +58,14 @@
"eslint": "^7.2.0", "eslint": "^7.2.0",
"eslint-config-airbnb": "18.2.1", "eslint-config-airbnb": "18.2.1",
"eslint-config-airbnb-typescript": "^14.0.0", "eslint-config-airbnb-typescript": "^14.0.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-no-relative-import-paths": "^1.5.2", "eslint-plugin-no-relative-import-paths": "^1.5.2",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.21.5", "eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^1.7.0", "eslint-plugin-react-hooks": "^1.7.0",
"prettier": "^2.8.2",
"typescript": "^4.8.4" "typescript": "^4.8.4"
} }
} }

View File

@@ -10,9 +10,7 @@ import CssBaseline from '@mui/material/CssBaseline';
import AppContext from 'components/context/AppContext'; import AppContext from 'components/context/AppContext';
import DefaultNavBar from 'components/navbar/DefaultNavBar'; import DefaultNavBar from 'components/navbar/DefaultNavBar';
import React from 'react'; import React from 'react';
import { import { Redirect, Route, Switch } from 'react-router-dom';
Redirect, Route, Switch,
} from 'react-router-dom';
import Browse from 'screens/Browse'; import Browse from 'screens/Browse';
import DownloadQueue from 'screens/DownloadQueue'; import DownloadQueue from 'screens/DownloadQueue';
import Extensions from 'screens/Extensions'; import Extensions from 'screens/Extensions';
@@ -48,13 +46,7 @@ const App: React.FC = () => (
> >
<Switch> <Switch>
{/* General Routes */} {/* General Routes */}
<Route <Route exact path="/" render={() => <Redirect to="/library" />} />
exact
path="/"
render={() => (
<Redirect to="/library" />
)}
/>
<Route path="/settings/about"> <Route path="/settings/about">
<About /> <About />
</Route> </Route>
@@ -116,14 +108,7 @@ const App: React.FC = () => (
path="/manga/:mangaId/chapter/:chapterIndex" path="/manga/:mangaId/chapter/:chapterIndex"
// passing a key re-mounts the reader // passing a key re-mounts the reader
// when changing chapters // when changing chapters
render={(props: any) => ( render={(props: any) => <Reader key={props.match.params.chapterIndex} />}
<Reader
key={
props.match.params
.chapterIndex
}
/>
)}
/> />
</Switch> </Switch>
</AppContext> </AppContext>

View File

@@ -16,24 +16,34 @@ import useLocalStorage from 'util/useLocalStorage';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
interface IProps { interface IProps {
extension: IExtension extension: IExtension;
notifyInstall: () => void notifyInstall: () => void;
} }
export default function ExtensionCard(props: IProps) { export default function ExtensionCard(props: IProps) {
const { const {
extension: { extension: {
name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl, isNsfw, name,
lang,
versionName,
installed,
hasUpdate,
obsolete,
pkgName,
iconUrl,
isNsfw,
}, },
notifyInstall, notifyInstall,
} = props; } = props;
const [installedState, setInstalledState] = useState<string>( const [installedState, setInstalledState] = useState<string>(() => {
() => { if (obsolete) {
if (obsolete) { return 'obsolete'; } return 'obsolete';
if (hasUpdate) { return 'update'; } }
return (installed ? 'uninstall' : 'install'); if (hasUpdate) {
}, return 'update';
); }
return installed ? 'uninstall' : 'install';
});
const [serverAddress] = useLocalStorage<String>('serverBaseURL', ''); const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [useCache] = useLocalStorage<boolean>('useCache', true); const [useCache] = useLocalStorage<boolean>('useCache', true);
@@ -42,8 +52,7 @@ export default function ExtensionCard(props: IProps) {
function install() { function install() {
setInstalledState('installing'); setInstalledState('installing');
client.get(`/api/v1/extension/install/${pkgName}`) client.get(`/api/v1/extension/install/${pkgName}`).then(() => {
.then(() => {
setInstalledState('uninstall'); setInstalledState('uninstall');
notifyInstall(); notifyInstall();
}); });
@@ -51,8 +60,7 @@ export default function ExtensionCard(props: IProps) {
function update() { function update() {
setInstalledState('updating'); setInstalledState('updating');
client.get(`/api/v1/extension/update/${pkgName}`) client.get(`/api/v1/extension/update/${pkgName}`).then(() => {
.then(() => {
setInstalledState('uninstall'); setInstalledState('uninstall');
notifyInstall(); notifyInstall();
}); });
@@ -60,8 +68,7 @@ export default function ExtensionCard(props: IProps) {
function uninstall() { function uninstall() {
setInstalledState('uninstalling'); setInstalledState('uninstalling');
client.get(`/api/v1/extension/uninstall/${pkgName}`) client.get(`/api/v1/extension/uninstall/${pkgName}`).then(() => {
.then(() => {
// setInstalledState('install'); // setInstalledState('install');
notifyInstall(); notifyInstall();
}); });
@@ -89,7 +96,8 @@ export default function ExtensionCard(props: IProps) {
return ( return (
<Card sx={{ margin: '10px' }}> <Card sx={{ margin: '10px' }}>
<CardContent sx={{ <CardContent
sx={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
@@ -113,11 +121,14 @@ export default function ExtensionCard(props: IProps) {
{name} {name}
</Typography> </Typography>
<Typography variant="caption" display="block" gutterBottom> <Typography variant="caption" display="block" gutterBottom>
{langPress} {langPress} {versionName}
{' '}
{versionName}
{isNsfw && ( {isNsfw && (
<Typography variant="caption" display="inline" gutterBottom color="red"> <Typography
variant="caption"
display="inline"
gutterBottom
color="red"
>
{' 18+'} {' 18+'}
</Typography> </Typography>
)} )}
@@ -131,7 +142,6 @@ export default function ExtensionCard(props: IProps) {
onClick={() => handleButtonClick()} onClick={() => handleButtonClick()}
> >
{installedState} {installedState}
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -65,22 +65,30 @@ const truncateText = (str: string, maxLength: number) => {
}; };
interface IProps { interface IProps {
manga: IMangaCard manga: IMangaCard;
gridLayout?: GridLayout gridLayout?: GridLayout;
dimensions: number dimensions: number;
inLibraryIndicator?: boolean inLibraryIndicator?: boolean;
} }
const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => { const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
const { const {
manga: { manga: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
id, title, thumbnailUrl, downloadCount, unreadCount: unread, inLibrary, id,
title,
thumbnailUrl,
downloadCount,
unreadCount: unread,
inLibrary,
}, },
gridLayout, gridLayout,
dimensions, dimensions,
inLibraryIndicator, inLibraryIndicator,
} = props; } = props;
const { options: { showUnreadBadge, showDownloadBadge } } = useLibraryOptionsContext(); const {
options: { showUnreadBadge, showDownloadBadge },
} = useLibraryOptionsContext();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', ''); const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [useCache] = useLocalStorage<boolean>('useCache', true); const [useCache] = useLocalStorage<boolean>('useCache', true);
@@ -92,7 +100,10 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
const cols = Math.ceil(dimensions / ItemWidth); const cols = Math.ceil(dimensions / ItemWidth);
return ( return (
<Grid item columns={cols} xs={1}> <Grid item columns={cols} xs={1}>
<Link to={mangaLinkTo} style={(gridLayout === GridLayout.Comfortable) ? { textDecoration: 'none' } : {}}> <Link
to={mangaLinkTo}
style={gridLayout === GridLayout.Comfortable ? { textDecoration: 'none' } : {}}
>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -113,7 +124,6 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
height: '100%', height: '100%',
}} }}
> >
<BadgeContainer <BadgeContainer
sx={{ sx={{
position: 'absolute', position: 'absolute',
@@ -129,14 +139,13 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
</Typography> </Typography>
)} )}
{showUnreadBadge && unread! > 0 && ( {showUnreadBadge && unread! > 0 && (
<Typography <Typography sx={{ backgroundColor: 'primary.dark' }}>
sx={{ backgroundColor: 'primary.dark' }}
>
{unread} {unread}
</Typography> </Typography>
)} )}
{showDownloadBadge && downloadCount! > 0 && ( {showDownloadBadge && downloadCount! > 0 && (
<Typography sx={{ <Typography
sx={{
backgroundColor: 'success.dark', backgroundColor: 'success.dark',
}} }}
> >
@@ -147,7 +156,8 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
<SpinnerImage <SpinnerImage
alt={title} alt={title}
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`} src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
imgStyle={inLibraryIndicator && inLibrary imgStyle={
inLibraryIndicator && inLibrary
? { ? {
height: '100%', height: '100%',
width: '100%', width: '100%',
@@ -158,19 +168,22 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
height: '100%', height: '100%',
width: '100%', width: '100%',
objectFit: 'cover', objectFit: 'cover',
}} }
}
spinnerStyle={{ spinnerStyle={{
display: 'grid', display: 'grid',
placeItems: 'center', placeItems: 'center',
}} }}
/> />
{(gridLayout === GridLayout.Comfortable) ? (<></>) : ( {gridLayout === GridLayout.Comfortable ? (
<></>
) : (
<> <>
<BottomGradient /> <BottomGradient />
<BottomGradientDoubledDown /> <BottomGradientDoubledDown />
</> </>
)} )}
{(gridLayout === GridLayout.Comfortable) ? ( {gridLayout === GridLayout.Comfortable ? (
<></> <></>
) : ( ) : (
<MangaTitle <MangaTitle
@@ -185,7 +198,7 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
)} )}
</CardActionArea> </CardActionArea>
</Card> </Card>
{(gridLayout === GridLayout.Comfortable) ? ( {gridLayout === GridLayout.Comfortable ? (
<MangaTitle <MangaTitle
sx={{ sx={{
position: 'relative', position: 'relative',
@@ -195,7 +208,9 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
> >
{truncateText(title, 61)} {truncateText(title, 61)}
</MangaTitle> </MangaTitle>
) : (<></>)} ) : (
<></>
)}
</Box> </Box>
</Link> </Link>
</Grid> </Grid>
@@ -205,10 +220,7 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
return ( return (
<Grid item xs={12}> <Grid item xs={12}>
<Card> <Card>
<CardActionArea <CardActionArea component={Link} to={mangaLinkTo}>
component={Link}
to={mangaLinkTo}
>
<CardContent <CardContent
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -220,7 +232,8 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
> >
<Avatar <Avatar
variant="rounded" variant="rounded"
sx={inLibraryIndicator && inLibrary sx={
inLibraryIndicator && inLibrary
? { ? {
width: 56, width: 56,
height: 56, height: 56,
@@ -235,7 +248,8 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
flex: '0 0 auto', flex: '0 0 auto',
marginRight: 2, marginRight: 2,
imageRendering: 'pixelated', imageRendering: 'pixelated',
}} }
}
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`} src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
/> />
<Box <Box
@@ -252,21 +266,18 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
</Box> </Box>
<BadgeContainer> <BadgeContainer>
{inLibraryIndicator && inLibrary && ( {inLibraryIndicator && inLibrary && (
<Typography <Typography sx={{ backgroundColor: 'primary.dark' }}>
sx={{ backgroundColor: 'primary.dark' }}
>
In library In library
</Typography> </Typography>
)} )}
{showUnreadBadge && unread! > 0 && ( {showUnreadBadge && unread! > 0 && (
<Typography <Typography sx={{ backgroundColor: 'primary.dark' }}>
sx={{ backgroundColor: 'primary.dark' }}
>
{unread} {unread}
</Typography> </Typography>
)} )}
{showDownloadBadge && downloadCount! > 0 && ( {showDownloadBadge && downloadCount! > 0 && (
<Typography sx={{ <Typography
sx={{
backgroundColor: 'success.dark', backgroundColor: 'success.dark',
}} }}
> >

View File

@@ -5,9 +5,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
useEffect, useLayoutEffect, useRef, useState,
} from 'react';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import EmptyView from 'components/util/EmptyView'; import EmptyView from 'components/util/EmptyView';
import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
@@ -17,23 +15,31 @@ import MangaCard from 'components/MangaCard';
import { GridLayout } from 'components/context/LibraryOptionsContext'; import { GridLayout } from 'components/context/LibraryOptionsContext';
export interface IMangaGridProps { export interface IMangaGridProps {
mangas: IMangaCard[] mangas: IMangaCard[];
isLoading: boolean isLoading: boolean;
message?: string message?: string;
messageExtra?: JSX.Element messageExtra?: JSX.Element;
hasNextPage: boolean hasNextPage: boolean;
lastPageNum: number lastPageNum: number;
setLastPageNum: (lastPageNum: number) => void setLastPageNum: (lastPageNum: number) => void;
gridLayout?: GridLayout gridLayout?: GridLayout;
horisontal?: boolean | undefined horisontal?: boolean | undefined;
noFaces?: boolean | undefined noFaces?: boolean | undefined;
inLibraryIndicator?: boolean inLibraryIndicator?: boolean;
} }
const MangaGrid: React.FC<IMangaGridProps> = (props) => { const MangaGrid: React.FC<IMangaGridProps> = (props) => {
const { const {
mangas, isLoading, message, messageExtra, mangas,
hasNextPage, lastPageNum, setLastPageNum, gridLayout, horisontal, noFaces, isLoading,
message,
messageExtra,
hasNextPage,
lastPageNum,
setLastPageNum,
gridLayout,
horisontal,
noFaces,
inLibraryIndicator, inLibraryIndicator,
} = props; } = props;
let mapped; let mapped;
@@ -42,7 +48,7 @@ const MangaGrid: React.FC<IMangaGridProps> = (props) => {
const scrollHandler = () => { const scrollHandler = () => {
if (lastManga.current) { if (lastManga.current) {
const rect = lastManga.current.getBoundingClientRect(); const rect = lastManga.current.getBoundingClientRect();
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) { if ((rect.y + rect.height) / window.innerHeight < 2 && hasNextPage) {
setLastPageNum(lastPageNum + 1); setLastPageNum(lastPageNum + 1);
} }
} }
@@ -73,9 +79,7 @@ const MangaGrid: React.FC<IMangaGridProps> = (props) => {
if (mangas.length === 0) { if (mangas.length === 0) {
if (isLoading) { if (isLoading) {
mapped = ( mapped = <LoadingPlaceholder />;
<LoadingPlaceholder />
);
} else { } else {
mapped = noFaces ? ( mapped = noFaces ? (
<Box <Box
@@ -83,9 +87,7 @@ const MangaGrid: React.FC<IMangaGridProps> = (props) => {
margin: 'auto', margin: 'auto',
}} }}
> >
<Typography variant="h5"> <Typography variant="h5">{message}</Typography>
{message}
</Typography>
{messageExtra} {messageExtra}
</Box> </Box>
) : ( ) : (
@@ -110,18 +112,22 @@ const MangaGrid: React.FC<IMangaGridProps> = (props) => {
<Grid <Grid
container container
spacing={1} spacing={1}
style={horisontal ? { style={
horisontal
? {
margin: 0, margin: 0,
width: '100%', width: '100%',
padding: '5px', padding: '5px',
overflowX: 'scroll', overflowX: 'scroll',
display: '-webkit-inline-box', display: '-webkit-inline-box',
flexWrap: 'nowrap', flexWrap: 'nowrap',
} : { }
: {
margin: 0, margin: 0,
width: '100%', width: '100%',
padding: '5px', padding: '5px',
}} }
}
> >
{mapped} {mapped}
</Grid> </Grid>

View File

@@ -36,14 +36,12 @@ const WiderWidthButtons = styled('div')(({ theme }) => ({
})); }));
interface IProps { interface IProps {
source: ISource source: ISource;
} }
const SourceCard: React.FC<IProps> = (props: IProps) => { const SourceCard: React.FC<IProps> = (props: IProps) => {
const { const {
source: { source: { id, name, lang, iconUrl, supportsLatest, isNsfw },
id, name, lang, iconUrl, supportsLatest, isNsfw,
},
} = props; } = props;
const history = useHistory(); const history = useHistory();
@@ -64,10 +62,7 @@ const SourceCard: React.FC<IProps> = (props: IProps) => {
margin: '10px', margin: '10px',
}} }}
> >
<CardActionArea <CardActionArea component={Link} to={`/sources/${id}/popular/`}>
component={Link}
to={`/sources/${id}/popular/`}
>
<CardContent <CardContent
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -76,7 +71,6 @@ const SourceCard: React.FC<IProps> = (props: IProps) => {
padding: 2, padding: 2,
}} }}
> >
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<Avatar <Avatar
variant="rounded" variant="rounded"
@@ -89,7 +83,13 @@ const SourceCard: React.FC<IProps> = (props: IProps) => {
}} }}
src={`${serverAddress}${iconUrl}?useCache=${useCache}`} src={`${serverAddress}${iconUrl}?useCache=${useCache}`}
/> />
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}> <Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
>
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
{name} {name}
</Typography> </Typography>
@@ -97,7 +97,12 @@ const SourceCard: React.FC<IProps> = (props: IProps) => {
<Typography variant="caption" display="block" gutterBottom> <Typography variant="caption" display="block" gutterBottom>
{langCodeToName(lang)} {langCodeToName(lang)}
{isNsfw && ( {isNsfw && (
<Typography variant="caption" display="inline" gutterBottom color="red"> <Typography
variant="caption"
display="inline"
gutterBottom
color="red"
>
{' 18+'} {' 18+'}
</Typography> </Typography>
)} )}

View File

@@ -10,19 +10,11 @@ import { Checkbox, CheckboxProps, FormControlLabel } from '@mui/material';
import React from 'react'; import React from 'react';
interface IProps extends CheckboxProps { interface IProps extends CheckboxProps {
label?: string label?: string;
} }
const CheckboxInput: React.FC<IProps> = ({ const CheckboxInput: React.FC<IProps> = ({ label, sx, ...rest }) => (
label, sx, ...rest <FormControlLabel control={<Checkbox {...rest} />} label={label} sx={sx} />
}) => (
<FormControlLabel
control={(
<Checkbox {...rest} />
)}
label={label}
sx={sx}
/>
); );
export default CheckboxInput; export default CheckboxInput;

View File

@@ -2,13 +2,11 @@ import { CircularProgress, IconButton, IconButtonProps } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
interface IProps extends Omit<IconButtonProps, 'onClick'> { interface IProps extends Omit<IconButtonProps, 'onClick'> {
loading?: boolean loading?: boolean;
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<any> onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<any>;
} }
const LoadingIconButton = ({ const LoadingIconButton = ({ onClick, children, loading: iLoading, ...rest }: IProps) => {
onClick, children, loading: iLoading, ...rest
}: IProps) => {
const [sLoading, setLoading] = useState(false); const [sLoading, setLoading] = useState(false);
const loading = sLoading || iLoading; const loading = sLoading || iLoading;
@@ -19,7 +17,7 @@ const LoadingIconButton = ({
return ( return (
<IconButton disabled={loading} {...rest} onClick={handleClick}> <IconButton disabled={loading} {...rest} onClick={handleClick}>
{loading ? (<CircularProgress size={24} />) : children} {loading ? <CircularProgress size={24} /> : children}
</IconButton> </IconButton>
); );
}; };

View File

@@ -10,19 +10,11 @@ import { FormControlLabel, Radio, RadioProps } from '@mui/material';
import React from 'react'; import React from 'react';
export interface RadioInputProps extends RadioProps { export interface RadioInputProps extends RadioProps {
label?: string label?: string;
} }
const RadioInput: React.FC<RadioInputProps> = ({ const RadioInput: React.FC<RadioInputProps> = ({ label, sx, ...rest }) => (
label, sx, ...rest <FormControlLabel control={<Radio {...rest} />} label={label} sx={sx} />
}) => (
<FormControlLabel
control={(
<Radio {...rest} />
)}
label={label}
sx={sx}
/>
); );
export default RadioInput; export default RadioInput;

View File

@@ -12,14 +12,14 @@ import React from 'react';
import RadioInput, { RadioInputProps } from 'components/atoms/RadioInput'; import RadioInput, { RadioInputProps } from 'components/atoms/RadioInput';
interface IProps extends RadioInputProps { interface IProps extends RadioInputProps {
sortDescending?: boolean | null | undefined sortDescending?: boolean | null | undefined;
} }
const SortRadioInput: React.FC<IProps> = ({ const SortRadioInput: React.FC<IProps> = ({ sortDescending, ...rest }) => (
sortDescending, ...rest
}) => (
<RadioInput <RadioInput
checkedIcon={sortDescending ? <ArrowDownward color="primary" /> : <ArrowUpward color="primary" />} checkedIcon={
sortDescending ? <ArrowDownward color="primary" /> : <ArrowUpward color="primary" />
}
{...rest} {...rest}
/> />
); );

View File

@@ -19,8 +19,8 @@ function nextState(state: CheckState): CheckState {
} }
export interface ThreeStateCheckboxProps extends Omit<CheckboxProps, 'checked' | 'onChange'> { export interface ThreeStateCheckboxProps extends Omit<CheckboxProps, 'checked' | 'onChange'> {
checked?: boolean | undefined | null checked?: boolean | undefined | null;
onChange?: (checked: boolean | undefined | null) => void onChange?: (checked: boolean | undefined | null) => void;
} }
/** /**

View File

@@ -11,19 +11,11 @@ import React from 'react';
import ThreeStateCheckbox, { ThreeStateCheckboxProps } from 'components/atoms/ThreeStateCheckbox'; import ThreeStateCheckbox, { ThreeStateCheckboxProps } from 'components/atoms/ThreeStateCheckbox';
interface IProps extends ThreeStateCheckboxProps { interface IProps extends ThreeStateCheckboxProps {
label?: string label?: string;
} }
const ThreeStateCheckboxInput: React.FC<IProps> = ({ const ThreeStateCheckboxInput: React.FC<IProps> = ({ label, sx, ...rest }) => (
label, sx, ...rest <FormControlLabel control={<ThreeStateCheckbox {...rest} />} label={label} sx={sx} />
}) => (
<FormControlLabel
control={(
<ThreeStateCheckbox {...rest} />
)}
label={label}
sx={sx}
/>
); );
export default ThreeStateCheckboxInput; export default ThreeStateCheckboxInput;

View File

@@ -6,15 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles';
StyledEngineProvider, ThemeProvider,
} from '@mui/material/styles';
import LibraryOptionsContextProvider from 'components/library/LibraryOptionsProvider'; import LibraryOptionsContextProvider from 'components/library/LibraryOptionsProvider';
import NavBarContextProvider from 'components/navbar/NavBarContextProvider'; import NavBarContextProvider from 'components/navbar/NavBarContextProvider';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { import { BrowserRouter as Router, Route } from 'react-router-dom';
BrowserRouter as Router, Route,
} from 'react-router-dom';
import { SWRConfig } from 'swr'; import { SWRConfig } from 'swr';
import createTheme from 'theme'; import createTheme from 'theme';
import { QueryParamProvider } from 'use-query-params'; import { QueryParamProvider } from 'use-query-params';
@@ -23,25 +19,22 @@ import useLocalStorage from 'util/useLocalStorage';
import DarkTheme from 'components/context/DarkTheme'; import DarkTheme from 'components/context/DarkTheme';
interface Props { interface Props {
children: React.ReactNode children: React.ReactNode;
} }
const AppContext: React.FC<Props> = ({ children }) => { const AppContext: React.FC<Props> = ({ children }) => {
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>( const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
'darkTheme',
true,
);
const darkThemeContext = useMemo(() => ({ const darkThemeContext = useMemo(
() => ({
darkTheme, darkTheme,
setDarkTheme, setDarkTheme,
}), [darkTheme]); }),
const theme = useMemo(
() => createTheme(darkTheme),
[darkTheme], [darkTheme],
); );
const theme = useMemo(() => createTheme(darkTheme), [darkTheme]);
return ( return (
<SWRConfig value={{ fetcher }}> <SWRConfig value={{ fetcher }}>
<Router> <Router>
@@ -50,9 +43,7 @@ const AppContext: React.FC<Props> = ({ children }) => {
<DarkTheme.Provider value={darkThemeContext}> <DarkTheme.Provider value={darkThemeContext}>
<QueryParamProvider ReactRouterRoute={Route}> <QueryParamProvider ReactRouterRoute={Route}>
<LibraryOptionsContextProvider> <LibraryOptionsContextProvider>
<NavBarContextProvider> <NavBarContextProvider>{children}</NavBarContextProvider>
{children}
</NavBarContextProvider>
</LibraryOptionsContextProvider> </LibraryOptionsContextProvider>
</QueryParamProvider> </QueryParamProvider>
</DarkTheme.Provider> </DarkTheme.Provider>

View File

@@ -8,8 +8,8 @@
import React from 'react'; import React from 'react';
type ContextType = { type ContextType = {
darkTheme: boolean darkTheme: boolean;
setDarkTheme: React.Dispatch<React.SetStateAction<boolean>> setDarkTheme: React.Dispatch<React.SetStateAction<boolean>>;
}; };
const DarkTheme = React.createContext<ContextType>({ const DarkTheme = React.createContext<ContextType>({

View File

@@ -9,20 +9,20 @@ import React, { useContext, useEffect } from 'react';
type ContextType = { type ContextType = {
// Default back button url // Default back button url
defaultBackTo: string | undefined defaultBackTo: string | undefined;
setDefaultBackTo: React.Dispatch<React.SetStateAction<string | undefined>> setDefaultBackTo: React.Dispatch<React.SetStateAction<string | undefined>>;
// AppBar title // AppBar title
title: string title: string;
setTitle: (title: string) => void setTitle: (title: string) => void;
// AppBar action buttons // AppBar action buttons
action: any action: any;
setAction: React.Dispatch<React.SetStateAction<any>> setAction: React.Dispatch<React.SetStateAction<any>>;
// Allow default navbar to be overrided // Allow default navbar to be overrided
override: INavbarOverride override: INavbarOverride;
setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>> setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>>;
}; };
const NavBarContext = React.createContext<ContextType>({ const NavBarContext = React.createContext<ContextType>({

View File

@@ -24,8 +24,10 @@ const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: IManga
} }
}; };
const downloadedFilter = (downloaded: NullAndUndefined<boolean>, const downloadedFilter = (
{ downloadCount }: IMangaCard): boolean => { downloaded: NullAndUndefined<boolean>,
{ downloadCount }: IMangaCard,
): boolean => {
switch (downloaded) { switch (downloaded) {
case true: case true:
return !!downloadCount && downloadCount >= 1; return !!downloadCount && downloadCount >= 1;
@@ -46,7 +48,8 @@ const filterManga = (
query: NullAndUndefined<string>, query: NullAndUndefined<string>,
unread: NullAndUndefined<boolean>, unread: NullAndUndefined<boolean>,
downloaded: NullAndUndefined<boolean>, downloaded: NullAndUndefined<boolean>,
): IMangaCard[] => manga.filter((m) => { ): IMangaCard[] =>
manga.filter((m) => {
if (query) { if (query) {
return queryFilter(query, m); return queryFilter(query, m);
} }
@@ -70,10 +73,17 @@ const sortManga = (
const result = [...manga]; const result = [...manga];
switch (sort) { switch (sort) {
case 'sortAlph': result.sort(sortByTitle); break; case 'sortAlph':
case 'sortID': result.sort(sortById); break; result.sort(sortByTitle);
case 'sortToRead': result.sort(sortByUnread); break; break;
default: break; case 'sortID':
result.sort(sortById);
break;
case 'sortToRead':
result.sort(sortByUnread);
break;
default:
break;
} }
if (desc === true) { if (desc === true) {
@@ -84,20 +94,32 @@ const sortManga = (
}; };
const LibraryMangaGrid: React.FC<IMangaGridProps & { lastLibraryUpdate: number }> = ({ const LibraryMangaGrid: React.FC<IMangaGridProps & { lastLibraryUpdate: number }> = ({
mangas, isLoading, hasNextPage, lastPageNum, setLastPageNum, message, lastLibraryUpdate, mangas,
isLoading,
hasNextPage,
lastPageNum,
setLastPageNum,
message,
lastLibraryUpdate,
}) => { }) => {
const [query] = useQueryParam('query', StringParam); const [query] = useQueryParam('query', StringParam);
const { options } = useLibraryOptionsContext(); const { options } = useLibraryOptionsContext();
const { unread, downloaded } = options; const { unread, downloaded } = options;
const sortedManga = useMemo(() => sortManga(mangas, options.sorts, options.sortDesc), const sortedManga = useMemo(
[mangas, lastLibraryUpdate, options.sorts, options.sortDesc]); () => sortManga(mangas, options.sorts, options.sortDesc),
[mangas, lastLibraryUpdate, options.sorts, options.sortDesc],
);
const filteredManga = useMemo(() => filterManga(sortedManga, query, unread, downloaded), const filteredManga = useMemo(
[sortedManga, lastLibraryUpdate, query, unread, downloaded]); () => filterManga(sortedManga, query, unread, downloaded),
[sortedManga, lastLibraryUpdate, query, unread, downloaded],
);
const showFilteredOutMessage = (unread != null || downloaded != null || query) const showFilteredOutMessage =
&& filteredManga.length === 0 && mangas.length > 0; (unread != null || downloaded != null || query) &&
filteredManga.length === 0 &&
mangas.length > 0;
return ( return (
<MangaGrid <MangaGrid

View File

@@ -27,8 +27,8 @@ const SORT_OPTIONS: [LibrarySortMode, string][] = [
]; ];
interface IProps { interface IProps {
open: boolean, open: boolean;
onClose: () => void, onClose: () => void;
} }
const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => { const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
@@ -51,8 +51,16 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
if (key === 'filter') { if (key === 'filter') {
return ( return (
<> <>
<ThreeStateCheckboxInput label="Unread" checked={options.unread} onChange={(c) => handleFilterChange('unread', c)} /> <ThreeStateCheckboxInput
<ThreeStateCheckboxInput label="Downloaded" checked={options.downloaded} onChange={(c) => handleFilterChange('downloaded', c)} /> label="Unread"
checked={options.unread}
onChange={(c) => handleFilterChange('unread', c)}
/>
<ThreeStateCheckboxInput
label="Downloaded"
checked={options.downloaded}
onChange={(c) => handleFilterChange('downloaded', c)}
/>
</> </>
); );
} }
@@ -63,9 +71,11 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
label={label} label={label}
checked={options.sorts === mode} checked={options.sorts === mode}
sortDescending={options.sortDesc} sortDescending={options.sortDesc}
onClick={() => (mode !== options.sorts onClick={() =>
mode !== options.sorts
? handleFilterChange('sorts', mode) ? handleFilterChange('sorts', mode)
: handleFilterChange('sortDesc', !options.sortDesc))} : handleFilterChange('sortDesc', !options.sortDesc)
}
/> />
)); ));
} }
@@ -75,24 +85,44 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
<> <>
<FormLabel>Display mode</FormLabel> <FormLabel>Display mode</FormLabel>
<RadioGroup <RadioGroup
onChange={(e) => handleFilterChange('gridLayout', Number(e.target.value))} onChange={(e) =>
handleFilterChange('gridLayout', Number(e.target.value))
}
value={gridLayout} value={gridLayout}
> >
<RadioInput label="Compact grid" value={GridLayout.Compact} checked={gridLayout == null || gridLayout === GridLayout.Compact} /> <RadioInput
<RadioInput label="Comfortable grid" value={GridLayout.Comfortable} checked={gridLayout === GridLayout.Comfortable} /> label="Compact grid"
<RadioInput label="List" value={GridLayout.List} checked={gridLayout === GridLayout.List} /> value={GridLayout.Compact}
checked={
gridLayout == null || gridLayout === GridLayout.Compact
}
/>
<RadioInput
label="Comfortable grid"
value={GridLayout.Comfortable}
checked={gridLayout === GridLayout.Comfortable}
/>
<RadioInput
label="List"
value={GridLayout.List}
checked={gridLayout === GridLayout.List}
/>
</RadioGroup> </RadioGroup>
<FormLabel sx={{ mt: 2 }}>Badges</FormLabel> <FormLabel sx={{ mt: 2 }}>Badges</FormLabel>
<CheckboxInput <CheckboxInput
label="Unread Badges" label="Unread Badges"
checked={showUnreadBadge === true} checked={showUnreadBadge === true}
onChange={() => handleFilterChange('showUnreadBadge', !showUnreadBadge)} onChange={() =>
handleFilterChange('showUnreadBadge', !showUnreadBadge)
}
/> />
<CheckboxInput <CheckboxInput
label="Download Badges" label="Download Badges"
checked={showDownloadBadge === true} checked={showDownloadBadge === true}
onChange={() => handleFilterChange('showDownloadBadge', !showDownloadBadge)} onChange={() =>
handleFilterChange('showDownloadBadge', !showDownloadBadge)
}
/> />
</> </>
); );

View File

@@ -5,7 +5,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import LibraryOptionsContext, { DefaultLibraryOptions } from 'components/context/LibraryOptionsContext'; import LibraryOptionsContext, {
DefaultLibraryOptions,
} from 'components/context/LibraryOptionsContext';
import React from 'react'; import React from 'react';
import useLocalStorage from 'util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
@@ -14,7 +16,10 @@ interface IProps {
} }
const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => { const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => {
const [options, setOptions] = useLocalStorage<LibraryOptions>('libraryOptions', DefaultLibraryOptions); const [options, setOptions] = useLocalStorage<LibraryOptions>(
'libraryOptions',
DefaultLibraryOptions,
);
return ( return (
<LibraryOptionsContext.Provider value={{ options, setOptions }}> <LibraryOptionsContext.Provider value={{ options, setOptions }}>

View File

@@ -18,10 +18,7 @@ const LibraryToolbarMenu: React.FC = () => {
return ( return (
<> <>
<IconButton <IconButton onClick={() => setOpen(!open)} color={active ? 'warning' : 'default'}>
onClick={() => setOpen(!open)}
color={active ? 'warning' : 'default'}
>
<FilterList /> <FilterList />
</IconButton> </IconButton>
<LibraryOptionsPanel open={open} onClose={() => setOpen(false)} /> <LibraryOptionsPanel open={open} onClose={() => setOpen(false)} />

View File

@@ -8,7 +8,7 @@ import client from 'util/client';
import makeToast from 'components/util/Toast'; import makeToast from 'components/util/Toast';
interface IProgressProps { interface IProgressProps {
progress: number progress: number;
} }
function Progress({ progress }: IProgressProps) { function Progress({ progress }: IProgressProps) {
@@ -16,19 +16,19 @@ function Progress({ progress }: IProgressProps) {
<Box sx={{ display: 'grid', placeItems: 'center', position: 'relative' }}> <Box sx={{ display: 'grid', placeItems: 'center', position: 'relative' }}>
<CircularProgress variant="determinate" value={progress} /> <CircularProgress variant="determinate" value={progress} />
<Box sx={{ position: 'absolute' }}> <Box sx={{ position: 'absolute' }}>
<Typography fontSize="0.8rem"> <Typography fontSize="0.8rem">{`${Math.round(progress)}%`}</Typography>
{`${Math.round(progress)}%`}
</Typography>
</Box> </Box>
</Box> </Box>
); );
} }
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws'); const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace(
'http',
'ws',
);
interface IUpdateCheckerProps { interface IUpdateCheckerProps {
handleFinishedUpdate: (time: number) => void handleFinishedUpdate: (time: number) => void;
} }
function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) { function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
@@ -58,9 +58,8 @@ function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
const { running, statusMap } = JSON.parse(e.data) as IUpdateStatus; const { running, statusMap } = JSON.parse(e.data) as IUpdateStatus;
const { COMPLETE = [], RUNNING = [], PENDING = [] } = statusMap; const { COMPLETE = [], RUNNING = [], PENDING = [] } = statusMap;
const currentProgress = 100 * ( const currentProgress =
COMPLETE.length / (COMPLETE.length + RUNNING.length + PENDING.length) 100 * (COMPLETE.length / (COMPLETE.length + RUNNING.length + PENDING.length));
);
const isUpdateFinished = currentProgress === 100; const isUpdateFinished = currentProgress === 100;
const ignoreFaultyMessage = !updateStarted && !running && isUpdateFinished; const ignoreFaultyMessage = !updateStarted && !running && isUpdateFinished;

View File

@@ -1,6 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws'); const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace(
'http',
'ws',
);
const useSubscription = <T>(path: string, callback?: (newValue: T) => boolean | void) => { const useSubscription = <T>(path: string, callback?: (newValue: T) => boolean | void) => {
const [state, setState] = useState<T | undefined>(); const [state, setState] = useState<T | undefined>();

View File

@@ -15,9 +15,7 @@ import DoneAll from '@mui/icons-material/DoneAll';
import Download from '@mui/icons-material/Download'; import Download from '@mui/icons-material/Download';
import MoreVertIcon from '@mui/icons-material/MoreVert'; import MoreVertIcon from '@mui/icons-material/MoreVert';
import RemoveDone from '@mui/icons-material/RemoveDone'; import RemoveDone from '@mui/icons-material/RemoveDone';
import { import { CardActionArea, Checkbox, ListItemIcon, ListItemText, Stack } from '@mui/material';
CardActionArea, Checkbox, ListItemIcon, ListItemText, Stack,
} from '@mui/material';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent'; import CardContent from '@mui/material/CardContent';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
@@ -33,19 +31,24 @@ import { BACK } from 'util/useBackTo';
import { getUploadDateString } from 'util/date'; import { getUploadDateString } from 'util/date';
interface IProps { interface IProps {
chapter: IChapter chapter: IChapter;
triggerChaptersUpdate: () => void triggerChaptersUpdate: () => void;
downloadChapter: IDownloadChapter | undefined downloadChapter: IDownloadChapter | undefined;
showChapterNumber: boolean showChapterNumber: boolean;
onSelect: (selected: boolean) => void onSelect: (selected: boolean) => void;
selected: boolean | null selected: boolean | null;
} }
const ChapterCard: React.FC<IProps> = (props: IProps) => { const ChapterCard: React.FC<IProps> = (props: IProps) => {
const theme = useTheme(); const theme = useTheme();
const { const {
chapter, triggerChaptersUpdate, downloadChapter: dc, showChapterNumber, onSelect, selected, chapter,
triggerChaptersUpdate,
downloadChapter: dc,
showChapterNumber,
onSelect,
selected,
} = props; } = props;
const isSelecting = selected !== null; const isSelecting = selected !== null;
@@ -71,7 +74,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
if (key === 'read') { if (key === 'read') {
formData.append('lastPageRead', '1'); 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()); .then(() => triggerChaptersUpdate());
}; };
@@ -81,7 +85,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
}; };
const deleteChapter = () => { const deleteChapter = () => {
client.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`) client
.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`)
.then(() => triggerChaptersUpdate()); .then(() => triggerChaptersUpdate());
handleClose(); handleClose();
}; };
@@ -112,7 +117,10 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
> >
<CardActionArea <CardActionArea
component={Link} 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={{ style={{
color: theme.palette.text[chapter.read ? 'disabled' : 'primary'], color: theme.palette.text[chapter.read ? 'disabled' : 'primary'],
}} }}
@@ -130,13 +138,16 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
<Stack direction="column" flex={1}> <Stack direction="column" flex={1}>
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
{chapter.bookmarked && ( {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} {showChapterNumber
</Typography> ? `Chapter ${chapter.chapterNumber}`
<Typography variant="caption"> : chapter.name}
{chapter.scanlator}
</Typography> </Typography>
<Typography variant="caption">{chapter.scanlator}</Typography>
<Typography variant="caption"> <Typography variant="caption">
{getUploadDateString(chapter.uploadDate)} {getUploadDateString(chapter.uploadDate)}
{isDownloaded && ' • Downloaded'} {isDownloaded && ' • Downloaded'}
@@ -164,18 +175,14 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
<ListItemIcon> <ListItemIcon>
<CheckBoxOutlineBlank fontSize="small" /> <CheckBoxOutlineBlank fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>Select</ListItemText>
Select
</ListItemText>
</MenuItem> </MenuItem>
{isDownloaded && ( {isDownloaded && (
<MenuItem onClick={deleteChapter}> <MenuItem onClick={deleteChapter}>
<ListItemIcon> <ListItemIcon>
<Delete fontSize="small" /> <Delete fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>Delete</ListItemText>
Delete
</ListItemText>
</MenuItem> </MenuItem>
)} )}
{canBeDownloaded && ( {canBeDownloaded && (
@@ -183,9 +190,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
<ListItemIcon> <ListItemIcon>
<Download fontSize="small" /> <Download fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>Download</ListItemText>
Download
</ListItemText>
</MenuItem> </MenuItem>
)} )}
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}> <MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
@@ -212,9 +217,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
<ListItemIcon> <ListItemIcon>
<DoneAll fontSize="small" /> <DoneAll fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>Mark previous as Read</ListItemText>
Mark previous as Read
</ListItemText>
</MenuItem> </MenuItem>
</Menu> </Menu>
</Card> </Card>

View File

@@ -5,9 +5,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { import { Button, CircularProgress, Stack } from '@mui/material';
Button, CircularProgress, Stack,
} from '@mui/material';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { styled } from '@mui/system'; import { styled } from '@mui/system';
import useSubscription from 'components/library/useSubscription'; import useSubscription from 'components/library/useSubscription';
@@ -17,10 +15,7 @@ import { filterAndSortChapters, useChapterOptions } from 'components/manga/util'
import EmptyView from 'components/util/EmptyView'; import EmptyView from 'components/util/EmptyView';
import { interpolate } from 'components/util/helpers'; import { interpolate } from 'components/util/helpers';
import makeToast from 'components/util/Toast'; import makeToast from 'components/util/Toast';
import React, { import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
ComponentProps,
useEffect, useMemo, useRef, useState,
} from 'react';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
import client, { useQuery } from 'util/client'; import client, { useQuery } from 'util/client';
import ChaptersToolbarMenu from 'components/manga/ChaptersToolbarMenu'; import ChaptersToolbarMenu from 'components/manga/ChaptersToolbarMenu';
@@ -66,13 +61,13 @@ const actionsStrings = {
}; };
export interface IChapterWithMeta { export interface IChapterWithMeta {
chapter: IChapter chapter: IChapter;
downloadChapter: IDownloadChapter | undefined downloadChapter: IDownloadChapter | undefined;
selected: boolean | null selected: boolean | null;
} }
interface IProps { interface IProps {
mangaId: string mangaId: string;
} }
const ChapterList: React.FC<IProps> = ({ mangaId }) => { const ChapterList: React.FC<IProps> = ({ mangaId }) => {
@@ -92,9 +87,9 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
if (prevQueueRef.current && queue) { if (prevQueueRef.current && queue) {
const prevQueue = prevQueueRef.current; const prevQueue = prevQueueRef.current;
const changedDownloads = queue.filter((cd) => { const changedDownloads = queue.filter((cd) => {
const prevChapterDownload = prevQueue const prevChapterDownload = prevQueue.find(
.find((pcd) => cd.chapterIndex === pcd.chapterIndex (pcd) => cd.chapterIndex === pcd.chapterIndex && cd.mangaId === pcd.mangaId,
&& cd.mangaId === pcd.mangaId); );
if (!prevChapterDownload) return true; if (!prevChapterDownload) return true;
return cd.state !== prevChapterDownload.state; return cd.state !== prevChapterDownload.state;
}); });
@@ -107,13 +102,19 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
prevQueueRef.current = queue; prevQueueRef.current = queue;
}, [queue]); }, [queue]);
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), // const visibleChapters = useMemo(
[chapters, options]); () => filterAndSortChapters(chapters, options), //
[chapters, options],
);
const firstUnreadChapter = useMemo(() => visibleChapters.slice() const firstUnreadChapter = useMemo(
() =>
visibleChapters
.slice()
.reverse() .reverse()
.find((c) => c.read === false), .find((c) => c.read === false),
[visibleChapters]); [visibleChapters],
);
const handleSelection = (index: number) => { const handleSelection = (index: number) => {
const chapter = visibleChapters[index]; const chapter = visibleChapters[index];
@@ -139,7 +140,10 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
setSelection(null); setSelection(null);
}; };
const handleFabAction: ComponentProps<typeof SelectionFAB>['onAction'] = (action, actionChapters) => { const handleFabAction: ComponentProps<typeof SelectionFAB>['onAction'] = (
action,
actionChapters,
) => {
if (actionChapters.length === 0) return; if (actionChapters.length === 0) return;
const chapterIds = actionChapters.map(({ chapter }) => chapter.id); const chapterIds = actionChapters.map(({ chapter }) => chapter.id);
@@ -160,14 +164,22 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
} }
actionPromise actionPromise
.then(() => makeToast(interpolate(chapterIds.length, actionsStrings[action].success), 'success')) .then(() =>
makeToast(
interpolate(chapterIds.length, actionsStrings[action].success),
'success',
),
)
.then(() => mutate()) .then(() => mutate())
.catch(() => makeToast(interpolate(chapterIds.length, actionsStrings[action].error), 'error')); .catch(() =>
makeToast(interpolate(chapterIds.length, actionsStrings[action].error), 'error'),
);
}; };
if (loading) { if (loading) {
return ( return (
<div style={{ <div
style={{
margin: '10px auto', margin: '10px auto',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
@@ -183,8 +195,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
const chaptersWithMeta: IChapterWithMeta[] = visibleChapters.map((chapter) => { const chaptersWithMeta: IChapterWithMeta[] = visibleChapters.map((chapter) => {
const downloadChapter = queue?.find( const downloadChapter = queue?.find(
(cd) => cd.chapterIndex === chapter.index (cd) => cd.chapterIndex === chapter.index && cd.mangaId === chapter.mangaId,
&& cd.mangaId === chapter.mangaId,
); );
const selected = selection?.includes(chapter.id) ?? null; const selected = selection?.includes(chapter.id) ?? null;
return { return {
@@ -194,7 +205,8 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
}; };
}); });
const selectedChapters = (selection === null) const selectedChapters =
selection === null
? null ? null
: chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id)); : chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id));
@@ -206,32 +218,38 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
sx={{ sx={{
m: 1, mb: 0, mr: 2, minHeight: 40, m: 1,
mb: 0,
mr: 2,
minHeight: 40,
}} }}
> >
<Typography variant="h5"> <Typography variant="h5">
{`${visibleChapters.length} Chapter${visibleChapters.length === 1 ? '' : 's'}`} {`${visibleChapters.length} Chapter${
visibleChapters.length === 1 ? '' : 's'
}`}
</Typography> </Typography>
{selection === null ? ( {selection === null ? (
<ChaptersToolbarMenu options={options} optionsDispatch={dispatch} /> <ChaptersToolbarMenu options={options} optionsDispatch={dispatch} />
) : ( ) : (
<Stack direction="row"> <Stack direction="row">
<Button size="small" onClick={handleSelectAll}>Select all</Button> <Button size="small" onClick={handleSelectAll}>
<Button size="small" onClick={handleClear}>Clear</Button> Select all
</Button>
<Button size="small" onClick={handleClear}>
Clear
</Button>
</Stack> </Stack>
)} )}
</Stack> </Stack>
{noChaptersFound && ( {noChaptersFound && <EmptyView message="No chapters found" />}
<EmptyView message="No chapters found" /> {noChaptersMatchingFilter && <EmptyView message="No chapters matching filter" />}
)}
{noChaptersMatchingFilter && (
<EmptyView message="No chapters matching filter" />
)}
<StyledVirtuoso <StyledVirtuoso
style={{ // override Virtuoso default values and set them with class style={{
// override Virtuoso default values and set them with class
height: 'undefined', height: 'undefined',
// 900 is the md breakpoint in MUI // 900 is the md breakpoint in MUI
overflowY: window.innerWidth < 900 ? 'visible' : 'auto', overflowY: window.innerWidth < 900 ? 'visible' : 'auto',
@@ -250,10 +268,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
/> />
</Stack> </Stack>
{selectedChapters !== null ? ( {selectedChapters !== null ? (
<SelectionFAB <SelectionFAB selectedChapters={selectedChapters} onAction={handleFabAction} />
selectedChapters={selectedChapters}
onAction={handleFabAction}
/>
) : ( ) : (
firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={mangaId} /> firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={mangaId} />
)} )}

View File

@@ -14,10 +14,10 @@ import React from 'react';
import { SORT_OPTIONS } from 'components/manga/util'; import { SORT_OPTIONS } from 'components/manga/util';
interface IProps { interface IProps {
open: boolean open: boolean;
onClose: () => void onClose: () => void;
options: ChapterListOptions options: ChapterListOptions;
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction> optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
} }
const TITLES = { const TITLES = {
@@ -26,9 +26,7 @@ const TITLES = {
display: 'Display', display: 'Display',
}; };
const ChapterOptions: React.FC<IProps> = ({ const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispatch }) => (
open, onClose, options, optionsDispatch,
}) => (
<OptionsTabs<'filter' | 'sort' | 'display'> <OptionsTabs<'filter' | 'sort' | 'display'>
open={open} open={open}
onClose={onClose} onClose={onClose}
@@ -39,9 +37,39 @@ const ChapterOptions: React.FC<IProps> = ({
if (key === 'filter') { if (key === 'filter') {
return ( return (
<> <>
<ThreeStateCheckboxInput label="Unread" checked={options.unread} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'unread', filterValue: c })} /> <ThreeStateCheckboxInput
<ThreeStateCheckboxInput label="Downloaded" checked={options.downloaded} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'downloaded', filterValue: c })} /> label="Unread"
<ThreeStateCheckboxInput label="Bookmarked" checked={options.bookmarked} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'bookmarked', filterValue: c })} /> 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} label={label}
checked={options.sortBy === mode} checked={options.sortBy === mode}
sortDescending={options.reverse} sortDescending={options.reverse}
onClick={() => (mode !== options.sortBy onClick={() =>
mode !== options.sortBy
? optionsDispatch({ type: 'sortBy', sortBy: mode }) ? optionsDispatch({ type: 'sortBy', sortBy: mode })
: optionsDispatch({ type: 'sortReverse' }))} : optionsDispatch({ type: 'sortReverse' })
}
/> />
)); ));
} }
if (key === 'display') { if (key === 'display') {
return ( 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="Source Title" value={false} />
<RadioInput label="Chapter Number" value /> <RadioInput label="Chapter Number" value />
</RadioGroup> </RadioGroup>

View File

@@ -12,8 +12,8 @@ import ChapterOptions from 'components/manga/ChapterOptions';
import { isFilterActive } from 'components/manga/util'; import { isFilterActive } from 'components/manga/util';
interface IProps { interface IProps {
options: ChapterListOptions options: ChapterListOptions;
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction> optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
} }
const ChaptersToolbarMenu = ({ options, optionsDispatch }: IProps) => { const ChaptersToolbarMenu = ({ options, optionsDispatch }: IProps) => {

View File

@@ -17,7 +17,8 @@ import { mutate } from 'swr';
import client from 'util/client'; import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
const useStyles = (inLibrary: boolean) => makeStyles((theme: Theme) => ({ const useStyles = (inLibrary: boolean) =>
makeStyles((theme: Theme) => ({
root: { root: {
width: '100%', width: '100%',
[theme.breakpoints.up('md')]: { [theme.breakpoints.up('md')]: {
@@ -112,7 +113,7 @@ const useStyles = (inLibrary: boolean) => makeStyles((theme: Theme) => ({
})); }));
interface IProps { interface IProps {
manga: IManga manga: IManga;
} }
function getSourceName(source: ISource) { function getSourceName(source: ISource) {
@@ -133,14 +134,24 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
const classes = useStyles(manga.inLibrary)(); const classes = useStyles(manga.inLibrary)();
const addToLibrary = () => { const addToLibrary = () => {
mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`, { ...manga, inLibrary: true }, { revalidate: false }); mutate(
client.get(`/api/v1/manga/${manga.id}/library/`) `/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`)); .then(() => mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`));
}; };
const removeFromLibrary = () => { const removeFromLibrary = () => {
mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`, { ...manga, inLibrary: false }, { revalidate: false }); mutate(
client.delete(`/api/v1/manga/${manga.id}/library/`) `/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`)); .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.top}>
<div className={classes.leftRight}> <div className={classes.leftRight}>
<div className={classes.leftSide}> <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>
<div className={classes.rightSide}> <div className={classes.rightSide}>
<h1> <h1>{manga.title}</h1>
{manga.title}
</h1>
<h3> <h3>
{'Author: '} {'Author: '}
<span>{getValueOrUnknown(manga.author)}</span> <span>{getValueOrUnknown(manga.author)}</span>
@@ -163,20 +175,21 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
{'Artist: '} {'Artist: '}
<span>{getValueOrUnknown(manga.artist)}</span> <span>{getValueOrUnknown(manga.artist)}</span>
</h3> </h3>
<h3> <h3>{`Status: ${manga.status}`}</h3>
{`Status: ${manga.status}`} <h3>{`Source: ${getSourceName(manga.source)}`}</h3>
</h3>
<h3>
{`Source: ${getSourceName(manga.source)}`}
</h3>
</div> </div>
</div> </div>
<div className={classes.buttons}> <div className={classes.buttons}>
<div> <div>
<IconButton onClick={manga.inLibrary ? removeFromLibrary : addToLibrary} size="large"> <IconButton
{manga.inLibrary onClick={manga.inLibrary ? removeFromLibrary : addToLibrary}
? <FavoriteIcon sx={{ mr: 1 }} /> size="large"
: <FavoriteBorderIcon sx={{ mr: 1 }} />} >
{manga.inLibrary ? (
<FavoriteIcon sx={{ mr: 1 }} />
) : (
<FavoriteBorderIcon sx={{ mr: 1 }} />
)}
<Typography sx={{ fontSize: { xs: '0.75em', sm: '0.85em' } }}> <Typography sx={{ fontSize: { xs: '0.75em', sm: '0.85em' } }}>
{manga.inLibrary ? 'In Library' : 'Add To Library'} {manga.inLibrary ? 'In Library' : 'Add To Library'}
</Typography> </Typography>
@@ -198,7 +211,9 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
<p>{manga.description}</p> <p>{manga.description}</p>
</div> </div>
<div className={classes.genre}> <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> </div>
</div> </div>

View File

@@ -9,7 +9,14 @@ import Label from '@mui/icons-material/Label';
import MoreHoriz from '@mui/icons-material/MoreHoriz'; import MoreHoriz from '@mui/icons-material/MoreHoriz';
import Refresh from '@mui/icons-material/Refresh'; import Refresh from '@mui/icons-material/Refresh';
import { import {
IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip, useMediaQuery, useTheme, IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tooltip,
useMediaQuery,
useTheme,
} from '@mui/material'; } from '@mui/material';
import CategorySelect from 'components/navbar/action/CategorySelect'; import CategorySelect from 'components/navbar/action/CategorySelect';
import React, { useState } from 'react'; import React, { useState } from 'react';
@@ -37,13 +44,22 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
{isLargeScreen && ( {isLargeScreen && (
<> <>
<Tooltip title="Reload data from source"> <Tooltip title="Reload data from source">
<IconButton onClick={() => { onRefresh(); }} disabled={refreshing}> <IconButton
onClick={() => {
onRefresh();
}}
disabled={refreshing}
>
<Refresh /> <Refresh />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{manga.inLibrary && ( {manga.inLibrary && (
<Tooltip title="Edit manga categories"> <Tooltip title="Edit manga categories">
<IconButton onClick={() => { setEditCategories(true); }}> <IconButton
onClick={() => {
setEditCategories(true);
}}
>
<Label /> <Label />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -71,37 +87,35 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
}} }}
> >
<MenuItem <MenuItem
onClick={() => { onRefresh(); handleClose(); }} onClick={() => {
onRefresh();
handleClose();
}}
disabled={refreshing} disabled={refreshing}
> >
<ListItemIcon> <ListItemIcon>
<Refresh fontSize="small" /> <Refresh fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>Reload data from source</ListItemText>
Reload data from source
</ListItemText>
</MenuItem> </MenuItem>
{manga.inLibrary && ( {manga.inLibrary && (
<MenuItem <MenuItem
onClick={() => { setEditCategories(true); handleClose(); }} onClick={() => {
setEditCategories(true);
handleClose();
}}
> >
<ListItemIcon> <ListItemIcon>
<Label fontSize="small" /> <Label fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>Edit manga categories</ListItemText>
Edit manga categories
</ListItemText>
</MenuItem> </MenuItem>
)} )}
</Menu> </Menu>
</> </>
)} )}
<CategorySelect <CategorySelect open={editCategories} setOpen={setEditCategories} mangaId={manga.id} />
open={editCategories}
setOpen={setEditCategories}
mangaId={manga.id}
/>
</> </>
); );
}; };

View File

@@ -12,19 +12,25 @@ import { PlayArrow } from '@mui/icons-material';
import { BACK } from 'util/useBackTo'; import { BACK } from 'util/useBackTo';
interface ResumeFABProps { interface ResumeFABProps {
chapter: IChapter chapter: IChapter;
mangaId: string mangaId: string;
} }
export default function ResumeFab(props: ResumeFABProps) { export default function ResumeFab(props: ResumeFABProps) {
const { chapter: { index, lastPageRead }, mangaId } = props; const {
chapter: { index, lastPageRead },
mangaId,
} = props;
return ( return (
<Fab <Fab
sx={{ position: 'fixed', bottom: '2em', right: '3em' }} sx={{ position: 'fixed', bottom: '2em', right: '3em' }}
component={Link} component={Link}
variant="extended" variant="extended"
color="primary" 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 /> <PlayArrow />
{index === 1 ? 'Start' : 'Resume'} {index === 1 ? 'Start' : 'Resume'}

View File

@@ -6,20 +6,24 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import MoreHoriz from '@mui/icons-material/MoreHoriz'; import MoreHoriz from '@mui/icons-material/MoreHoriz';
import { import { Fab, Menu } from '@mui/material';
Fab, Menu,
} from '@mui/material';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
import { pluralize } from 'components/util/helpers'; import { pluralize } from 'components/util/helpers';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import type { IChapterWithMeta } from 'components/manga/ChapterList'; import type { IChapterWithMeta } from 'components/manga/ChapterList';
import SelectionFABActionItem from 'components/manga/SelectionFABActionItem'; 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 { interface SelectionFABProps {
selectedChapters: IChapterWithMeta[] selectedChapters: IChapterWithMeta[];
onAction: (action: SelectionAction, chapters: IChapterWithMeta[]) => void onAction: (action: SelectionAction, chapters: IChapterWithMeta[]) => void;
} }
const SelectionFAB: React.FC<SelectionFABProps> = (props) => { const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
@@ -38,7 +42,10 @@ const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
return ( return (
<Box <Box
sx={{ sx={{
position: 'fixed', bottom: '2em', right: '3em', pt: 1, position: 'fixed',
bottom: '2em',
right: '3em',
pt: 1,
}} }}
ref={anchorEl} ref={anchorEl}
> >

View File

@@ -17,10 +17,10 @@ import type { IChapterWithMeta } from 'components/manga/ChapterList';
import type { SelectionAction } from 'components/manga/SelectionFAB'; import type { SelectionAction } from 'components/manga/SelectionFAB';
interface IProps { interface IProps {
action: SelectionAction action: SelectionAction;
matchingChapters: IChapterWithMeta[] matchingChapters: IChapterWithMeta[];
title: string title: string;
onClick: (action: SelectionAction, chapters: IChapterWithMeta[]) => void onClick: (action: SelectionAction, chapters: IChapterWithMeta[]) => void;
} }
const ICONS = { const ICONS = {
@@ -32,16 +32,11 @@ const ICONS = {
mark_as_unread: RemoveDone, mark_as_unread: RemoveDone,
}; };
const SelectionFABActionItem: React.FC<IProps> = ({ const SelectionFABActionItem: React.FC<IProps> = ({ action, matchingChapters, onClick, title }) => {
action, matchingChapters, onClick, title,
}) => {
const count = matchingChapters.length; const count = matchingChapters.length;
const Icon = ICONS[action]; const Icon = ICONS[action];
return ( return (
<MenuItem <MenuItem onClick={() => onClick(action, matchingChapters)} disabled={count === 0}>
onClick={() => onClick(action, matchingChapters)}
disabled={count === 0}
>
<ListItemIcon> <ListItemIcon>
<Icon fontSize="small" /> <Icon fontSize="small" />
</ListItemIcon> </ListItemIcon>

View File

@@ -16,10 +16,14 @@ export const useRefreshManga = (mangaId: string) => {
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
setFetchingOnline(true); setFetchingOnline(true);
await Promise.all([ await Promise.all([
fetcher(`/api/v1/manga/${mangaId}/?onlineFetch=true`) fetcher(`/api/v1/manga/${mangaId}/?onlineFetch=true`).then((res) =>
.then((res) => mutate(`/api/v1/manga/${mangaId}/?onlineFetch=false`, res, { revalidate: false })), 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}/chapters?onlineFetch=true`).then((res) =>
mutate(`/api/v1/manga/${mangaId}/chapters?onlineFetch=false`, res, {
revalidate: false,
}),
),
]).finally(() => setFetchingOnline(false)); ]).finally(() => setFetchingOnline(false));
}, [mangaId]); }, [mangaId]);

View File

@@ -17,15 +17,15 @@ const defaultChapterOptions: ChapterListOptions = {
showChapterNumber: false, showChapterNumber: false,
}; };
function chapterOptionsReducer(state: ChapterListOptions, function chapterOptionsReducer(
actions: ChapterOptionsReducerAction) state: ChapterListOptions,
: ChapterListOptions { actions: ChapterOptionsReducerAction,
): ChapterListOptions {
switch (actions.type) { switch (actions.type) {
case 'filter': case 'filter':
// eslint-disable-next-line no-case-declarations // eslint-disable-next-line no-case-declarations
const active = state.unread !== false const active =
&& state.downloaded !== false state.unread !== false && state.downloaded !== false && state.bookmarked !== false;
&& state.bookmarked !== false;
return { return {
...state, ...state,
active, active,
@@ -53,8 +53,10 @@ export function unreadFilter(unread: NullAndUndefined<boolean>, { read: isChapte
} }
} }
function downloadFilter(downloaded: NullAndUndefined<boolean>, function downloadFilter(
{ downloaded: chapterDownload }: IChapter) { downloaded: NullAndUndefined<boolean>,
{ downloaded: chapterDownload }: IChapter,
) {
switch (downloaded) { switch (downloaded) {
case true: case true:
return chapterDownload; return chapterDownload;
@@ -65,8 +67,10 @@ function downloadFilter(downloaded: NullAndUndefined<boolean>,
} }
} }
function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>, function bookmarkedFilter(
{ bookmarked: chapterBookmarked }: IChapter) { bookmarked: NullAndUndefined<boolean>,
{ bookmarked: chapterBookmarked }: IChapter,
) {
switch (bookmarked) { switch (bookmarked) {
case true: case true:
return chapterBookmarked; return chapterBookmarked;
@@ -77,14 +81,20 @@ function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>,
} }
} }
export function filterAndSortChapters(chapters: IChapter[], options: ChapterListOptions) export function filterAndSortChapters(
: IChapter[] { chapters: IChapter[],
options: ChapterListOptions,
): IChapter[] {
const filtered = options.active const filtered = options.active
? chapters.filter((chp) => unreadFilter(options.unread, chp) ? chapters.filter(
&& downloadFilter(options.downloaded, chp) (chp) =>
&& bookmarkedFilter(options.bookmarked, chp)) unreadFilter(options.unread, chp) &&
downloadFilter(options.downloaded, chp) &&
bookmarkedFilter(options.bookmarked, chp),
)
: [...chapters]; : [...chapters];
const Sorted = options.sortBy === 'fetchedAt' const Sorted =
options.sortBy === 'fetchedAt'
? filtered.sort((a, b) => a.fetchedAt - b.fetchedAt) ? filtered.sort((a, b) => a.fetchedAt - b.fetchedAt)
: filtered; : filtered;
if (options.reverse) { if (options.reverse) {
@@ -93,12 +103,11 @@ export function filterAndSortChapters(chapters: IChapter[], options: ChapterList
return Sorted; return Sorted;
} }
export const useChapterOptions = (mangaId: string) => useReducerLocalStorage< export const useChapterOptions = (mangaId: string) =>
ChapterListOptions, useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>(
ChapterOptionsReducerAction
>(
chapterOptionsReducer, chapterOptionsReducer,
`${mangaId}filterOptions`, defaultChapterOptions, `${mangaId}filterOptions`,
defaultChapterOptions,
); );
export const SORT_OPTIONS: [ChapterSortMode, string][] = [ export const SORT_OPTIONS: [ChapterSortMode, string][] = [

View File

@@ -12,7 +12,7 @@ import { Box } from '@mui/system';
import React from 'react'; import React from 'react';
interface DownloadStateIndicatorProps { interface DownloadStateIndicatorProps {
download: IDownloadChapter download: IDownloadChapter;
} }
const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ download }) => ( const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ download }) => (
@@ -25,10 +25,7 @@ const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ downloa
}} }}
> >
{download.progress !== 0 && ( {download.progress !== 0 && (
<CircularProgress <CircularProgress variant="determinate" value={download.progress * 100} />
variant="determinate"
value={download.progress * 100}
/>
)} )}
<Box <Box
sx={{ sx={{

View File

@@ -5,22 +5,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { import { Drawer } from '@mui/material';
Drawer,
} from '@mui/material';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
import React from 'react'; import React from 'react';
interface IProps { interface IProps {
open: boolean open: boolean;
onClose: () => void onClose: () => void;
children: React.ReactNode children: React.ReactNode;
minHeight?: number minHeight?: number;
} }
const OptionsPanel: React.FC<IProps> = ({ const OptionsPanel: React.FC<IProps> = ({ open, onClose, children, minHeight }) => (
open, onClose, children, minHeight,
}) => (
<Drawer <Drawer
anchor="bottom" anchor="bottom"
open={open} open={open}
@@ -34,9 +30,7 @@ const OptionsPanel: React.FC<IProps> = ({
}, },
}} }}
> >
<Box> <Box>{children}</Box>
{children}
</Box>
</Drawer> </Drawer>
); );

View File

@@ -11,25 +11,26 @@ import React, { useState } from 'react';
import OptionsPanel from 'components/molecules/OptionsPanel'; import OptionsPanel from 'components/molecules/OptionsPanel';
interface IProps<T = string> { interface IProps<T = string> {
open: boolean open: boolean;
onClose: () => void onClose: () => void;
tabs: T[] tabs: T[];
tabTitle: (key: T) => React.ReactNode tabTitle: (key: T) => React.ReactNode;
tabContent: (key: T) => React.ReactNode tabContent: (key: T) => React.ReactNode;
minHeight?: number minHeight?: number;
} }
const OptionsTabs = <T extends string = string>({ const OptionsTabs = <T extends string = string>({
open, onClose, tabs, tabTitle, tabContent, minHeight, open,
onClose,
tabs,
tabTitle,
tabContent,
minHeight,
}: IProps<T>) => { }: IProps<T>) => {
const [tabNum, setTabNum] = useState(0); const [tabNum, setTabNum] = useState(0);
return ( return (
<OptionsPanel <OptionsPanel open={open} onClose={onClose} minHeight={minHeight}>
open={open}
onClose={onClose}
minHeight={minHeight}
>
<Tabs <Tabs
value={tabNum} value={tabNum}
variant="fullWidth" variant="fullWidth"
@@ -43,9 +44,7 @@ const OptionsTabs = <T extends string = string>({
</Tabs> </Tabs>
{tabs.map((tab, tabIndex) => ( {tabs.map((tab, tabIndex) => (
<TabPanel key={tab} index={tabIndex} currentIndex={tabNum}> <TabPanel key={tab} index={tabIndex} currentIndex={tabNum}>
<Stack sx={{ px: 3, py: 1, minHeight }}> <Stack sx={{ px: 3, py: 1, minHeight }}>{tabContent(tab)}</Stack>
{tabContent(tab)}
</Stack>
</TabPanel> </TabPanel>
))} ))}
</OptionsPanel> </OptionsPanel>

View File

@@ -39,37 +39,43 @@ const navbarItems: Array<NavbarItem> = [
SelectedIconComponent: CollectionsBookmarkIcon, SelectedIconComponent: CollectionsBookmarkIcon,
IconComponent: CollectionsOutlinedBookmarkIcon, IconComponent: CollectionsOutlinedBookmarkIcon,
show: 'both', show: 'both',
}, { },
{
path: '/updates', path: '/updates',
title: 'Updates', title: 'Updates',
SelectedIconComponent: NewReleasesIcon, SelectedIconComponent: NewReleasesIcon,
IconComponent: NewReleasesOutlinedIcon, IconComponent: NewReleasesOutlinedIcon,
show: 'both', show: 'both',
}, { },
{
path: '/extensions', path: '/extensions',
title: 'Extensions', title: 'Extensions',
SelectedIconComponent: ExtensionIcon, SelectedIconComponent: ExtensionIcon,
IconComponent: ExtensionOutlinedIcon, IconComponent: ExtensionOutlinedIcon,
show: 'desktop', show: 'desktop',
}, { },
{
path: '/sources', path: '/sources',
title: 'Sources', title: 'Sources',
SelectedIconComponent: ExploreIcon, SelectedIconComponent: ExploreIcon,
IconComponent: ExploreOutlinedIcon, IconComponent: ExploreOutlinedIcon,
show: 'desktop', show: 'desktop',
}, { },
{
path: '/browse', path: '/browse',
title: 'Browse', title: 'Browse',
SelectedIconComponent: ExploreIcon, SelectedIconComponent: ExploreIcon,
IconComponent: ExploreOutlinedIcon, IconComponent: ExploreOutlinedIcon,
show: 'mobile', show: 'mobile',
}, { },
{
path: '/downloads', path: '/downloads',
title: 'Downloads', title: 'Downloads',
SelectedIconComponent: GetAppIcon, SelectedIconComponent: GetAppIcon,
IconComponent: GetAppOutlinedIcon, IconComponent: GetAppOutlinedIcon,
show: 'both', show: 'both',
}, { },
{
path: '/settings', path: '/settings',
title: 'Settings', title: 'Settings',
SelectedIconComponent: SettingsIcon, SelectedIconComponent: SettingsIcon,
@@ -94,7 +100,9 @@ export default function DefaultNavBar() {
let navbar = <></>; let navbar = <></>;
if (isMobileWidth) { if (isMobileWidth) {
if (isMainRoute) { if (isMainRoute) {
navbar = <MobileBottomBar navBarItems={navbarItems.filter((it) => it.show !== 'desktop')} />; navbar = (
<MobileBottomBar navBarItems={navbarItems.filter((it) => it.show !== 'desktop')} />
);
} }
} else { } else {
navbar = <DesktopSideBar navBarItems={navbarItems.filter((it) => it.show !== 'mobile')} />; navbar = <DesktopSideBar navBarItems={navbarItems.filter((it) => it.show !== 'mobile')} />;
@@ -124,7 +132,12 @@ export default function DefaultNavBar() {
<ArrowBack /> <ArrowBack />
</IconButton> </IconButton>
)} )}
<Typography variant={isMobileWidth ? 'h6' : 'h5'} sx={{ flexGrow: 1 }} noWrap textOverflow="ellipsis"> <Typography
variant={isMobileWidth ? 'h6' : 'h5'}
sx={{ flexGrow: 1 }}
noWrap
textOverflow="ellipsis"
>
{title} {title}
</Typography> </Typography>
{action} {action}
@@ -137,7 +150,7 @@ export default function DefaultNavBar() {
} }
interface INavbarToolbarProps { interface INavbarToolbarProps {
children?: React.ReactNode children?: React.ReactNode;
} }
export const NavbarToolbar: React.FC<INavbarToolbarProps> = ({ children }) => { export const NavbarToolbar: React.FC<INavbarToolbarProps> = ({ children }) => {

View File

@@ -9,7 +9,7 @@ import React, { useState } from 'react';
import NavBarContext from 'components/context/NavbarContext'; import NavBarContext from 'components/context/NavbarContext';
interface IProps { interface IProps {
children: React.ReactNode children: React.ReactNode;
} }
export default function NavBarProvider({ children }: IProps) { export default function NavBarProvider({ children }: IProps) {
@@ -36,9 +36,5 @@ export default function NavBarProvider({ children }:IProps) {
override, override,
setOverride, setOverride,
}; };
return ( return <NavBarContext.Provider value={value}>{children}</NavBarContext.Provider>;
<NavBarContext.Provider value={value}>
{children}
</NavBarContext.Provider>
);
} }

View File

@@ -104,28 +104,23 @@ const OpenDrawerButton = styled(IconButton)(({ theme }) => ({
})); }));
interface IProps { interface IProps {
settings: IReaderSettings settings: IReaderSettings;
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
manga: IManga | IMangaCard manga: IManga | IMangaCard;
chapter: IChapter chapter: IChapter;
curPage: number curPage: number;
} }
export default function ReaderNavBar(props: IProps) { export default function ReaderNavBar(props: IProps) {
const history = useHistory(); const history = useHistory();
const backTo = useBackTo(); const backTo = useBackTo();
const location = useLocation<{ const location = useLocation<{
prevDrawerOpen?: boolean, prevDrawerOpen?: boolean;
prevSettingsCollapseOpen?: boolean prevSettingsCollapseOpen?: boolean;
}>(); }>();
const { const { prevDrawerOpen, prevSettingsCollapseOpen } = location.state ?? {};
prevDrawerOpen,
prevSettingsCollapseOpen,
} = location.state ?? {};
const { const { settings, setSettingValue, manga, chapter, curPage } = props;
settings, setSettingValue, manga, chapter, curPage,
} = props;
const [drawerOpen, setDrawerOpen] = useState(settings.staticNav || prevDrawerOpen); const [drawerOpen, setDrawerOpen] = useState(settings.staticNav || prevDrawerOpen);
const [updateDrawerOnRender, setUpdateDrawerOnRender] = useState(true); const [updateDrawerOnRender, setUpdateDrawerOnRender] = useState(true);
@@ -194,13 +189,13 @@ export default function ReaderNavBar(props: IProps) {
mountOnEnter mountOnEnter
unmountOnExit unmountOnExit
> >
<Root sx={{ <Root
sx={{
position: settings.staticNav ? 'sticky' : 'fixed', position: settings.staticNav ? 'sticky' : 'fixed',
}} }}
> >
<header> <header>
{!settings.staticNav {!settings.staticNav && (
&& (
<IconButton <IconButton
edge="start" edge="start"
color="inherit" color="inherit"
@@ -212,7 +207,12 @@ export default function ReaderNavBar(props: IProps) {
<KeyboardArrowLeftIcon /> <KeyboardArrowLeftIcon />
</IconButton> </IconButton>
)} )}
<Typography variant="h1" textOverflow="ellipsis" overflow="hidden" sx={{ py: 1 }}> <Typography
variant="h1"
textOverflow="ellipsis"
overflow="hidden"
sx={{ py: 1 }}
>
{chapter.name} {chapter.name}
</Typography> </Typography>
<IconButton <IconButton
@@ -262,12 +262,9 @@ export default function ReaderNavBar(props: IProps) {
</Collapse> </Collapse>
<Divider sx={{ my: 1, mx: 2 }} /> <Divider sx={{ my: 1, mx: 2 }} />
<Navigation> <Navigation>
<span> <span>{`Currently on page ${curPage + 1} of ${chapter.pageCount}`}</span>
{`Currently on page ${curPage + 1} of ${chapter.pageCount}`}
</span>
<ChapterNavigation> <ChapterNavigation>
{chapter.index > 1 {chapter.index > 1 && (
&& (
<Link <Link
replace replace
to={{ to={{
@@ -287,8 +284,7 @@ export default function ReaderNavBar(props: IProps) {
</Button> </Button>
</Link> </Link>
)} )}
{chapter.index < chapter.chapterCount {chapter.index < chapter.chapterCount && (
&& (
<Link <Link
replace replace
style={{ gridArea: 'next' }} style={{ gridArea: 'next' }}
@@ -300,10 +296,7 @@ export default function ReaderNavBar(props: IProps) {
}, },
}} }}
> >
<Button <Button variant="outlined" endIcon={<KeyboardArrowRightIcon />}>
variant="outlined"
endIcon={<KeyboardArrowRightIcon />}
>
Next Chapter Next Chapter
</Button> </Button>
</Link> </Link>

View File

@@ -17,15 +17,17 @@ import FormGroup from '@mui/material/FormGroup';
import client, { useQuery } from 'util/client'; import client, { useQuery } from 'util/client';
interface IProps { interface IProps {
open: boolean open: boolean;
setOpen: (value: boolean) => void setOpen: (value: boolean) => void;
mangaId: number mangaId: number;
} }
export default function CategorySelect(props: IProps) { export default function CategorySelect(props: IProps) {
const { open, setOpen, mangaId } = props; const { open, setOpen, mangaId } = props;
const { data: mangaCategoriesData, mutate } = useQuery<ICategory[]>(`/api/v1/manga/${mangaId}/category`); const { data: mangaCategoriesData, mutate } = useQuery<ICategory[]>(
`/api/v1/manga/${mangaId}/category`,
);
const { data: categoriesData } = useQuery<ICategory[]>('/api/v1/category'); const { data: categoriesData } = useQuery<ICategory[]>('/api/v1/category');
const allCategories = useMemo(() => { const allCategories = useMemo(() => {
@@ -50,8 +52,7 @@ export default function CategorySelect(props: IProps) {
const { checked } = event.target as HTMLInputElement; const { checked } = event.target as HTMLInputElement;
const method = checked ? client.get : client.delete; const method = checked ? client.get : client.delete;
method(`/api/v1/manga/${mangaId}/category/${categoryId}`) method(`/api/v1/manga/${mangaId}/category/${categoryId}`).then(() => mutate());
.then(() => mutate());
}; };
return ( return (
@@ -68,8 +69,7 @@ export default function CategorySelect(props: IProps) {
<DialogTitle>Set categories</DialogTitle> <DialogTitle>Set categories</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<FormGroup> <FormGroup>
{allCategories.length === 0 {allCategories.length === 0 && (
&& (
<span> <span>
No categories found! No categories found!
<br /> <br />
@@ -78,19 +78,18 @@ export default function CategorySelect(props: IProps) {
)} )}
{allCategories.map((category) => ( {allCategories.map((category) => (
<FormControlLabel <FormControlLabel
control={( control={
<Checkbox <Checkbox
checked={selectedIds.includes(category.id)} checked={selectedIds.includes(category.id)}
onChange={(e) => handleChange(e, category.id)} onChange={(e) => handleChange(e, category.id)}
color="default" color="default"
/> />
)} }
label={category.name} label={category.name}
key={category.id} key={category.id}
/> />
))} ))}
</FormGroup> </FormGroup>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button autoFocus onClick={handleCancel} color="primary"> <Button autoFocus onClick={handleCancel} color="primary">

View File

@@ -31,16 +31,14 @@ function removeAll(firstList: any[], secondList: any[]) {
} }
interface IProps { interface IProps {
shownLangs: string[] shownLangs: string[];
setShownLangs: (arg0: string[]) => void setShownLangs: (arg0: string[]) => void;
allLangs: string[] allLangs: string[];
forcedLangs?: string[] forcedLangs?: string[];
} }
export default function LangSelect(props: IProps) { export default function LangSelect(props: IProps) {
const { const { shownLangs, setShownLangs, allLangs, forcedLangs } = props;
shownLangs, setShownLangs, allLangs, forcedLangs,
} = props;
// hold a copy and only sate state on parent when OK pressed, improves performance // hold a copy and only sate state on parent when OK pressed, improves performance
const [mShownLangs, setMShownLangs] = useState( const [mShownLangs, setMShownLangs] = useState(
removeAll(cloneObject(shownLangs), forcedLangs!), removeAll(cloneObject(shownLangs), forcedLangs!),
@@ -102,11 +100,9 @@ export default function LangSelect(props: IProps) {
onChange={(e) => handleChange(e, lang)} onChange={(e) => handleChange(e, lang)}
/> />
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItem>
))} ))}
</List> </List>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button autoFocus onClick={handleCancel} color="primary"> <Button autoFocus onClick={handleCancel} color="primary">

View File

@@ -24,7 +24,7 @@ const SideNavBarContainer = styled('div')(({ theme }) => ({
})); }));
interface IProps { interface IProps {
navBarItems: Array<NavbarItem> navBarItems: Array<NavbarItem>;
} }
export default function DesktopSideBar({ navBarItems }: IProps) { export default function DesktopSideBar({ navBarItems }: IProps) {
@@ -32,18 +32,27 @@ export default function DesktopSideBar({ navBarItems }: IProps) {
const theme = useTheme(); const theme = useTheme();
const iconFor = (path: string, IconComponent: any, SelectedIconComponent: any) => { const iconFor = (path: string, IconComponent: any, SelectedIconComponent: any) => {
if (location.pathname === path) return <SelectedIconComponent sx={{ color: 'primary.main' }} fontSize="large" />; if (location.pathname === path)
return <IconComponent sx={{ color: (theme.palette.mode === 'dark') ? 'grey.A400' : 'grey.600' }} fontSize="large" />; return <SelectedIconComponent sx={{ color: 'primary.main' }} fontSize="large" />;
return (
<IconComponent
sx={{ color: theme.palette.mode === 'dark' ? 'grey.A400' : 'grey.600' }}
fontSize="large"
/>
);
}; };
return ( return (
<SideNavBarContainer> <SideNavBarContainer>
{ {
// eslint-disable-next-line react/destructuring-assignment // eslint-disable-next-line react/destructuring-assignment
navBarItems.map(({ navBarItems.map(
path, title, IconComponent, SelectedIconComponent, ({ path, title, IconComponent, SelectedIconComponent }: NavbarItem) => (
}: NavbarItem) => ( <Link
<Link to={path} style={{ color: 'inherit', textDecoration: 'none' }} key={path}> to={path}
style={{ color: 'inherit', textDecoration: 'none' }}
key={path}
>
<ListItem disableRipple button key={title}> <ListItem disableRipple button key={title}>
<ListItemIcon sx={{ minWidth: '0' }}> <ListItemIcon sx={{ minWidth: '0' }}>
<Tooltip placement="right" title={title}> <Tooltip placement="right" title={title}>
@@ -52,7 +61,8 @@ export default function DesktopSideBar({ navBarItems }: IProps) {
</ListItemIcon> </ListItemIcon>
</ListItem> </ListItem>
</Link> </Link>
)) ),
)
} }
</SideNavBarContainer> </SideNavBarContainer>
); );

View File

@@ -33,7 +33,7 @@ const Link = styled(RRDLink)({
}); });
interface IProps { interface IProps {
navBarItems: Array<NavbarItem> navBarItems: Array<NavbarItem>;
} }
export default function MobileBottomBar({ navBarItems }: IProps) { export default function MobileBottomBar({ navBarItems }: IProps) {
@@ -41,34 +41,39 @@ export default function MobileBottomBar({ navBarItems }: IProps) {
const theme = useTheme(); const theme = useTheme();
const iconFor = (path: string, IconComponent: any, SelectedIconComponent: any) => { const iconFor = (path: string, IconComponent: any, SelectedIconComponent: any) => {
if (location.pathname === path) return <SelectedIconComponent sx={{ color: 'primary.main' }} fontSize="medium" />; if (location.pathname === path)
return <IconComponent sx={{ color: (theme.palette.mode === 'dark') ? 'grey.A400' : 'grey.600' }} fontSize="medium" />; return <SelectedIconComponent sx={{ color: 'primary.main' }} fontSize="medium" />;
return (
<IconComponent
sx={{ color: theme.palette.mode === 'dark' ? 'grey.A400' : 'grey.600' }}
fontSize="medium"
/>
);
}; };
return ( return (
<BottomNavContainer> <BottomNavContainer>
{ {navBarItems.map(
navBarItems.map(( ({ path, title, IconComponent, SelectedIconComponent }: NavbarItem) => (
{
path, title, IconComponent, SelectedIconComponent,
}: NavbarItem,
) => (
<Link to={path} key={path}> <Link to={path} key={path}>
<ListItem disableRipple button sx={{ justifyContent: 'center', padding: '8px' }} key={title}> <ListItem
<Box disableRipple
display="flex" button
flexDirection="column" sx={{ justifyContent: 'center', padding: '8px' }}
alignItems="center" key={title}
> >
<Box display="flex" flexDirection="column" alignItems="center">
{iconFor(path, IconComponent, SelectedIconComponent)} {iconFor(path, IconComponent, SelectedIconComponent)}
<Box sx={{ <Box
sx={{
fontSize: '0.65rem', fontSize: '0.65rem',
color:
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
color: location.pathname === path location.pathname === path
? 'primary.main' ? 'primary.main'
: ((theme.palette.mode === 'dark') : theme.palette.mode === 'dark'
? 'grey.A400' ? 'grey.A400'
: 'grey.600'), : 'grey.600',
}} }}
> >
{title} {title}
@@ -76,8 +81,8 @@ export default function MobileBottomBar({ navBarItems }: IProps) {
</Box> </Box>
</ListItem> </ListItem>
</Link> </Link>
)) ),
} )}
</BottomNavContainer> </BottomNavContainer>
); );
} }

View File

@@ -18,16 +18,14 @@ const Image = styled('img')({
}); });
interface IProps { interface IProps {
index: number index: number;
image1src: string image1src: string;
image2src: string image2src: string;
settings: IReaderSettings settings: IReaderSettings;
} }
const DoublePage = React.forwardRef((props: IProps, ref: any) => { const DoublePage = React.forwardRef((props: IProps, ref: any) => {
const { const { image1src, image2src, index, settings } = props;
image1src, image2src, index, settings,
} = props;
return ( return (
<Box <Box
@@ -42,14 +40,8 @@ const DoublePage = React.forwardRef((props: IProps, ref: any) => {
overflowX: 'scroll', overflowX: 'scroll',
}} }}
> >
<Image <Image src={image1src} alt={`Page #${index}`} />
src={image1src} <Image src={image2src} alt={`Page #${index + 1}`} />
alt={`Page #${index}`}
/>
<Image
src={image2src}
alt={`Page #${index + 1}`}
/>
</Box> </Box>
); );
}); });

View File

@@ -22,15 +22,18 @@ function imageStyle(settings: IReaderSettings): any {
width: window.innerWidth, width: window.innerWidth,
}); });
} }
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
return () => { return () => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
}; };
}, []); }, []);
if (settings.readerType === 'DoubleLTR' if (
|| settings.readerType === 'DoubleRTL' settings.readerType === 'DoubleLTR' ||
|| settings.readerType === 'ContinuesHorizontalLTR' settings.readerType === 'DoubleRTL' ||
|| settings.readerType === 'ContinuesHorizontalRTL') { settings.readerType === 'ContinuesHorizontalLTR' ||
settings.readerType === 'ContinuesHorizontalRTL'
) {
return { return {
display: 'block', display: 'block',
marginLeft: '7px', marginLeft: '7px',
@@ -54,16 +57,14 @@ function imageStyle(settings: IReaderSettings): any {
} }
interface IProps { interface IProps {
src: string src: string;
index: number index: number;
onImageLoad: () => void onImageLoad: () => void;
settings: IReaderSettings settings: IReaderSettings;
} }
const Page = React.forwardRef((props: IProps, ref: any) => { const Page = React.forwardRef((props: IProps, ref: any) => {
const { const { src, index, onImageLoad, settings } = props;
src, index, onImageLoad, settings,
} = props;
const [useCache] = useLocalStorage<boolean>('useCache', true); const [useCache] = useLocalStorage<boolean>('useCache', true);

View File

@@ -9,16 +9,17 @@ import React from 'react';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
interface IProps { interface IProps {
settings: IReaderSettings settings: IReaderSettings;
curPage: number curPage: number;
pageCount: number pageCount: number;
} }
export default function PageNumber(props: IProps) { export default function PageNumber(props: IProps) {
const { settings, curPage, pageCount } = props; const { settings, curPage, pageCount } = props;
return ( return (
<Box sx={{ <Box
sx={{
display: settings.showPageNumber ? 'block' : 'none', display: settings.showPageNumber ? 'block' : 'none',
position: 'fixed', position: 'fixed',
bottom: '50px', bottom: '50px',

View File

@@ -6,20 +6,22 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { import { List, ListItem, ListItemText, Switch } from '@mui/material';
List, ListItem, ListItemText, Switch,
} from '@mui/material';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import React from 'react'; import React from 'react';
interface IProps extends IReaderSettings { interface IProps extends IReaderSettings {
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
} }
export default function ReaderSettingsOptions({ export default function ReaderSettingsOptions({
staticNav, loadNextOnEnding, readerType, showPageNumber, setSettingValue, staticNav,
loadNextOnEnding,
readerType,
showPageNumber,
setSettingValue,
}: IProps) { }: IProps) {
return ( return (
<> <>
@@ -62,33 +64,17 @@ export default function ReaderSettingsOptions({
onChange={(e) => setSettingValue('readerType', e.target.value)} onChange={(e) => setSettingValue('readerType', e.target.value)}
sx={{ p: 0 }} sx={{ p: 0 }}
> >
<MenuItem value="SingleLTR"> <MenuItem value="SingleLTR">Single Page (LTR)</MenuItem>
Single Page (LTR) <MenuItem value="SingleRTL">Single Page (RTL)</MenuItem>
</MenuItem>
<MenuItem value="SingleRTL">
Single Page (RTL)
</MenuItem>
{/* <MenuItem value="SingleVertical"> {/* <MenuItem value="SingleVertical">
Vertical(WIP) Vertical(WIP)
</MenuItem> */} </MenuItem> */}
<MenuItem value="DoubleLTR"> <MenuItem value="DoubleLTR">Double Page (LTR)</MenuItem>
Double Page (LTR) <MenuItem value="DoubleRTL">Double Page (RTL)</MenuItem>
</MenuItem> <MenuItem value="Webtoon">Webtoon</MenuItem>
<MenuItem value="DoubleRTL"> <MenuItem value="ContinuesVertical">Continues Vertical</MenuItem>
Double Page (RTL) <MenuItem value="ContinuesHorizontalLTR">Horizontal (LTR)</MenuItem>
</MenuItem> <MenuItem value="ContinuesHorizontalRTL">Horizontal (RTL)</MenuItem>
<MenuItem value="Webtoon">
Webtoon
</MenuItem>
<MenuItem value="ContinuesVertical">
Continues Vertical
</MenuItem>
<MenuItem value="ContinuesHorizontalLTR">
Horizontal (LTR)
</MenuItem>
<MenuItem value="ContinuesHorizontalRTL">
Horizontal (RTL)
</MenuItem>
</Select> </Select>
</ListItem> </ListItem>
</List> </List>

View File

@@ -30,9 +30,7 @@ const isSinglePage = (index: number, spreadPages: boolean[]): boolean => {
}; };
export default function DoublePagedPager(props: IReaderProps) { export default function DoublePagedPager(props: IReaderProps) {
const { const { pages, settings, setCurPage, curPage, nextChapter, prevChapter } = props;
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
} = props;
const selfRef = useRef<HTMLDivElement>(null); const selfRef = useRef<HTMLDivElement>(null);
const pagesRef = useRef<HTMLImageElement[]>([]); const pagesRef = useRef<HTMLImageElement[]>([]);
@@ -74,7 +72,7 @@ export default function DoublePagedPager(props: IReaderProps) {
<Page <Page
key={curPage} key={curPage}
index={curPage} index={curPage}
src={(pagesDisplayed.current === 1) ? pages[curPage].src : ''} src={pagesDisplayed.current === 1 ? pages[curPage].src : ''}
onImageLoad={() => {}} onImageLoad={() => {}}
settings={settings} settings={settings}
/>, />,
@@ -101,7 +99,7 @@ export default function DoublePagedPager(props: IReaderProps) {
function nextPage() { function nextPage() {
if (curPage < pages.length - 1) { if (curPage < pages.length - 1) {
const nextCurPage = curPage + pagesDisplayed.current; const nextCurPage = curPage + pagesDisplayed.current;
setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage); setCurPage(nextCurPage >= pages.length ? pages.length - 1 : nextCurPage);
} else if (settings.loadNextOnEnding) { } else if (settings.loadNextOnEnding) {
nextChapter(); nextChapter();
} }
@@ -110,7 +108,7 @@ export default function DoublePagedPager(props: IReaderProps) {
function prevPage() { function prevPage() {
if (curPage > 0) { if (curPage > 0) {
const nextCurPage = curPage - pagesToGoBack(); const nextCurPage = curPage - pagesToGoBack();
setCurPage((nextCurPage < 0) ? 0 : nextCurPage); setCurPage(nextCurPage < 0 ? 0 : nextCurPage);
} else { } else {
prevChapter(); prevChapter();
} }
@@ -167,9 +165,11 @@ export default function DoublePagedPager(props: IReaderProps) {
useEffect(() => { useEffect(() => {
const retryDisplay = setInterval(() => { const retryDisplay = setInterval(() => {
const isLastPage = (curPage === pages.length - 1); const isLastPage = curPage === pages.length - 1;
if ((!isLastPage && pageLoaded.current[curPage] && pageLoaded.current[curPage + 1]) if (
|| pageLoaded.current[curPage]) { (!isLastPage && pageLoaded.current[curPage] && pageLoaded.current[curPage + 1]) ||
pageLoaded.current[curPage]
) {
setPagesToDisplay(); setPagesToDisplay();
displayPages(); displayPages();
clearInterval(retryDisplay); clearInterval(retryDisplay);
@@ -189,23 +189,23 @@ export default function DoublePagedPager(props: IReaderProps) {
return ( return (
<Box ref={selfRef}> <Box ref={selfRef}>
<Box id="preload" sx={{ display: 'none' }}> <Box id="preload" sx={{ display: 'none' }}>
{ {pages.map((page) => (
pages.map((page) => (
<img <img
ref={(e:HTMLImageElement) => { pagesRef.current[page.index] = e; }} ref={(e: HTMLImageElement) => {
pagesRef.current[page.index] = e;
}}
key={`${page.index}`} key={`${page.index}`}
src={page.src} src={page.src}
onLoad={handleImageLoad(page.index)} onLoad={handleImageLoad(page.index)}
alt={`${page.index}`} alt={`${page.index}`}
/> />
)) ))}
}
</Box> </Box>
<Box <Box
id="display" id="display"
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: (settings.readerType === 'DoubleLTR') ? 'row' : 'row-reverse', flexDirection: settings.readerType === 'DoubleLTR' ? 'row' : 'row-reverse',
justifyContent: 'center', justifyContent: 'center',
margin: '0 auto', margin: '0 auto',
width: 'auto', width: 'auto',

View File

@@ -30,9 +30,7 @@ const isAtEnd = () => {
const isAtStart = () => window.scrollX <= 0; const isAtStart = () => window.scrollX <= 0;
export default function HorizontalPager(props: IReaderProps) { export default function HorizontalPager(props: IReaderProps) {
const { const { pages, curPage, initialPage, settings, setCurPage, prevChapter, nextChapter } = props;
pages, curPage, initialPage, settings, setCurPage, prevChapter, nextChapter,
} = props;
const currentPageRef = useRef(initialPage); const currentPageRef = useRef(initialPage);
const selfRef = useRef<HTMLDivElement>(null); const selfRef = useRef<HTMLDivElement>(null);
@@ -166,8 +164,10 @@ export default function HorizontalPager(props: IReaderProps) {
ref={selfRef} ref={selfRef}
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: (settings.readerType === 'ContinuesHorizontalLTR') ? 'row' : 'row-reverse', flexDirection:
justifyContent: (settings.readerType === 'ContinuesHorizontalLTR') ? 'flex-start' : 'flex-end', settings.readerType === 'ContinuesHorizontalLTR' ? 'row' : 'row-reverse',
justifyContent:
settings.readerType === 'ContinuesHorizontalLTR' ? 'flex-start' : 'flex-end',
margin: '0 auto', margin: '0 auto',
width: 'auto', width: 'auto',
height: 'auto', height: 'auto',
@@ -175,18 +175,18 @@ export default function HorizontalPager(props: IReaderProps) {
userSelect: 'none', userSelect: 'none',
}} }}
> >
{ {pages.map((page) => (
pages.map((page) => (
<Page <Page
key={page.index} key={page.index}
index={page.index} index={page.index}
src={page.src} src={page.src}
onImageLoad={() => {}} onImageLoad={() => {}}
settings={settings} settings={settings}
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }} ref={(e: HTMLDivElement) => {
pagesRef.current[page.index] = e;
}}
/> />
)) ))}
}
</Box> </Box>
); );
} }

View File

@@ -10,9 +10,7 @@ import { Box } from '@mui/system';
import Page from 'components/reader/Page'; import Page from 'components/reader/Page';
export default function PagedReader(props: IReaderProps) { export default function PagedReader(props: IReaderProps) {
const { const { pages, settings, setCurPage, curPage, nextChapter, prevChapter } = props;
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
} = props;
const selfRef = useRef<HTMLDivElement>(null); const selfRef = useRef<HTMLDivElement>(null);

View File

@@ -34,9 +34,7 @@ const isAtBottom = () => {
const isAtTop = () => window.scrollY <= 0; const isAtTop = () => window.scrollY <= 0;
export default function VerticalPager(props: IReaderProps) { export default function VerticalPager(props: IReaderProps) {
const { const { pages, settings, setCurPage, initialPage, nextChapter, prevChapter } = props;
pages, settings, setCurPage, initialPage, nextChapter, prevChapter,
} = props;
const currentPageRef = useRef(initialPage); const currentPageRef = useRef(initialPage);
const selfRef = useRef<HTMLDivElement>(null); const selfRef = useRef<HTMLDivElement>(null);
@@ -74,7 +72,8 @@ export default function VerticalPager(props: IReaderProps) {
}; };
}, [settings.loadNextOnEnding]); }, [settings.loadNextOnEnding]);
const go = useCallback((direction: 'up' | 'down') => { const go = useCallback(
(direction: 'up' | 'down') => {
if (direction === 'down' && isAtBottom()) { if (direction === 'down' && isAtBottom()) {
nextChapter(); nextChapter();
return; return;
@@ -86,10 +85,14 @@ export default function VerticalPager(props: IReaderProps) {
} }
window.scroll({ window.scroll({
top: window.scrollY + (window.innerHeight * SCROLL_OFFSET) * (direction === 'up' ? -1 : 1), top:
window.scrollY +
window.innerHeight * SCROLL_OFFSET * (direction === 'up' ? -1 : 1),
behavior: SCROLL_BEHAVIOR, behavior: SCROLL_BEHAVIOR,
}); });
}, [nextChapter, prevChapter]); },
[nextChapter, prevChapter],
);
useEffect(() => { useEffect(() => {
const handleKeyboard = (e: KeyboardEvent) => { const handleKeyboard = (e: KeyboardEvent) => {
@@ -134,18 +137,18 @@ export default function VerticalPager(props: IReaderProps) {
}} }}
onClick={(e) => go(e.clientX > window.innerWidth / 2 ? 'down' : 'up')} onClick={(e) => go(e.clientX > window.innerWidth / 2 ? 'down' : 'up')}
> >
{ {pages.map((page) => (
pages.map((page) => (
<Page <Page
key={page.index} key={page.index}
index={page.index} index={page.index}
src={page.src} src={page.src}
onImageLoad={() => {}} onImageLoad={() => {}}
settings={settings} settings={settings}
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }} ref={(e: HTMLDivElement) => {
pagesRef.current[page.index] = e;
}}
/> />
)) ))}
}
</Box> </Box>
); );
} }

View File

@@ -5,16 +5,17 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { import { IconButton, Menu, MenuItem, FormControlLabel, Radio } from '@mui/material';
IconButton, Menu, MenuItem, FormControlLabel, Radio,
} from '@mui/material';
import React from 'react'; import React from 'react';
import ViewModuleIcon from '@mui/icons-material/ViewModule'; import ViewModuleIcon from '@mui/icons-material/ViewModule';
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext'; import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button // TODO: clean up this to use a FormControl, and remove dependency on name o radio button
export default function SourceGridLayout() { export default function SourceGridLayout() {
const { options: { SourcegridLayout }, setOptions } = useLibraryOptionsContext(); const {
options: { SourcegridLayout },
setOptions,
} = useLibraryOptionsContext();
const [anchorEl, setAnchorEl] = React.useState(null); const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
@@ -25,10 +26,7 @@ export default function SourceGridLayout() {
setAnchorEl(null); setAnchorEl(null);
}; };
function setGridContextOptions( function setGridContextOptions(e: React.ChangeEvent<HTMLInputElement>, checked: boolean) {
e: React.ChangeEvent<HTMLInputElement>,
checked: boolean,
) {
if (checked) { if (checked) {
setOptions((prev: any) => ({ ...prev, SourcegridLayout: parseInt(e.target.name, 10) })); setOptions((prev: any) => ({ ...prev, SourcegridLayout: parseInt(e.target.name, 10) }));
} }
@@ -57,40 +55,40 @@ export default function SourceGridLayout() {
<FormControlLabel <FormControlLabel
label="Compact grid" label="Compact grid"
value={GridLayout.Compact} value={GridLayout.Compact}
control={( control={
<Radio <Radio
name={GridLayout.Compact.toString()} name={GridLayout.Compact.toString()}
checked={ checked={
SourcegridLayout === GridLayout.Compact SourcegridLayout === GridLayout.Compact ||
|| SourcegridLayout === undefined SourcegridLayout === undefined
} }
onChange={setGridContextOptions} onChange={setGridContextOptions}
/> />
)} }
/> />
</MenuItem> </MenuItem>
<MenuItem onClick={handleClose}> <MenuItem onClick={handleClose}>
<FormControlLabel <FormControlLabel
label="Comfortable grid" label="Comfortable grid"
control={( control={
<Radio <Radio
name={GridLayout.Comfortable.toString()} name={GridLayout.Comfortable.toString()}
checked={SourcegridLayout === GridLayout.Comfortable} checked={SourcegridLayout === GridLayout.Comfortable}
onChange={setGridContextOptions} onChange={setGridContextOptions}
/> />
)} }
/> />
</MenuItem> </MenuItem>
<MenuItem onClick={handleClose}> <MenuItem onClick={handleClose}>
<FormControlLabel <FormControlLabel
label="List" label="List"
control={( control={
<Radio <Radio
name={GridLayout.List.toString()} name={GridLayout.List.toString()}
checked={SourcegridLayout === GridLayout.List} checked={SourcegridLayout === GridLayout.List}
onChange={setGridContextOptions} onChange={setGridContextOptions}
/> />
)} }
/> />
</MenuItem> </MenuItem>
</Menu> </Menu>

View File

@@ -17,8 +17,14 @@ function filterManga(mangas: IMangaCard[]): IMangaCard[] {
export default function SourceMangaGrid(props: IMangaGridProps) { export default function SourceMangaGrid(props: IMangaGridProps) {
const { const {
mangas, isLoading, hasNextPage, lastPageNum, mangas,
setLastPageNum, message, messageExtra, gridLayout, isLoading,
hasNextPage,
lastPageNum,
setLastPageNum,
message,
messageExtra,
gridLayout,
} = props; } = props;
const filteredManga = filterManga(mangas); const filteredManga = filterManga(mangas);

View File

@@ -23,33 +23,29 @@ import GroupFilter from 'components/source/filters/GroupFilter';
import SeperatorFilter from 'components/source/filters/SeparatorFilter'; import SeperatorFilter from 'components/source/filters/SeparatorFilter';
interface IFilters { interface IFilters {
sourceFilter: ISourceFilters[] sourceFilter: ISourceFilters[];
updateFilterValue: Function updateFilterValue: Function;
group: number | undefined group: number | undefined;
update: any update: any;
} }
interface IFilters1 { interface IFilters1 {
sourceFilter: ISourceFilters[] sourceFilter: ISourceFilters[];
updateFilterValue: Function updateFilterValue: Function;
resetFilterValue: Function resetFilterValue: Function;
setTriggerUpdate: Function setTriggerUpdate: Function;
setSearch: Function setSearch: Function;
update: any update: any;
} }
export function Options({ export function Options({ sourceFilter, group, updateFilterValue, update }: IFilters) {
sourceFilter,
group,
updateFilterValue,
update,
}: IFilters) {
return ( return (
<Stack key={`filters ${group}`}> <Stack key={`filters ${group}`}>
{sourceFilter.map((e: ISourceFilters, index) => { {sourceFilter.map((e: ISourceFilters, index) => {
let checkif = update.find((el: { let checkif = update.find(
group: number | undefined; position: number; (el: { group: number | undefined; position: number }) =>
}) => el.group === group && el.position === index); el.group === group && el.position === index,
);
checkif = checkif ? checkif.state : checkif; checkif = checkif ? checkif.state : checkif;
switch (e.type) { switch (e.type) {
case 'CheckBox': case 'CheckBox':
@@ -57,7 +53,7 @@ export function Options({
<CheckBoxFilter <CheckBoxFilter
key={`filters ${e.filter.name}`} key={`filters ${e.filter.name}`}
name={e.filter.name} name={e.filter.name}
state={checkif === 'false' || e.filter.state as boolean} state={checkif === 'false' || (e.filter.state as boolean)}
position={index} position={index}
group={group} group={group}
updateFilterValue={updateFilterValue} updateFilterValue={updateFilterValue}
@@ -77,10 +73,7 @@ export function Options({
); );
case 'Header': case 'Header':
return ( return (
<HeaderFilter <HeaderFilter key={`filters ${e.filter.name}`} name={e.filter.name} />
key={`filters ${e.filter.name}`}
name={e.filter.name}
/>
); );
case 'Select': case 'Select':
return ( return (
@@ -88,7 +81,7 @@ export function Options({
key={`filters ${e.filter.name}`} key={`filters ${e.filter.name}`}
name={e.filter.name} name={e.filter.name}
values={e.filter.displayValues} values={e.filter.displayValues}
state={parseInt(checkif, 10) || e.filter.state as number} state={parseInt(checkif, 10) || (e.filter.state as number)}
selected={e.filter.selected} selected={e.filter.selected}
position={index} position={index}
group={group} group={group}
@@ -109,7 +102,7 @@ export function Options({
key={`filters ${e.filter.name}`} key={`filters ${e.filter.name}`}
name={e.filter.name} name={e.filter.name}
values={e.filter.values} values={e.filter.values}
state={checkif ? JSON.parse(checkif) : e.filter.state as IState} state={checkif ? JSON.parse(checkif) : (e.filter.state as IState)}
position={index} position={index}
group={group} group={group}
updateFilterValue={updateFilterValue} updateFilterValue={updateFilterValue}
@@ -121,7 +114,7 @@ export function Options({
<TextFilter <TextFilter
key={`filters ${e.filter.name}`} key={`filters ${e.filter.name}`}
name={e.filter.name} name={e.filter.name}
state={checkif || e.filter.state as string} state={checkif || (e.filter.state as string)}
position={index} position={index}
group={group} group={group}
updateFilterValue={updateFilterValue} updateFilterValue={updateFilterValue}
@@ -133,7 +126,7 @@ export function Options({
<TriStateFilter <TriStateFilter
key={`filters ${e.filter.name}`} key={`filters ${e.filter.name}`}
name={e.filter.name} name={e.filter.name}
state={parseInt(checkif, 10) || e.filter.state as number} state={parseInt(checkif, 10) || (e.filter.state as number)}
position={index} position={index}
group={group} group={group}
updateFilterValue={updateFilterValue} updateFilterValue={updateFilterValue}
@@ -141,7 +134,7 @@ export function Options({
/> />
); );
default: default:
return (<Box key={`${e.filter.name}null`} />); return <Box key={`${e.filter.name}null`} />;
} }
})} })}
</Stack> </Stack>
@@ -181,21 +174,10 @@ export default function SourceOptions({
Filter Filter
</Fab> </Fab>
<OptionsPanel <OptionsPanel open={FilterOptions} onClose={() => setFilterOptions(false)}>
open={FilterOptions}
onClose={() => setFilterOptions(false)}
>
<Box sx={{ display: 'flex', p: 2, pb: 0 }}> <Box sx={{ display: 'flex', p: 2, pb: 0 }}>
<Button <Button onClick={handleReset}>Reset</Button>
onClick={handleReset} <Button sx={{ marginLeft: 'auto' }} variant="contained" onClick={handleSubmit}>
>
Reset
</Button>
<Button
sx={{ marginLeft: 'auto' }}
variant="contained"
onClick={handleSubmit}
>
Submit Submit
</Button> </Button>
</Box> </Box>

View File

@@ -9,37 +9,29 @@ import CheckboxInput from 'components/atoms/CheckboxInput';
import React from 'react'; import React from 'react';
interface Props { interface Props {
state: boolean state: boolean;
name: string name: string;
position: number position: number;
group: number | undefined group: number | undefined;
updateFilterValue: Function updateFilterValue: Function;
update: any update: any;
} }
const CheckBoxFilter: React.FC<Props> = (props: Props) => { const CheckBoxFilter: React.FC<Props> = (props: Props) => {
const { const { state, name, position, group, updateFilterValue, update } = props;
state,
name,
position,
group,
updateFilterValue,
update,
} = props;
const [val, setval] = React.useState(state); const [val, setval] = React.useState(state);
const handleChange = (event: { target: { name: any; checked: any; }; }) => { const handleChange = (event: { target: { name: any; checked: any } }) => {
setval(event.target.checked); setval(event.target.checked);
const upd = update.filter((e: { const upd = update.filter(
position: number; group: number | undefined; (e: { position: number; group: number | undefined }) =>
}) => !(position === e.position && group === e.group)); !(position === e.position && group === e.group),
);
updateFilterValue([...upd, { position, state: event.target.checked.toString(), group }]); updateFilterValue([...upd, { position, state: event.target.checked.toString(), group }]);
}; };
if (state !== undefined) { if (state !== undefined) {
return ( return <CheckboxInput label={name} checked={val} onChange={handleChange} />;
<CheckboxInput label={name} checked={val} onChange={handleChange} />
);
} }
return null; return null;
}; };

View File

@@ -6,30 +6,22 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { ExpandLess, ExpandMore } from '@mui/icons-material'; import { ExpandLess, ExpandMore } from '@mui/icons-material';
import { import { Collapse, ListItemButton, ListItemText, Stack } from '@mui/material';
Collapse, ListItemButton, ListItemText, Stack,
} from '@mui/material';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
import React from 'react'; import React from 'react';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { Options } from 'components/source/SourceOptions'; import { Options } from 'components/source/SourceOptions';
interface Props { interface Props {
state: ISourceFilters[] state: ISourceFilters[];
name: string name: string;
position: number position: number;
updateFilterValue: Function updateFilterValue: Function;
update: any update: any;
} }
const GroupFilter: React.FC<Props> = (props: Props) => { const GroupFilter: React.FC<Props> = (props: Props) => {
const { const { state, name, position, updateFilterValue, update } = props;
state,
name,
position,
updateFilterValue,
update,
} = props;
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);

View File

@@ -10,9 +10,13 @@ import { Typography } from '@mui/material';
import React from 'react'; import React from 'react';
interface Props { interface Props {
name: string name: string;
} }
const HeaderFilter: React.FC<Props> = ({ name }) => (<Typography key={name} sx={{ mt: 2 }} variant="subtitle2">{name}</Typography>); const HeaderFilter: React.FC<Props> = ({ name }) => (
<Typography key={name} sx={{ mt: 2 }} variant="subtitle2">
{name}
</Typography>
);
export default HeaderFilter; export default HeaderFilter;

View File

@@ -13,20 +13,20 @@ import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
interface Props { interface Props {
values: any values: any;
name: string name: string;
state: number state: number;
selected: Selected | undefined selected: Selected | undefined;
position: number position: number;
updateFilterValue: Function updateFilterValue: Function;
group: number | undefined group: number | undefined;
update: any update: any;
} }
interface Selected { interface Selected {
displayname: string displayname: string;
value: string value: string;
_value: string _value: string;
} }
function hasSelect( function hasSelect(
@@ -40,30 +40,24 @@ function hasSelect(
) { ) {
const [val, setval] = React.useState(state); const [val, setval] = React.useState(state);
if (values) { if (values) {
const handleChange = (event: { target: { name: any; value: any; }; }) => { const handleChange = (event: { target: { name: any; value: any } }) => {
const vall = values.map((e) => e.displayname).indexOf(`${event.target.value}`); const vall = values.map((e) => e.displayname).indexOf(`${event.target.value}`);
setval(vall); setval(vall);
const upd = update.filter((e: { const upd = update.filter(
position: number; group: number | undefined; (e: { position: number; group: number | undefined }) =>
}) => !(position === e.position && group === e.group)); !(position === e.position && group === e.group),
);
updateFilterValue([...upd, { position, state: vall.toString(), group }]); updateFilterValue([...upd, { position, state: vall.toString(), group }]);
}; };
const rett = values.map((e: Selected) => ( const rett = values.map((e: Selected) => (
<MenuItem <MenuItem key={`${name} ${e.displayname}`} value={e.displayname}>
key={`${name} ${e.displayname}`} {e.displayname}
value={e.displayname}
>
{
e.displayname
}
</MenuItem> </MenuItem>
)); ));
return ( return (
<FormControl sx={{ my: 1 }} variant="standard"> <FormControl sx={{ my: 1 }} variant="standard">
<InputLabel> <InputLabel>{name}</InputLabel>
{name}
</InputLabel>
<Select <Select
name={name} name={name}
value={values[val].displayname} value={values[val].displayname}
@@ -90,27 +84,25 @@ function noSelect(
const [val, setval] = React.useState(state); const [val, setval] = React.useState(state);
if (values) { if (values) {
const handleChange = (event: { target: { name: any; value: any; }; }) => { const handleChange = (event: { target: { name: any; value: any } }) => {
const vall = values.indexOf(`${event.target.value}`); const vall = values.indexOf(`${event.target.value}`);
setval(vall); setval(vall);
const upd = update.filter((e: { const upd = update.filter(
position: number; group: number | undefined; (e: { position: number; group: number | undefined }) =>
}) => !(position === e.position && group === e.group)); !(position === e.position && group === e.group),
);
updateFilterValue([...upd, { position, state: vall.toString(), group }]); updateFilterValue([...upd, { position, state: vall.toString(), group }]);
}; };
const rett = values.map((value: string) => (<MenuItem key={`${name} ${value}`} value={value}>{value}</MenuItem>)); const rett = values.map((value: string) => (
<MenuItem key={`${name} ${value}`} value={value}>
{value}
</MenuItem>
));
return ( return (
<FormControl sx={{ my: 1 }} variant="standard"> <FormControl sx={{ my: 1 }} variant="standard">
<InputLabel> <InputLabel>{name}</InputLabel>
{name} <Select name={name} value={values[val]} label={name} onChange={handleChange}>
</InputLabel>
<Select
name={name}
value={values[val]}
label={name}
onChange={handleChange}
>
{rett} {rett}
</Select> </Select>
</FormControl> </FormControl>
@@ -130,26 +122,10 @@ const SelectFilter: React.FC<Props> = ({
group, group,
}) => { }) => {
if (selected === undefined) { if (selected === undefined) {
return noSelect( return noSelect(values, name, state, position, updateFilterValue, update, group);
values,
name,
state,
position,
updateFilterValue,
update,
group,
);
} }
return hasSelect( return hasSelect(values, name, state, position, updateFilterValue, update, group);
values,
name,
state,
position,
updateFilterValue,
update,
group,
);
}; };
export default SelectFilter; export default SelectFilter;

View File

@@ -9,9 +9,13 @@ import { Divider } from '@mui/material';
import React from 'react'; import React from 'react';
interface Props { interface Props {
name: string name: string;
} }
const SeparatorFilter: React.FC<Props> = ({ name }) => (<Divider key={name} sx={{ my: 1 }} textAlign="center">{name}</Divider>); const SeparatorFilter: React.FC<Props> = ({ name }) => (
<Divider key={name} sx={{ my: 1 }} textAlign="center">
{name}
</Divider>
);
export default SeparatorFilter; export default SeparatorFilter;

View File

@@ -6,33 +6,23 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { ExpandLess, ExpandMore } from '@mui/icons-material'; import { ExpandLess, ExpandMore } from '@mui/icons-material';
import { import { Collapse, ListItemButton, ListItemText, Stack } from '@mui/material';
Collapse, ListItemButton, ListItemText, Stack,
} from '@mui/material';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
import SortRadioInput from 'components/atoms/SortRadioInput'; import SortRadioInput from 'components/atoms/SortRadioInput';
import React from 'react'; import React from 'react';
interface Props { interface Props {
values: any values: any;
name: string name: string;
state: IState state: IState;
position: number position: number;
group: number | undefined group: number | undefined;
updateFilterValue: Function updateFilterValue: Function;
update: any update: any;
} }
const SortFilter: React.FC<Props> = (props: Props) => { const SortFilter: React.FC<Props> = (props: Props) => {
const { const { values, name, state, position, group, updateFilterValue, update } = props;
values,
name,
state,
position,
group,
updateFilterValue,
update,
} = props;
const [val, setval] = React.useState(state); const [val, setval] = React.useState(state);
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@@ -51,9 +41,10 @@ const SortFilter: React.FC<Props> = (props: Props) => {
} }
tmp.index = index; tmp.index = index;
setval(tmp); setval(tmp);
const upd = update.filter((e: { const upd = update.filter(
position: number; group: number | undefined; (e: { position: number; group: number | undefined }) =>
}) => !(position === e.position && group === e.group)); !(position === e.position && group === e.group),
);
updateFilterValue([...upd, { position, state: JSON.stringify(tmp), group }]); updateFilterValue([...upd, { position, state: JSON.stringify(tmp), group }]);
}; };

View File

@@ -6,36 +6,28 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import { import { FormControl, Input, InputAdornment, InputLabel } from '@mui/material';
FormControl, Input, InputAdornment, InputLabel,
} from '@mui/material';
import React from 'react'; import React from 'react';
interface Props { interface Props {
state: string state: string;
name: string name: string;
position: number position: number;
group: number | undefined group: number | undefined;
updateFilterValue: Function updateFilterValue: Function;
update: any update: any;
} }
const TextFilter: React.FC<Props> = (props) => { const TextFilter: React.FC<Props> = (props) => {
const { const { state, name, position, group, updateFilterValue, update } = props;
state,
name,
position,
group,
updateFilterValue,
update,
} = props;
const [Search, setsearch] = React.useState(state || ''); const [Search, setsearch] = React.useState(state || '');
let typingTimer: NodeJS.Timeout; let typingTimer: NodeJS.Timeout;
function doneTyping(e: React.ChangeEvent<HTMLInputElement>) { function doneTyping(e: React.ChangeEvent<HTMLInputElement>) {
const upd = update.filter((el: { const upd = update.filter(
position: number; group: number | undefined; (el: { position: number; group: number | undefined }) =>
}) => !(position === el.position && group === el.group)); !(position === el.position && group === el.group),
);
updateFilterValue([...upd, { position, state: e.target.value, group }]); updateFilterValue([...upd, { position, state: e.target.value, group }]);
} }
@@ -43,29 +35,29 @@ const TextFilter: React.FC<Props> = (props) => {
setsearch(e.target.value); setsearch(e.target.value);
clearTimeout(typingTimer); clearTimeout(typingTimer);
typingTimer = setTimeout(() => { doneTyping(e); }, 2500); typingTimer = setTimeout(() => {
doneTyping(e);
}, 2500);
} }
if (state !== undefined) { if (state !== undefined) {
return ( return (
<FormControl sx={{ my: 1 }} variant="standard"> <FormControl sx={{ my: 1 }} variant="standard">
<InputLabel> <InputLabel>{name}</InputLabel>
{name}
</InputLabel>
<Input <Input
name={name} name={name}
value={Search || ''} value={Search || ''}
onChange={handleChange} onChange={handleChange}
endAdornment={( endAdornment={
<InputAdornment position="end"> <InputAdornment position="end">
<SearchIcon /> <SearchIcon />
</InputAdornment> </InputAdornment>
)} }
/> />
</FormControl> </FormControl>
); );
} }
return (<></>); return <></>;
}; };
export default TextFilter; export default TextFilter;

View File

@@ -9,37 +9,34 @@ import ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
import React from 'react'; import React from 'react';
interface Props { interface Props {
state: number state: number;
name: string name: string;
position: number position: number;
group: number | undefined group: number | undefined;
updateFilterValue: Function updateFilterValue: Function;
update: any update: any;
} }
const TriStateFilter: React.FC<Props> = (props) => { const TriStateFilter: React.FC<Props> = (props) => {
const { const { state, name, position, group, updateFilterValue, update } = props;
state,
name,
position,
group,
updateFilterValue,
update,
} = props;
const [val, setval] = React.useState<number>(Number(state)); const [val, setval] = React.useState<number>(Number(state));
const handleChange = (checked: boolean | null | undefined) => { const handleChange = (checked: boolean | null | undefined) => {
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
const newState = checked === undefined ? 0 : checked ? 1 : 2; const newState = checked === undefined ? 0 : checked ? 1 : 2;
setval(newState); setval(newState);
const upd = update.filter((e: { const upd = update.filter(
position: number; group: number | undefined; (e: { position: number; group: number | undefined }) =>
}) => !(position === e.position && group === e.group)); !(position === e.position && group === e.group),
updateFilterValue([...upd, { );
updateFilterValue([
...upd,
{
position, position,
state: newState.toString(), state: newState.toString(),
group, group,
}]); },
]);
}; };
if (state !== undefined) { if (state !== undefined) {
@@ -51,7 +48,7 @@ const TriStateFilter: React.FC<Props> = (props) => {
/> />
); );
} }
return (<></>); return <></>;
}; };
export default TriStateFilter; export default TriStateFilter;

View File

@@ -17,9 +17,7 @@ import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
export default function EditTextPreference(props: EditTextPreferenceProps) { export default function EditTextPreference(props: EditTextPreferenceProps) {
const { const { title, summary, dialogTitle, dialogMessage, currentValue, updateValue } = props;
title, summary, dialogTitle, dialogMessage, currentValue, updateValue,
} = props;
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue); const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
const [dialogOpen, setDialogOpen] = useState<boolean>(false); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
@@ -39,23 +37,13 @@ export default function EditTextPreference(props: EditTextPreferenceProps) {
return ( return (
<> <>
<ListItem <ListItem button onClick={() => setDialogOpen(true)}>
button <ListItemText primary={title} secondary={summary} />
onClick={() => setDialogOpen(true)}
>
<ListItemText
primary={title}
secondary={summary}
/>
</ListItem> </ListItem>
<Dialog open={dialogOpen} onClose={handleDialogCancel}> <Dialog open={dialogOpen} onClose={handleDialogCancel}>
<DialogTitle> <DialogTitle>{dialogTitle}</DialogTitle>
{dialogTitle}
</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>{dialogMessage}</DialogContentText>
{dialogMessage}
</DialogContentText>
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"

View File

@@ -18,17 +18,15 @@ import FormControlLabel from '@mui/material/FormControlLabel';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
interface IListDialogProps { interface IListDialogProps {
value: string value: string;
open: boolean open: boolean;
onClose: (arg0: string | null) => void onClose: (arg0: string | null) => void;
options: string[] options: string[];
title: string title: string;
} }
function ListDialog(props: IListDialogProps) { function ListDialog(props: IListDialogProps) {
const { const { value: valueProp, open, onClose, options, title } = props;
value: valueProp, open, onClose, options, title,
} = props;
const [value, setValue] = React.useState(valueProp); const [value, setValue] = React.useState(valueProp);
const radioGroupRef = React.useRef<HTMLDivElement>(null); const radioGroupRef = React.useRef<HTMLDivElement>(null);
@@ -65,11 +63,7 @@ function ListDialog(props: IListDialogProps) {
> >
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<RadioGroup <RadioGroup ref={radioGroupRef} value={value} onChange={handleChange}>
ref={radioGroupRef}
value={value}
onChange={handleChange}
>
{options.map((option) => ( {options.map((option) => (
<FormControlLabel <FormControlLabel
value={option} value={option}
@@ -91,9 +85,7 @@ function ListDialog(props: IListDialogProps) {
} }
export default function ListPreference(props: ListPreferenceProps) { export default function ListPreference(props: ListPreferenceProps) {
const { const { title, summary, currentValue, updateValue, entryValues, entries } = props;
title, summary, currentValue, updateValue, entryValues, entries,
} = props;
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue); const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
const [dialogOpen, setDialogOpen] = useState<boolean>(false); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
@@ -131,10 +123,7 @@ export default function ListPreference(props: ListPreferenceProps) {
return ( return (
<> <>
<ListItem <ListItem button onClick={() => setDialogOpen(true)}>
button
onClick={() => setDialogOpen(true)}
>
<ListItemText primary={title} secondary={getSummary()} /> <ListItemText primary={title} secondary={getSummary()} />
</ListItem> </ListItem>
<ListDialog <ListDialog

View File

@@ -19,17 +19,15 @@ import Button from '@mui/material/Button';
import cloneObject from 'util/cloneObject'; import cloneObject from 'util/cloneObject';
interface IListDialogProps { interface IListDialogProps {
selectedValues: string[] selectedValues: string[];
open: boolean open: boolean;
onClose: (arg0: string[] | null) => void onClose: (arg0: string[] | null) => void;
values: string[] values: string[];
title: string title: string;
} }
function ListDialog(props: IListDialogProps) { function ListDialog(props: IListDialogProps) {
const { const { selectedValues: selectedValuesProp, open, onClose, values, title } = props;
selectedValues: selectedValuesProp, open, onClose, values, title,
} = props;
const [selectedValues, setSelectedValues] = React.useState(selectedValuesProp); const [selectedValues, setSelectedValues] = React.useState(selectedValuesProp);
React.useEffect(() => { React.useEffect(() => {
@@ -56,7 +54,8 @@ function ListDialog(props: IListDialogProps) {
selectedValuesClone.push(value); selectedValuesClone.push(value);
setSelectedValues(selectedValuesClone); setSelectedValues(selectedValuesClone);
} }
} else if (hasEntry) { // not checked and has entry } else if (hasEntry) {
// not checked and has entry
const selectedValuesClone = cloneObject(selectedValues) as string[]; const selectedValuesClone = cloneObject(selectedValues) as string[];
const index = selectedValuesClone.indexOf(value); const index = selectedValuesClone.indexOf(value);
selectedValuesClone.splice(index, 1); selectedValuesClone.splice(index, 1);
@@ -75,7 +74,7 @@ function ListDialog(props: IListDialogProps) {
<FormGroup> <FormGroup>
{values.map((value) => ( {values.map((value) => (
<FormControlLabel <FormControlLabel
control={( control={
<Checkbox <Checkbox
checked={selectedValues.some( checked={selectedValues.some(
(selectedValue) => value === selectedValue, (selectedValue) => value === selectedValue,
@@ -83,7 +82,7 @@ function ListDialog(props: IListDialogProps) {
onChange={(e) => handleChange(e, value)} onChange={(e) => handleChange(e, value)}
color="default" color="default"
/> />
)} }
label={value} label={value}
key={value} key={value}
/> />
@@ -101,9 +100,7 @@ function ListDialog(props: IListDialogProps) {
} }
export default function MultiSelectListPreference(props: MultiSelectListPreferenceProps) { export default function MultiSelectListPreference(props: MultiSelectListPreferenceProps) {
const { const { title, summary, currentValue, updateValue, entryValues, entries } = props;
title, summary, currentValue, updateValue, entryValues, entries,
} = props;
const [internalCurrentValue, setInternalCurrentValue] = useState<string[]>(currentValue); const [internalCurrentValue, setInternalCurrentValue] = useState<string[]>(currentValue);
const [dialogOpen, setDialogOpen] = useState<boolean>(false); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
@@ -111,12 +108,14 @@ export default function MultiSelectListPreference(props: MultiSelectListPreferen
setInternalCurrentValue(currentValue); setInternalCurrentValue(currentValue);
}, [currentValue]); }, [currentValue]);
const findEntriesOf = (values: string[]) => values.map((value) => { const findEntriesOf = (values: string[]) =>
values.map((value) => {
const idx = entryValues.indexOf(value); const idx = entryValues.indexOf(value);
return entries[idx]; return entries[idx];
}); });
const findEntryValuesOf = (values: string[]) => values.map((value) => { const findEntryValuesOf = (values: string[]) =>
values.map((value) => {
const idx = entries.indexOf(value); const idx = entries.indexOf(value);
return entryValues[idx]; return entryValues[idx];
}); });
@@ -137,10 +136,7 @@ export default function MultiSelectListPreference(props: MultiSelectListPreferen
return ( return (
<> <>
<ListItem <ListItem button onClick={() => setDialogOpen(true)}>
button
onClick={() => setDialogOpen(true)}
>
<ListItemText primary={title} secondary={getSummary()} /> <ListItemText primary={title} secondary={getSummary()} />
</ListItem> </ListItem>
<ListDialog <ListDialog

View File

@@ -13,14 +13,14 @@ import Switch from '@mui/material/Switch';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
function getTwoStateType(type: 'Checkbox' | 'Switch') { function getTwoStateType(type: 'Checkbox' | 'Switch') {
if (type === 'Switch') { return Switch; } if (type === 'Switch') {
return Switch;
}
return Checkbox; return Checkbox;
} }
function TwoSatePreference(props: TwoStatePreferenceProps) { function TwoSatePreference(props: TwoStatePreferenceProps) {
const { const { title, summary, currentValue, updateValue, type } = props;
title, summary, currentValue, updateValue, type,
} = props;
const [internalCurrentValue, setInternalCurrentValue] = useState<boolean>(currentValue); const [internalCurrentValue, setInternalCurrentValue] = useState<boolean>(currentValue);
useEffect(() => { useEffect(() => {
@@ -31,8 +31,7 @@ function TwoSatePreference(props: TwoStatePreferenceProps) {
<ListItem> <ListItem>
<ListItemText primary={title} secondary={summary} /> <ListItemText primary={title} secondary={summary} />
<ListItemSecondaryAction> <ListItemSecondaryAction>
{React.createElement(getTwoStateType(type), {React.createElement(getTwoStateType(type), {
{
edge: 'end', edge: 'end',
checked: internalCurrentValue, checked: internalCurrentValue,
onChange: () => { onChange: () => {
@@ -50,6 +49,7 @@ function TwoSatePreference(props: TwoStatePreferenceProps) {
export function CheckBoxPreference(props: CheckBoxPreferenceProps) { export function CheckBoxPreference(props: CheckBoxPreferenceProps) {
return <TwoSatePreference {...props} type="Checkbox" />; return <TwoSatePreference {...props} type="Checkbox" />;
} }
export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) { export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) {
return <TwoSatePreference {...props} type="Switch" />; return <TwoSatePreference {...props} type="Switch" />;
} }

View File

@@ -13,7 +13,7 @@ import CancelIcon from '@mui/icons-material/Cancel';
import { useQueryParam, StringParam } from 'use-query-params'; import { useQueryParam, StringParam } from 'use-query-params';
interface IProps { interface IProps {
autoOpen?: boolean autoOpen?: boolean;
} }
const defaultProps = { const defaultProps = {
@@ -29,11 +29,14 @@ const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value === '' ? undefined : e.target.value); setQuery(e.target.value === '' ? undefined : e.target.value);
} }
const cancelSearch = () => { const cancelSearch = () => {
setQuery(null); setQuery(null);
setSearchOpen(false); setSearchOpen(false);
}; };
const handleBlur = () => { if (!query) setSearchOpen(false); }; const handleBlur = () => {
if (!query) setSearchOpen(false);
};
const openSearch = () => { const openSearch = () => {
setSearchOpen(true); setSearchOpen(true);
// Put Focus Action at the end of the Callstack so Input actually exists on the dom // Put Focus Action at the end of the Callstack so Input actually exists on the dom
@@ -43,7 +46,7 @@ const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
}; };
const handleSearchShortcut = (e: KeyboardEvent) => { const handleSearchShortcut = (e: KeyboardEvent) => {
if ((e.code === 'F3') || (e.ctrlKey && e.code === 'KeyF')) { if (e.code === 'F3' || (e.ctrlKey && e.code === 'KeyF')) {
e.preventDefault(); e.preventDefault();
openSearch(); openSearch();
} }
@@ -65,20 +68,17 @@ const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
return ( return (
<> <>
{searchOpen {searchOpen ? (
? (
<Input <Input
value={query || ''} value={query || ''}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
inputRef={inputRef} inputRef={inputRef}
endAdornment={( endAdornment={
<IconButton <IconButton onClick={cancelSearch}>
onClick={cancelSearch}
>
<CancelIcon /> <CancelIcon />
</IconButton> </IconButton>
)} }
/> />
) : ( ) : (
<IconButton onClick={openSearch}> <IconButton onClick={openSearch}>

View File

@@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import createSvgIcon from '@mui/material/utils/createSvgIcon'; import createSvgIcon from '@mui/material/utils/createSvgIcon';
const d = 'M 11 1 C 9.3550302 1 8 2.3550302 8 4 L 4 4 C 2.9069372 4 2 4.9069372 2 6 L 2 12 L 4 12 C 4.5650302 12 5 12.43497 5 13 C 5 13.56503 4.5650302 14 4 14 L 2 14 L 2 20 C 2 21.093063 2.9069372 22 4 22 L 10 22 L 10 20 C 10 19.43497 10.43497 19 11 19 C 11.56503 19 12 19.43497 12 20 L 12 22 L 18 22 C 19.093063 22 20 21.093063 20 20 L 20 16 C 21.64497 16 23 14.64497 23 13 C 23 11.35503 21.64497 10 20 10 L 20 6 C 20 4.9069372 19.093063 4 18 4 L 14 4 C 14 2.3550302 12.64497 1 11 1 z M 11 3 C 11.56503 3 12 3.4349698 12 4 L 12 6 L 18 6 L 18 12 L 20 12 C 20.56503 12 21 12.43497 21 13 C 21 13.56503 20.56503 14 20 14 L 18 14 L 18 20 L 14 20 C 14 18.35503 12.64497 17 11 17 C 9.3550302 17 8 18.35503 8 20 L 4 20 L 4 16 C 5.6449698 16 7 14.64497 7 13 C 7 11.35503 5.6449698 10 4 10 L 4 6 L 10 6 L 10 4 C 10 3.4349698 10.43497 3 11 3 z'; const d =
'M 11 1 C 9.3550302 1 8 2.3550302 8 4 L 4 4 C 2.9069372 4 2 4.9069372 2 6 L 2 12 L 4 12 C 4.5650302 12 5 12.43497 5 13 C 5 13.56503 4.5650302 14 4 14 L 2 14 L 2 20 C 2 21.093063 2.9069372 22 4 22 L 10 22 L 10 20 C 10 19.43497 10.43497 19 11 19 C 11.56503 19 12 19.43497 12 20 L 12 22 L 18 22 C 19.093063 22 20 21.093063 20 20 L 20 16 C 21.64497 16 23 14.64497 23 13 C 23 11.35503 21.64497 10 20 10 L 20 6 C 20 4.9069372 19.093063 4 18 4 L 14 4 C 14 2.3550302 12.64497 1 11 1 z M 11 3 C 11.56503 3 12 3.4349698 12 4 L 12 6 L 18 6 L 18 12 L 20 12 C 20.56503 12 21 12.43497 21 13 C 21 13.56503 20.56503 14 20 14 L 18 14 L 18 20 L 14 20 C 14 18.35503 12.64497 17 11 17 C 9.3550302 17 8 18.35503 8 20 L 4 20 L 4 16 C 5.6449698 16 7 14.64497 7 13 C 7 11.35503 5.6449698 10 4 10 L 4 6 L 10 6 L 10 4 C 10 3.4349698 10.43497 3 11 3 z';
const icon = createSvgIcon(<path d={d} />, 'CustomExtensionOutlined'); const icon = createSvgIcon(<path d={d} />, 'CustomExtensionOutlined');

View File

@@ -12,14 +12,7 @@ import { useTheme } from '@mui/material/styles';
import { useMediaQuery } from '@mui/material'; import { useMediaQuery } from '@mui/material';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
const ERROR_FACES = [ const ERROR_FACES = ['(・o・;)', 'Σ(ಠ_ಠ)', 'ಥ_ಥ', '(˘・_・˘)', '(; ̄Д ̄)', '(・Д・。'];
'(・o・;)',
'Σ(ಠ_ಠ)',
'ಥ_ಥ',
'(˘・_・˘)',
'(; ̄Д ̄)',
'(・Д・。',
];
function getRandomErrorFace() { function getRandomErrorFace() {
const randIndex = Math.floor(Math.random() * ERROR_FACES.length); const randIndex = Math.floor(Math.random() * ERROR_FACES.length);
@@ -27,8 +20,8 @@ function getRandomErrorFace() {
} }
interface IProps { interface IProps {
message: string message: string;
messageExtra?: JSX.Element messageExtra?: JSX.Element;
} }
export default function EmptyView({ message, messageExtra }: IProps) { export default function EmptyView({ message, messageExtra }: IProps) {
@@ -38,7 +31,8 @@ export default function EmptyView({ message, messageExtra }: IProps) {
const errorFace = useMemo(() => getRandomErrorFace(), []); const errorFace = useMemo(() => getRandomErrorFace(), []);
return ( return (
<Box sx={{ <Box
sx={{
position: 'absolute', position: 'absolute',
left: `calc(50% + ${isMobileWidth ? '0px' : theme.spacing(8 / 2)})`, left: `calc(50% + ${isMobileWidth ? '0px' : theme.spacing(8 / 2)})`,
top: '50%', top: '50%',
@@ -49,9 +43,7 @@ export default function EmptyView({ message, messageExtra }: IProps) {
<Typography variant="h3" gutterBottom> <Typography variant="h3" gutterBottom>
{errorFace} {errorFace}
</Typography> </Typography>
<Typography variant="h5"> <Typography variant="h5">{message}</Typography>
{message}
</Typography>
{messageExtra} {messageExtra}
</Box> </Box>
); );

View File

@@ -10,16 +10,14 @@ import CircularProgress from '@mui/material/CircularProgress';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
interface IProps { interface IProps {
shouldRender?: boolean | (() => boolean) shouldRender?: boolean | (() => boolean);
children?: React.ReactNode children?: React.ReactNode;
component?: string | React.FunctionComponent<any> | React.ComponentClass<any, any> component?: string | React.FunctionComponent<any> | React.ComponentClass<any, any>;
componentProps?: any componentProps?: any;
} }
export default function LoadingPlaceholder(props: IProps) { export default function LoadingPlaceholder(props: IProps) {
const { const { children, shouldRender, component, componentProps } = props;
children, shouldRender, component, componentProps,
} = props;
let condition = true; let condition = true;
if (shouldRender !== undefined) { if (shouldRender !== undefined) {
@@ -32,16 +30,13 @@ export default function LoadingPlaceholder(props: IProps) {
} }
if (children) { if (children) {
return ( return <>{children}</>;
<>
{children}
</>
);
} }
} }
return ( return (
<Box sx={{ <Box
sx={{
margin: '10px auto', margin: '10px auto',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',

View File

@@ -12,21 +12,19 @@ import { Theme } from '@mui/system/createTheme';
import { SxProps } from '@mui/system/styleFunctionSx'; import { SxProps } from '@mui/system/styleFunctionSx';
interface IProps { interface IProps {
src: string src: string;
alt: string alt: string;
imgRef?: React.RefObject<HTMLImageElement> imgRef?: React.RefObject<HTMLImageElement>;
spinnerStyle?: SxProps<Theme> spinnerStyle?: SxProps<Theme>;
imgStyle?: CSSProperties imgStyle?: CSSProperties;
onImageLoad?: () => void onImageLoad?: () => void;
} }
export default function SpinnerImage(props: IProps) { export default function SpinnerImage(props: IProps) {
const { const { src, alt, onImageLoad, imgRef, spinnerStyle, imgStyle } = props;
src, alt, onImageLoad, imgRef, spinnerStyle, imgStyle,
} = props;
const [imageSrc, setImagsrc] = useState<string>(''); const [imageSrc, setImagsrc] = useState<string>('');
useEffect(() => { useEffect(() => {
@@ -61,14 +59,7 @@ export default function SpinnerImage(props: IProps) {
return <Box sx={spinnerStyle} />; return <Box sx={spinnerStyle} />;
} }
return ( return <img style={imgStyle} ref={imgRef} src={imageSrc} alt={alt} />;
<img
style={imgStyle}
ref={imgRef}
src={imageSrc}
alt={alt}
/>
);
} }
SpinnerImage.defaultProps = { SpinnerImage.defaultProps = {

View File

@@ -14,16 +14,10 @@ interface IProps {
} }
export default function TabPanel(props: IProps) { export default function TabPanel(props: IProps) {
const { const { children, index, currentIndex } = props;
children, index, currentIndex,
} = props;
return ( return (
<div <div role="tabpanel" hidden={index !== currentIndex} id={`simple-tabpanel-${index}`}>
role="tabpanel"
hidden={index !== currentIndex}
id={`simple-tabpanel-${index}`}
>
{currentIndex === index && children} {currentIndex === index && children}
</div> </div>
); );

View File

@@ -22,8 +22,8 @@ function Transition(props: SlideProps) {
} }
interface IToastProps { interface IToastProps {
message: string message: string;
severity: Severity severity: Severity;
} }
export function Toast(props: IToastProps) { export function Toast(props: IToastProps) {
@@ -61,15 +61,20 @@ export default function makeToast(message: string, severity: Severity) {
setTimeout(() => removeToast(container.id), 3500); setTimeout(() => removeToast(container.id), 3500);
} }
export function makeToaster( export function makeToaster([toasts, setToasts]: [
[toasts, setToasts] : [React.ReactElement[], React.ReactElement[],
(arg0: React.ReactElement[]) => void], (arg0: React.ReactElement[]) => void,
): [React.ReactElement[], ((message: string, severity: Severity) => void)] { ]): [React.ReactElement[], (message: string, severity: Severity) => void] {
return [toasts, (message: string, severity: Severity) => { return [
setToasts([<Toast toasts,
(message: string, severity: Severity) => {
setToasts([
<Toast
key={Math.floor(Math.random() * 1000) + 1} key={Math.floor(Math.random() * 1000) + 1}
message={message} message={message}
severity={severity} severity={severity}
/>]); />,
}]; ]);
},
];
} }

View File

@@ -6,14 +6,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const pluralize = (count: number, input: string | { one: string, many: string }) => { export const pluralize = (count: number, input: string | { one: string; many: string }) => {
if (typeof input === 'string') { if (typeof input === 'string') {
return `${input}${count === 1 ? '' : 's'}`; return `${input}${count === 1 ? '' : 's'}`;
} }
return input[count === 1 ? 'one' : 'many']; return input[count === 1 ? 'one' : 'many'];
}; };
export const interpolate = (count: number, input: { one: string, many: string }) => { export const interpolate = (count: number, input: { one: string; many: string }) => {
const text = count === 1 ? input.one : input.many; const text = count === 1 ? input.one : input.many;
return text.replaceAll('%count%', count.toString()); return text.replaceAll('%count%', count.toString());
}; };

View File

@@ -11,16 +11,12 @@ import DeleteIcon from '@mui/icons-material/Delete';
import DragHandle from '@mui/icons-material/DragHandle'; import DragHandle from '@mui/icons-material/DragHandle';
import PauseIcon from '@mui/icons-material/Pause'; import PauseIcon from '@mui/icons-material/Pause';
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import { import { Card, CardActionArea, Stack } from '@mui/material';
Card, CardActionArea, Stack,
} from '@mui/material';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import NavbarContext from 'components/context/NavbarContext'; import NavbarContext from 'components/context/NavbarContext';
import EmptyView from 'components/util/EmptyView'; import EmptyView from 'components/util/EmptyView';
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect } from 'react';
import { import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
DragDropContext, Draggable, Droppable, DropResult,
} from 'react-beautiful-dnd';
import client from 'util/client'; import client from 'util/client';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
@@ -56,8 +52,7 @@ const DownloadQueue: React.FC = () => {
}, []); }, []);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const onDragEnd = (result: DropResult) => { const onDragEnd = (result: DropResult) => {};
};
if (queue.length === 0) { if (queue.length === 0) {
return <EmptyView message="No downloads" />; return <EmptyView message="No downloads" />;
@@ -65,14 +60,15 @@ const DownloadQueue: React.FC = () => {
const handleDelete = (chapter: IChapter) => { const handleDelete = (chapter: IChapter) => {
// required to stop before deleting otherwise the download kept going. Server issue? // required to stop before deleting otherwise the download kept going. Server issue?
client.get('/api/v1/downloads/stop') client.get('/api/v1/downloads/stop').then(() =>
.then(() => Promise.all([ Promise.all([
// remove from download queue // remove from download queue
client.delete(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`), client.delete(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`),
// delete partial download, should be handle server side? // delete partial download, should be handle server side?
// bug: The folder and the last image downloaded are not deleted // bug: The folder and the last image downloaded are not deleted
client.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`), client.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`),
])); ]),
);
}; };
return ( return (
@@ -101,22 +97,38 @@ const DownloadQueue: React.FC = () => {
> >
<Card <Card
sx={{ sx={{
backgroundColor: snapshot.isDragging ? 'custom.light' : undefined, backgroundColor: snapshot.isDragging
? 'custom.light'
: undefined,
}} }}
> >
<CardActionArea <CardActionArea
component={Link} component={Link}
to={{ pathname: `/manga/${item.chapter.mangaId}`, state: { backLink: BACK } }} to={{
sx={{ display: 'flex', alignItems: 'center', p: 1 }} pathname: `/manga/${item.chapter.mangaId}`,
state: { backLink: BACK },
}}
sx={{
display: 'flex',
alignItems: 'center',
p: 1,
}}
> >
<IconButton sx={{ pointerEvents: 'none' }}> <IconButton sx={{ pointerEvents: 'none' }}>
<DragHandle /> <DragHandle />
</IconButton> </IconButton>
<Stack sx={{ flex: 1, ml: 1 }} direction="column"> <Stack
sx={{ flex: 1, ml: 1 }}
direction="column"
>
<Typography variant="h6"> <Typography variant="h6">
{item.manga.title} {item.manga.title}
</Typography> </Typography>
<Typography variant="caption" display="block" gutterBottom> <Typography
variant="caption"
display="block"
gutterBottom
>
{item.chapter.name} {item.chapter.name}
</Typography> </Typography>
</Stack> </Stack>

View File

@@ -5,9 +5,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { import React, { useContext, useEffect, useState, useMemo, useRef } from 'react';
useContext, useEffect, useState, useMemo, useRef,
} from 'react';
import { fromEvent } from 'file-selector'; import { fromEvent } from 'file-selector';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
@@ -30,7 +28,7 @@ const EXTENSIONS = 1;
const allLangs: string[] = []; const allLangs: string[] = [];
interface GroupedExtension { interface GroupedExtension {
[key: string]: IExtension[] [key: string]: IExtension[];
} }
function groupExtensions(extensions: IExtension[]) { function groupExtensions(extensions: IExtension[]) {
@@ -39,7 +37,9 @@ function groupExtensions(extensions: IExtension[]) {
extensions.forEach((extension) => { extensions.forEach((extension) => {
if (sortedExtenions[extension.lang] === undefined) { if (sortedExtenions[extension.lang] === undefined) {
sortedExtenions[extension.lang] = []; sortedExtenions[extension.lang] = [];
if (extension.lang !== 'all') { allLangs.push(extension.lang); } if (extension.lang !== 'all') {
allLangs.push(extension.lang);
}
} }
if (extension.installed) { if (extension.installed) {
if (extension.hasUpdate) { if (extension.hasUpdate) {
@@ -67,7 +67,10 @@ function groupExtensions(extensions: IExtension[]) {
export default function MangaExtensions() { export default function MangaExtensions() {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs()); const [shownLangs, setShownLangs] = useLocalStorage<string[]>(
'shownExtensionLangs',
extensionDefaultLangs(),
);
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true); const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
@@ -78,10 +81,7 @@ export default function MangaExtensions() {
setAction( setAction(
<> <>
<AppbarSearch /> <AppbarSearch />
<IconButton <IconButton onClick={() => inputRef.current?.click()} size="large">
onClick={() => inputRef.current?.click()}
size="large"
>
<AddIcon /> <AddIcon />
</IconButton> </IconButton>
<LangSelect <LangSelect
@@ -93,17 +93,33 @@ export default function MangaExtensions() {
); );
}, [shownLangs]); }, [shownLangs]);
const { data: allExtensions, mutate, loading } = useQuery<IExtension[]>('/api/v1/extension/list'); const {
data: allExtensions,
mutate,
loading,
} = useQuery<IExtension[]>('/api/v1/extension/list');
const filteredExtensions = useMemo(() => (allExtensions ?? []).filter((ext) => { const filteredExtensions = useMemo(
() =>
(allExtensions ?? []).filter((ext) => {
const nsfwFilter = showNsfw || !ext.isNsfw; const nsfwFilter = showNsfw || !ext.isNsfw;
if (!query) return nsfwFilter; if (!query) return nsfwFilter;
return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase()); return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase());
}), [allExtensions, showNsfw, query]); }),
[allExtensions, showNsfw, query],
);
const groupedExtensions = useMemo(() => groupExtensions(filteredExtensions) const groupedExtensions = useMemo(
() =>
groupExtensions(filteredExtensions)
.filter((group) => group[EXTENSIONS].length > 0) .filter((group) => group[EXTENSIONS].length > 0)
.filter((group) => ['installed', 'updates pending', 'all', ...shownLangs].includes(group[LANGUAGE])), [shownLangs, filteredExtensions]); .filter((group) =>
['installed', 'updates pending', 'all', ...shownLangs].includes(
group[LANGUAGE],
),
),
[shownLangs, filteredExtensions],
);
const flatRenderItems: (IExtension | string)[] = groupedExtensions.flat(2); const flatRenderItems: (IExtension | string)[] = groupedExtensions.flat(2);
@@ -119,8 +135,10 @@ export default function MangaExtensions() {
} }
makeToast('Installing Extension File....', 'info'); makeToast('Installing Extension File....', 'info');
client.post('/api/v1/extension/install', client
formData, { headers: { 'Content-Type': 'multipart/form-data' } }) .post('/api/v1/extension/install', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
.then(() => { .then(() => {
makeToast('Installed extension successfully!', 'success'); makeToast('Installed extension successfully!', 'success');
mutate(); mutate();
@@ -175,7 +193,7 @@ export default function MangaExtensions() {
}} }}
totalCount={flatRenderItems.length} totalCount={flatRenderItems.length}
itemContent={(index) => { itemContent={(index) => {
if (typeof (flatRenderItems[index]) === 'string') { if (typeof flatRenderItems[index] === 'string') {
const item = flatRenderItems[index] as string; const item = flatRenderItems[index] as string;
return ( return (
<Typography <Typography

View File

@@ -6,9 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { Tab, Tabs } from '@mui/material'; import { Tab, Tabs } from '@mui/material';
import React, { import React, { useContext, useEffect, useState } from 'react';
useContext, useEffect, useState,
} from 'react';
import NavbarContext from 'components/context/NavbarContext'; import NavbarContext from 'components/context/NavbarContext';
import EmptyView from 'components/util/EmptyView'; import EmptyView from 'components/util/EmptyView';
import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
@@ -62,7 +60,12 @@ export default function Library() {
}; };
if (tabsError != null) { if (tabsError != null) {
return <EmptyView message="Could not load categories" messageExtra={tabsError?.message ?? tabsError} />; return (
<EmptyView
message="Could not load categories"
messageExtra={tabsError?.message ?? tabsError}
/>
);
} }
if (loading) { if (loading) {
@@ -109,11 +112,13 @@ export default function Library() {
</Tabs> </Tabs>
{tabs.map((tab) => ( {tabs.map((tab) => (
<TabPanel key={tab.order} index={tab.order} currentIndex={activeTab.order}> <TabPanel key={tab.order} index={tab.order} currentIndex={activeTab.order}>
{tab === activeTab && (mangaError {tab === activeTab &&
? ( (mangaError ? (
<EmptyView message="Could not load manga" messageExtra={mangaError?.message ?? mangaError} /> <EmptyView
) message="Could not load manga"
: ( messageExtra={mangaError?.message ?? mangaError}
/>
) : (
<LibraryMangaGrid <LibraryMangaGrid
mangas={mangas} mangas={mangas}
lastLibraryUpdate={lastLibraryUpdate} lastLibraryUpdate={lastLibraryUpdate}

View File

@@ -6,9 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { Warning } from '@mui/icons-material'; import { Warning } from '@mui/icons-material';
import { import { CircularProgress, IconButton, Stack, Tooltip } from '@mui/material';
CircularProgress, IconButton, Stack, Tooltip,
} from '@mui/material';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
import NavbarContext, { useSetDefaultBackTo } from 'components/context/NavbarContext'; import NavbarContext, { useSetDefaultBackTo } from 'components/context/NavbarContext';
import ChapterList from 'components/manga/ChapterList'; import ChapterList from 'components/manga/ChapterList';
@@ -18,9 +16,7 @@ import MangaToolbarMenu from 'components/manga/MangaToolbarMenu';
import { NavbarToolbar } from 'components/navbar/DefaultNavBar'; import { NavbarToolbar } from 'components/navbar/DefaultNavBar';
import EmptyView from 'components/util/EmptyView'; import EmptyView from 'components/util/EmptyView';
import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
import React, { import React, { useContext, useEffect, useRef } from 'react';
useContext, useEffect, useRef,
} from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useQuery } from 'util/client'; import { useQuery } from 'util/client';
@@ -32,12 +28,20 @@ const Manga: React.FC = () => {
const autofetchedRef = useRef(false); const autofetchedRef = useRef(false);
const { const {
data: manga, error, loading, isValidating, mutate, data: manga,
error,
loading,
isValidating,
mutate,
} = useQuery<IManga>(`/api/v1/manga/${id}/?onlineFetch=false`); } = useQuery<IManga>(`/api/v1/manga/${id}/?onlineFetch=false`);
const [refresh, { loading: refreshing }] = useRefreshManga(id); const [refresh, { loading: refreshing }] = useRefreshManga(id);
useSetDefaultBackTo(manga?.inLibrary === false && manga.sourceId != null ? `/sources/${manga.sourceId}/popular` : '/library'); useSetDefaultBackTo(
manga?.inLibrary === false && manga.sourceId != null
? `/sources/${manga.sourceId}/popular`
: '/library',
);
useEffect(() => { useEffect(() => {
// Automatically fetch manga from source if data is older then 24 hours // Automatically fetch manga from source if data is older then 24 hours
@@ -45,9 +49,9 @@ const Manga: React.FC = () => {
// not update age for some reason (ie. error on source side) // not update age for some reason (ie. error on source side)
if (manga == null) return; if (manga == null) return;
if ( if (
manga.inLibrary manga.inLibrary &&
&& (manga.age > AUTOFETCH_AGE || manga.chaptersAge > AUTOFETCH_AGE) (manga.age > AUTOFETCH_AGE || manga.chaptersAge > AUTOFETCH_AGE) &&
&& autofetchedRef.current === false autofetchedRef.current === false
) { ) {
autofetchedRef.current = true; autofetchedRef.current = true;
refresh(); refresh();
@@ -59,29 +63,28 @@ const Manga: React.FC = () => {
}, [manga?.title]); }, [manga?.title]);
if (error && !manga) { if (error && !manga) {
return ( return <EmptyView message="Could not load manga" messageExtra={error.message ?? error} />;
<EmptyView message="Could not load manga" messageExtra={error.message ?? error} />
);
} }
return ( return (
<Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}> <Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>
<NavbarToolbar> <NavbarToolbar>
<Stack direction="row" alignItems="center"> <Stack direction="row" alignItems="center">
{error && !isValidating && !refreshing && ( {error && !isValidating && !refreshing && (
<Tooltip title={( <Tooltip
title={
<> <>
Could not fetch manga data Could not fetch manga data
<br /> <br />
{error.message ?? error} {error.message ?? error}
</> </>
)} }
> >
<IconButton onClick={() => mutate()}> <IconButton onClick={() => mutate()}>
<Warning color="error" /> <Warning color="error" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
)} )}
{(manga && (refreshing || isValidating)) && ( {manga && (refreshing || isValidating) && (
<IconButton disabled> <IconButton disabled>
<CircularProgress size={16} /> <CircularProgress size={16} />
</IconButton> </IconButton>

View File

@@ -6,9 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import React, { import React, { useCallback, useContext, useEffect, useState } from 'react';
useCallback, useContext, useEffect, useState,
} from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import HorizontalPager from 'components/reader/pager/HorizontalPager'; import HorizontalPager from 'components/reader/pager/HorizontalPager';
import PageNumber from 'components/reader/PageNumber'; import PageNumber from 'components/reader/PageNumber';
@@ -67,22 +65,26 @@ export default function Reader() {
const [serverAddress] = useLocalStorage<String>('serverBaseURL', ''); const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string, mangaId: string }>(); const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>();
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' }); const [manga, setManga] = useState<IMangaCard | IManga>({
id: +mangaId,
title: '',
thumbnailUrl: '',
});
const [chapter, setChapter] = useState<IChapter | IPartialChapter>(initialChapter()); const [chapter, setChapter] = useState<IChapter | IPartialChapter>(initialChapter());
const [curPage, setCurPage] = useState<number>(0); const [curPage, setCurPage] = useState<number>(0);
const { setOverride, setTitle } = useContext(NavbarContext); const { setOverride, setTitle } = useContext(NavbarContext);
const { const { settings: defaultSettings, loading: areDefaultSettingsLoading } =
settings: defaultSettings, useDefaultReaderSettings();
loading: areDefaultSettingsLoading,
} = useDefaultReaderSettings();
const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings)); const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings));
const [isMangaLoading, setIsMangaLoading] = useState(true); const [isMangaLoading, setIsMangaLoading] = useState(true);
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => { const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
setSettings({ ...settings, [key]: value }); setSettings({ ...settings, [key]: value });
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() => makeToast('Failed to save the reader settings to the server', 'warning')); requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
makeToast('Failed to save the reader settings to the server', 'warning'),
);
}; };
useEffect(() => { useEffect(() => {
@@ -95,15 +97,16 @@ export default function Reader() {
useEffect(() => { useEffect(() => {
if (!areDefaultSettingsLoading && !isMangaLoading) { if (!areDefaultSettingsLoading && !isMangaLoading) {
checkAndHandleMissingStoredReaderSettings(manga, 'manga', defaultSettings).catch(() => {}); checkAndHandleMissingStoredReaderSettings(manga, 'manga', defaultSettings).catch(
() => {},
);
setSettings(getReaderSettingsFor(manga, defaultSettings)); setSettings(getReaderSettingsFor(manga, defaultSettings));
} }
}, [areDefaultSettingsLoading, isMangaLoading]); }, [areDefaultSettingsLoading, isMangaLoading]);
useEffect(() => { useEffect(() => {
// set the custom navbar // set the custom navbar
setOverride( setOverride({
{
status: true, status: true,
value: ( value: (
<ReaderNavBar <ReaderNavBar
@@ -114,8 +117,7 @@ export default function Reader() {
curPage={curPage} curPage={curPage}
/> />
), ),
}, });
);
// clean up for when we leave the reader // clean up for when we leave the reader
return () => setOverride({ status: false, value: <div /> }); return () => setOverride({ status: false, value: <div /> });
@@ -123,7 +125,8 @@ export default function Reader() {
useEffect(() => { useEffect(() => {
setIsMangaLoading(true); setIsMangaLoading(true);
client.get(`/api/v1/manga/${mangaId}/`) client
.get(`/api/v1/manga/${mangaId}/`)
.then((response) => response.data) .then((response) => response.data)
.then((data: IManga) => { .then((data: IManga) => {
setManga(data); setManga(data);
@@ -133,7 +136,8 @@ export default function Reader() {
useEffect(() => { useEffect(() => {
setChapter(initialChapter); setChapter(initialChapter);
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`) client
.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
.then((response) => response.data) .then((response) => response.data)
.then((data: IChapter) => { .then((data: IChapter) => {
setChapter(data); setChapter(data);
@@ -166,21 +170,31 @@ export default function Reader() {
formData.append('read', 'true'); formData.append('read', 'true');
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData); client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
history.replace({ pathname: `/manga/${manga.id}/chapter/${chapter.index + 1}`, state: history.location.state }); history.replace({
pathname: `/manga/${manga.id}/chapter/${chapter.index + 1}`,
state: history.location.state,
});
} }
}, [chapter.index, chapter.chapterCount, chapter.pageCount, manga.id]); }, [chapter.index, chapter.chapterCount, chapter.pageCount, manga.id]);
const prevChapter = useCallback(() => { const prevChapter = useCallback(() => {
if (chapter.index > 1) { if (chapter.index > 1) {
history.replace({ pathname: `/manga/${manga.id}/chapter/${chapter.index - 1}`, state: history.location.state }); history.replace({
pathname: `/manga/${manga.id}/chapter/${chapter.index - 1}`,
state: history.location.state,
});
} }
}, [chapter.index, manga.id]); }, [chapter.index, manga.id]);
// return spinner while chpater data is loading // return spinner while chpater data is loading
if (chapter.pageCount === -1) { if (chapter.pageCount === -1) {
return ( return (
<Box sx={{ <Box
height: '100vh', width: '100vw', display: 'grid', placeItems: 'center', sx={{
height: '100vh',
width: '100vw',
display: 'grid',
placeItems: 'center',
}} }}
> >
<CircularProgress thickness={5} /> <CircularProgress thickness={5} />
@@ -196,15 +210,11 @@ export default function Reader() {
const ReaderComponent = getReaderComponent(settings.readerType); const ReaderComponent = getReaderComponent(settings.readerType);
// last page, also probably read = true, we will load the first page. // last page, also probably read = true, we will load the first page.
const initialPage = (chapter.lastPageRead === chapter.pageCount - 1) ? 0 : chapter.lastPageRead; const initialPage = chapter.lastPageRead === chapter.pageCount - 1 ? 0 : chapter.lastPageRead;
return ( return (
<Box sx={{ width: settings.staticNav ? 'calc(100vw - 300px)' : '100vw' }}> <Box sx={{ width: settings.staticNav ? 'calc(100vw - 300px)' : '100vw' }}>
<PageNumber <PageNumber settings={settings} curPage={curPage} pageCount={chapter.pageCount} />
settings={settings}
curPage={curPage}
pageCount={chapter.pageCount}
/>
<ReaderComponent <ReaderComponent
pages={pages} pages={pages}
pageCount={chapter.pageCount} pageCount={chapter.pageCount}

View File

@@ -17,7 +17,10 @@ import { Link } from 'react-router-dom';
import { StringParam, useQueryParam } from 'use-query-params'; import { StringParam, useQueryParam } from 'use-query-params';
import client from 'util/client'; import client from 'util/client';
import { import {
langCodeToName, langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs, langCodeToName,
langSortCmp,
sourceDefualtLangs,
sourceForcedDefaultLangs,
} from 'util/language'; } from 'util/language';
import useLocalStorage from 'util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
@@ -25,7 +28,9 @@ function sourceToLangList(sources: ISource[]) {
const result: string[] = []; const result: string[] = [];
sources.forEach((source) => { sources.forEach((source) => {
if (result.indexOf(source.lang) === -1) { result.push(source.lang); } if (result.indexOf(source.lang) === -1) {
result.push(source.lang);
}
}); });
result.sort(langSortCmp); result.sort(langSortCmp);
@@ -38,7 +43,10 @@ const SearchAll: React.FC = () => {
const [triggerUpdate, setTriggerUpdate] = useState<number>(2); const [triggerUpdate, setTriggerUpdate] = useState<number>(2);
const [mangas, setMangas] = useState<any>({}); const [mangas, setMangas] = useState<any>({});
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs()); const [shownLangs, setShownLangs] = useLocalStorage<string[]>(
'shownSourceLangs',
sourceDefualtLangs(),
);
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true); const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
const [sources, setSources] = useState<ISource[]>([]); const [sources, setSources] = useState<ISource[]>([]);
@@ -56,26 +64,36 @@ const SearchAll: React.FC = () => {
setAction( setAction(
<> <>
<AppbarSearch /> <AppbarSearch />
</> </>,
,
); );
}, []); }, []);
useEffect(() => { useEffect(() => {
client.get('/api/v1/source/list') client
.get('/api/v1/source/list')
.then((response) => response.data) .then((response) => response.data)
.then((data) => { .then((data) => {
setSources(data.sort((a: { displayName: string; }, b: { displayName: string; }) => { setSources(
if (a.displayName < b.displayName) { return -1; } data.sort((a: { displayName: string }, b: { displayName: string }) => {
if (a.displayName > b.displayName) { return 1; } if (a.displayName < b.displayName) {
return -1;
}
if (a.displayName > b.displayName) {
return 1;
}
return 0; return 0;
})); setFetchedSources(true); }),
);
setFetchedSources(true);
}); });
}, []); }, []);
async function doIT(elem: any[]) { async function doIT(elem: any[]) {
elem.map((ele) => limit.add(async () => { elem.map((ele) =>
const response = await client.get(`/api/v1/source/${ele.id}/search?searchTerm=${query || ''}&pageNum=1`); limit.add(async () => {
const response = await client.get(
`/api/v1/source/${ele.id}/search?searchTerm=${query || ''}&pageNum=1`,
);
const data = await response.data; const data = await response.data;
const tmp = mangas; const tmp = mangas;
tmp[ele.id] = data.mangaList; tmp[ele.id] = data.mangaList;
@@ -84,7 +102,8 @@ const SearchAll: React.FC = () => {
tmp2[ele.id] = true; tmp2[ele.id] = true;
setFetched(tmp2); setFetched(tmp2);
setResetUI(1); setResetUI(1);
})); }),
);
} }
useEffect(() => { useEffect(() => {
@@ -98,7 +117,11 @@ const SearchAll: React.FC = () => {
setFetched({}); setFetched({});
setMangas({}); setMangas({});
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
doIT(sources.filter(({ lang }) => shownLangs.indexOf(lang) !== -1).filter((source) => showNsfw || !source.isNsfw)); doIT(
sources
.filter(({ lang }) => shownLangs.indexOf(lang) !== -1)
.filter((source) => showNsfw || !source.isNsfw),
);
}, [triggerUpdate]); }, [triggerUpdate]);
useEffect(() => { useEffect(() => {
@@ -137,9 +160,7 @@ const SearchAll: React.FC = () => {
setTitle('Sources'); setTitle('Sources');
setAction( setAction(
<> <>
<AppbarSearch <AppbarSearch autoOpen />
autoOpen
/>
<LangSelect <LangSelect
shownLangs={shownLangs} shownLangs={shownLangs}
setShownLangs={setShownLangs} setShownLangs={setShownLangs}
@@ -154,20 +175,33 @@ const SearchAll: React.FC = () => {
return ( return (
<> <>
{/* eslint-disable-next-line max-len */} {/* eslint-disable-next-line max-len */}
{sources.filter(({ lang }) => shownLangs.indexOf(lang) !== -1).filter((source) => showNsfw || !source.isNsfw).sort((a, b) => { {sources
.filter(({ lang }) => shownLangs.indexOf(lang) !== -1)
.filter((source) => showNsfw || !source.isNsfw)
.sort((a, b) => {
const af = fetched[a.id]; const af = fetched[a.id];
const bf = fetched[b.id]; const bf = fetched[b.id];
if (af && !bf) { return -1; } if (af && !bf) {
if (!af && bf) { return 1; } return -1;
if (!af && !bf) { return 0; } }
if (!af && bf) {
return 1;
}
if (!af && !bf) {
return 0;
}
const al = mangas[a.id].length === 0; const al = mangas[a.id].length === 0;
const bl = mangas[b.id].length === 0; const bl = mangas[b.id].length === 0;
if (al && !bl) { return 1; } if (al && !bl) {
if (bl && !al) { return -1; } return 1;
}
if (bl && !al) {
return -1;
}
return 0; return 0;
}).map(({ lang, id, displayName }) => ( })
( .map(({ lang, id, displayName }) => (
<> <>
<Card sx={{ margin: '10px' }}> <Card sx={{ margin: '10px' }}>
<CardActionArea <CardActionArea
@@ -175,9 +209,7 @@ const SearchAll: React.FC = () => {
to={`/sources/${id}/popular/?R&query=${query}`} to={`/sources/${id}/popular/?R&query=${query}`}
sx={{ p: 3 }} sx={{ p: 3 }}
> >
<Typography variant="h5"> <Typography variant="h5">{displayName}</Typography>
{displayName}
</Typography>
<Typography variant="caption"> <Typography variant="caption">
{langCodeToName(lang)} {langCodeToName(lang)}
</Typography> </Typography>
@@ -195,13 +227,11 @@ const SearchAll: React.FC = () => {
inLibraryIndicator inLibraryIndicator
/> />
</> </>
)
))} ))}
</> </>
); );
} }
return (<></>); return <></>;
}; };
export default SearchAll; export default SearchAll;

View File

@@ -38,7 +38,10 @@ import ListItemLink from 'components/util/ListItemLink';
export default function Settings() { export default function Settings() {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Settings'); setAction(<></>); }, []); useEffect(() => {
setTitle('Settings');
setAction(<></>);
}, []);
const { darkTheme, setDarkTheme } = useContext(DarkTheme); const { darkTheme, setDarkTheme } = useContext(DarkTheme);
const [serverAddress, setServerAddress] = useLocalStorage<String>('serverBaseURL', ''); const [serverAddress, setServerAddress] = useLocalStorage<String>('serverBaseURL', '');
@@ -193,9 +196,7 @@ export default function Settings() {
<Dialog open={dialogOpen} onClose={handleDialogCancel}> <Dialog open={dialogOpen} onClose={handleDialogCancel}>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>Enter Server Address</DialogContentText>
Enter Server Address
</DialogContentText>
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
@@ -219,9 +220,7 @@ export default function Settings() {
</Dialog> </Dialog>
<Dialog open={dialogOpenItemWidth} onClose={handleDialogCancelItemWidth}> <Dialog open={dialogOpenItemWidth} onClose={handleDialogCancelItemWidth}>
<DialogTitle> <DialogTitle>Manga Item width</DialogTitle>
Manga Item width
</DialogTitle>
<DialogContent <DialogContent
sx={{ sx={{
width: '98%', width: '98%',

View File

@@ -9,7 +9,10 @@ import React, { useContext, useEffect } from 'react';
import NavbarContext from 'components/context/NavbarContext'; import NavbarContext from 'components/context/NavbarContext';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import client, { useQuery } from 'util/client'; import client, { useQuery } from 'util/client';
import { SwitchPreferenceCompat, CheckBoxPreference } from 'components/sourceConfiguration/TwoStatePreference'; import {
SwitchPreferenceCompat,
CheckBoxPreference,
} from 'components/sourceConfiguration/TwoStatePreference';
import ListPreference from 'components/sourceConfiguration/ListPreference'; import ListPreference from 'components/sourceConfiguration/ListPreference';
import EditTextPreference from 'components/sourceConfiguration/EditTextPreference'; import EditTextPreference from 'components/sourceConfiguration/EditTextPreference';
import MultiSelectListPreference from 'components/sourceConfiguration/MultiSelectListPreference'; import MultiSelectListPreference from 'components/sourceConfiguration/MultiSelectListPreference';
@@ -36,10 +39,15 @@ function getPrefComponent(type: string) {
export default function SourceConfigure() { export default function SourceConfigure() {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Source Configuration'); setAction(<></>); }, []); useEffect(() => {
setTitle('Source Configuration');
setAction(<></>);
}, []);
const { sourceId } = useParams<{ sourceId: string }>(); const { sourceId } = useParams<{ sourceId: string }>();
const { data: sourcePreferences = [], mutate } = useQuery<SourcePreferences[]>(`/api/v1/source/${sourceId}/preferences`); const { data: sourcePreferences = [], mutate } = useQuery<SourcePreferences[]>(
`/api/v1/source/${sourceId}/preferences`,
);
const convertToString = (position: number, value: any): string => { const convertToString = (position: number, value: any): string => {
switch (sourcePreferences[position].props.defaultValueType) { switch (sourcePreferences[position].props.defaultValueType) {
@@ -50,19 +58,19 @@ export default function SourceConfigure() {
} }
}; };
const updateValue = (position: number) => ( const updateValue = (position: number) => (value: any) => {
(value: any) => { client
client.post(`/api/v1/source/${sourceId}/preferences`, .post(
JSON.stringify({ position, value: convertToString(position, value) })) `/api/v1/source/${sourceId}/preferences`,
JSON.stringify({ position, value: convertToString(position, value) }),
)
.then(() => mutate()); .then(() => mutate());
} };
);
return ( return (
<> <>
<List sx={{ padding: 0 }}> <List sx={{ padding: 0 }}>
{sourcePreferences.map( {sourcePreferences.map((it, index) => {
(it, index) => {
const props = cloneObject(it.props); const props = cloneObject(it.props);
props.updateValue = updateValue(index); props.updateValue = updateValue(index);
props.key = index; props.key = index;
@@ -70,8 +78,7 @@ export default function SourceConfigure() {
// TypeScript is dumb in detecting extra props // TypeScript is dumb in detecting extra props
// @ts-ignore // @ts-ignore
return React.createElement(getPrefComponent(it.type), props); return React.createElement(getPrefComponent(it.type), props);
}, })}
)}
</List> </List>
</> </>
); );

View File

@@ -19,9 +19,9 @@ import SourceGridLayout from 'components/source/GridLayouts';
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext'; import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
interface IPos { interface IPos {
position: number position: number;
state: any state: any;
group?: number group?: number;
} }
export default function SourceMangas(props: { popular: boolean }) { export default function SourceMangas(props: { popular: boolean }) {
@@ -48,7 +48,8 @@ export default function SourceMangas(props: { popular: boolean }) {
const { options } = useLibraryOptionsContext(); const { options } = useLibraryOptionsContext();
function makeFilters() { function makeFilters() {
client.get(`/api/v1/source/${sourceId}/filters`) client
.get(`/api/v1/source/${sourceId}/filters`)
.then((response) => response.data) .then((response) => response.data)
.then((data: ISourceFilters[]) => { .then((data: ISourceFilters[]) => {
SetData(data); SetData(data);
@@ -60,7 +61,8 @@ export default function SourceMangas(props: { popular: boolean }) {
}, []); }, []);
useEffect(() => { useEffect(() => {
client.get(`/api/v1/source/${sourceId}`) client
.get(`/api/v1/source/${sourceId}`)
.then((response) => response.data) .then((response) => response.data)
.then((data: ISource) => { .then((data: ISource) => {
setTitle(data.displayName); setTitle(data.displayName);
@@ -79,20 +81,25 @@ export default function SourceMangas(props: { popular: boolean }) {
if (update.length > 0) { if (update.length > 0) {
const rep = update; const rep = update;
setUpdate([]); setUpdate([]);
client.post(`/api/v1/source/${sourceId}/filters`, client
.post(
`/api/v1/source/${sourceId}/filters`,
rep.map((e: IPos) => { rep.map((e: IPos) => {
const { position, state, group }: IPos = e; const { position, state, group }: IPos = e;
return group === undefined ? { return group === undefined
? {
position, position,
state, state,
} : { }
: {
position: group, position: group,
state: JSON.stringify({ state: JSON.stringify({
position, position,
state, state,
}), }),
}; };
})) }),
)
.then(() => { .then(() => {
setTriggerUpdate(0); setTriggerUpdate(0);
makeFilters(); makeFilters();
@@ -101,7 +108,9 @@ export default function SourceMangas(props: { popular: boolean }) {
setFetched(false); setFetched(false);
setMangas([]); setMangas([]);
setLastPageNum(0); setLastPageNum(0);
if (Noreset === undefined && Search) { setNoreset(null); } if (Noreset === undefined && Search) {
setNoreset(null);
}
} }
}, [triggerUpdate]); }, [triggerUpdate]);
@@ -111,8 +120,7 @@ export default function SourceMangas(props: { popular: boolean }) {
setNoreset(undefined); setNoreset(undefined);
setReset(1); setReset(1);
} else if (Noreset === undefined) { } else if (Noreset === undefined) {
client.get(`/api/v1/source/${sourceId}/filters?reset=true`) client.get(`/api/v1/source/${sourceId}/filters?reset=true`).then(() => {
.then(() => {
makeFilters(); makeFilters();
setSearch(false); setSearch(false);
if (reset === 1) { if (reset === 1) {
@@ -140,8 +148,7 @@ export default function SourceMangas(props: { popular: boolean }) {
<SettingsIcon /> <SettingsIcon />
</IconButton> </IconButton>
)} )}
</> </>,
,
); );
return () => { return () => {
@@ -152,8 +159,12 @@ export default function SourceMangas(props: { popular: boolean }) {
useEffect(() => { useEffect(() => {
if (query) { if (query) {
setSearch(true); setSearch(true);
} else { setSearch(false); } } else {
if (Noreset === undefined) { setInit(null); } setSearch(false);
}
if (Noreset === undefined) {
setInit(null);
}
}, [query]); }, [query]);
useEffect(() => { useEffect(() => {
@@ -163,16 +174,27 @@ export default function SourceMangas(props: { popular: boolean }) {
}, 1000); }, 1000);
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn);
} }
if (Search !== undefined) { setInit(null); } if (Search !== undefined) {
setInit(null);
}
return () => {}; return () => {};
}, [Search, query]); }, [Search, query]);
useEffect(() => { useEffect(() => {
if (lastPageNum !== 0) { if (lastPageNum !== 0) {
const sourceType = props.popular ? 'popular' : 'latest'; 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}`}`) 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((response) => response.data)
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => { .then((data: { mangaList: IManga[]; hasNextPage: boolean }) => {
setMangas([ setMangas([
...mangas, ...mangas,
...data.mangaList.map((it) => ({ ...data.mangaList.map((it) => ({
@@ -180,11 +202,14 @@ export default function SourceMangas(props: { popular: boolean }) {
thumbnailUrl: it.thumbnailUrl, thumbnailUrl: it.thumbnailUrl,
id: it.id, id: it.id,
inLibrary: it.inLibrary, inLibrary: it.inLibrary,
}))]); })),
]);
setHasNextPage(data.hasNextPage); setHasNextPage(data.hasNextPage);
setFetched(true); setFetched(true);
}); });
} else { setLastPageNum(1); } } else {
setLastPageNum(1);
}
}, [lastPageNum]); }, [lastPageNum]);
let message; let message;
@@ -196,7 +221,9 @@ export default function SourceMangas(props: { popular: boolean }) {
messageExtra = ( messageExtra = (
<> <>
<span>Check out </span> <span>Check out </span>
<a href="https://github.com/Suwayomi/Tachidesk-Server/wiki/Local-Source">Local source guide</a> <a href="https://github.com/Suwayomi/Tachidesk-Server/wiki/Local-Source">
Local source guide
</a>
</> </>
); );
} }

View File

@@ -10,7 +10,10 @@ import LangSelect from 'components/navbar/action/LangSelect';
import SourceCard from 'components/SourceCard'; import SourceCard from 'components/SourceCard';
import NavbarContext from 'components/context/NavbarContext'; import NavbarContext from 'components/context/NavbarContext';
import { import {
sourceDefualtLangs, sourceForcedDefaultLangs, langCodeToName, langSortCmp, sourceDefualtLangs,
sourceForcedDefaultLangs,
langCodeToName,
langSortCmp,
} from 'util/language'; } from 'util/language';
import useLocalStorage from 'util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
@@ -23,7 +26,9 @@ function sourceToLangList(sources: ISource[]) {
const result: string[] = []; const result: string[] = [];
sources.forEach((source) => { sources.forEach((source) => {
if (result.indexOf(source.lang) === -1) { result.push(source.lang); } if (result.indexOf(source.lang) === -1) {
result.push(source.lang);
}
}); });
result.sort(langSortCmp); result.sort(langSortCmp);
@@ -33,7 +38,9 @@ function sourceToLangList(sources: ISource[]) {
function groupByLang(sources: ISource[]) { function groupByLang(sources: ISource[]) {
const result = {} as any; const result = {} as any;
sources.forEach((source) => { sources.forEach((source) => {
if (result[source.lang] === undefined) { result[source.lang] = [] as ISource[]; } if (result[source.lang] === undefined) {
result[source.lang] = [] as ISource[];
}
result[source.lang].push(source); result[source.lang].push(source);
}); });
@@ -43,7 +50,10 @@ function groupByLang(sources: ISource[]) {
export default function Sources() { export default function Sources() {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs()); const [shownLangs, setShownLangs] = useLocalStorage<string[]>(
'shownSourceLangs',
sourceDefualtLangs(),
);
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true); const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
const { data: sources, loading } = useQuery<ISource[]>('/api/v1/source/list'); const { data: sources, loading } = useQuery<ISource[]>('/api/v1/source/list');
@@ -70,10 +80,7 @@ export default function Sources() {
setTitle('Sources'); setTitle('Sources');
setAction( setAction(
<> <>
<IconButton <IconButton onClick={() => history.push('/sources/all/search/')} size="large">
onClick={() => history.push('/sources/all/search/')}
size="large"
>
<TravelExploreIcon /> <TravelExploreIcon />
</IconButton> </IconButton>
<LangSelect <LangSelect
@@ -89,27 +96,29 @@ export default function Sources() {
if (loading) return <LoadingPlaceholder />; if (loading) return <LoadingPlaceholder />;
if (sources?.length === 0) { if (sources?.length === 0) {
return (<h3>No sources found. Install Some Extensions first.</h3>); return <h3>No sources found. Install Some Extensions first.</h3>;
} }
return ( return (
<> <>
{/* eslint-disable-next-line max-len */} {/* eslint-disable-next-line max-len */}
{Object.entries(groupByLang(sources ?? [])).sort((a, b) => langSortCmp(a[0], b[0])).map(([lang, list]) => ( {Object.entries(groupByLang(sources ?? []))
.sort((a, b) => langSortCmp(a[0], b[0]))
.map(
([lang, list]) =>
shownLangs.indexOf(lang) !== -1 && ( shownLangs.indexOf(lang) !== -1 && (
<React.Fragment key={lang}> <React.Fragment key={lang}>
<h1 key={lang} style={{ marginLeft: 25 }}>{langCodeToName(lang)}</h1> <h1 key={lang} style={{ marginLeft: 25 }}>
{langCodeToName(lang)}
</h1>
{(list as ISource[]) {(list as ISource[])
.filter((source) => showNsfw || !source.isNsfw) .filter((source) => showNsfw || !source.isNsfw)
.map((source) => ( .map((source) => (
<SourceCard <SourceCard key={source.id} source={source} />
key={source.id}
source={source}
/>
))} ))}
</React.Fragment> </React.Fragment>
) ),
))} )}
</> </>
); );
} }

View File

@@ -17,9 +17,7 @@ import NavbarContext from 'components/context/NavbarContext';
import DownloadStateIndicator from 'components/molecules/DownloadStateIndicator'; import DownloadStateIndicator from 'components/molecules/DownloadStateIndicator';
import EmptyView from 'components/util/EmptyView'; import EmptyView from 'components/util/EmptyView';
import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
import React, { import React, { useContext, useEffect, useRef, useState } from 'react';
useContext, useEffect, useRef, useState,
} from 'react';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import client from 'util/client'; import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
@@ -31,9 +29,11 @@ function epochToDate(epoch: number) {
} }
function isTheSameDay(first: Date, second: Date) { function isTheSameDay(first: Date, second: Date) {
return first.getDate() === second.getDate() return (
&& first.getMonth() === second.getMonth() first.getDate() === second.getDate() &&
&& first.getFullYear() === second.getFullYear(); first.getMonth() === second.getMonth() &&
first.getFullYear() === second.getFullYear()
);
} }
function getDateString(date: Date) { function getDateString(date: Date) {
@@ -46,8 +46,9 @@ function getDateString(date: Date) {
return date.toLocaleDateString(); return date.toLocaleDateString();
} }
function groupByDate(updates: IMangaChapter[]): function groupByDate(
[string, { item: IMangaChapter, globalIdx: number }[] ][] { updates: IMangaChapter[],
): [string, { item: IMangaChapter; globalIdx: number }[]][] {
if (updates.length === 0) return []; if (updates.length === 0) return [];
const groups = {}; const groups = {};
@@ -63,7 +64,10 @@ function groupByDate(updates: IMangaChapter[]):
return Object.keys(groups).map((key) => [key, groups[key]]); return Object.keys(groups).map((key) => [key, groups[key]]);
} }
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws'); const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace(
'http',
'ws',
);
const initialQueue = { const initialQueue = {
status: 'Stopped', status: 'Stopped',
queue: [], queue: [],
@@ -104,13 +108,11 @@ const Updates: React.FC = () => {
useEffect(() => { useEffect(() => {
if (hasNextPage) { if (hasNextPage) {
client.get(`/api/v1/update/recentChapters/${lastPageNum}`) client
.get(`/api/v1/update/recentChapters/${lastPageNum}`)
.then((response) => response.data) .then((response) => response.data)
.then(({ hasNextPage: fetchedHasNextPage, page }: PaginatedList<IMangaChapter>) => { .then(({ hasNextPage: fetchedHasNextPage, page }: PaginatedList<IMangaChapter>) => {
setUpdateEntries([ setUpdateEntries([...updateEntries, ...page]);
...updateEntries,
...page,
]);
setHasNextPage(fetchedHasNextPage); setHasNextPage(fetchedHasNextPage);
setFetched(true); setFetched(true);
}); });
@@ -122,7 +124,7 @@ const Updates: React.FC = () => {
const scrollHandler = () => { const scrollHandler = () => {
if (lastEntry.current) { if (lastEntry.current) {
const rect = lastEntry.current.getBoundingClientRect(); const rect = lastEntry.current.getBoundingClientRect();
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) { if ((rect.y + rect.height) / window.innerHeight < 2 && hasNextPage) {
setLastPageNum(lastPageNum + 1); setLastPageNum(lastPageNum + 1);
} }
} }
@@ -134,8 +136,12 @@ const Updates: React.FC = () => {
}; };
}, [hasNextPage, updateEntries]); }, [hasNextPage, updateEntries]);
if (!fetched) { return <LoadingPlaceholder />; } if (!fetched) {
if (fetched && updateEntries.length === 0) { return <EmptyView message="You don't have any updates yet." />; } return <LoadingPlaceholder />;
}
if (fetched && updateEntries.length === 0) {
return <EmptyView message="You don't have any updates yet." />;
}
const downloadForChapter = (chapter: IChapter) => { const downloadForChapter = (chapter: IChapter) => {
const { index, mangaId } = chapter; const { index, mangaId } = chapter;
@@ -170,7 +176,10 @@ const Updates: React.FC = () => {
> >
<CardActionArea <CardActionArea
component={Link} component={Link}
to={{ pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`, state: history.location.state }} to={{
pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`,
state: history.location.state,
}}
> >
<CardContent <CardContent
sx={{ sx={{
@@ -196,7 +205,11 @@ const Updates: React.FC = () => {
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
{manga.title} {manga.title}
</Typography> </Typography>
<Typography variant="caption" display="block" gutterBottom> <Typography
variant="caption"
display="block"
gutterBottom
>
{chapter.name} {chapter.name}
</Typography> </Typography>
</Box> </Box>

View File

@@ -10,7 +10,10 @@ import { useQuery } from 'util/client';
export default function About() { export default function About() {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('About'); setAction(<></>); }, []); useEffect(() => {
setTitle('About');
setAction(<></>);
}, []);
const { data: about } = useQuery<IAbout>('/api/v1/settings/about'); const { data: about } = useQuery<IAbout>('/api/v1/settings/about');

View File

@@ -17,7 +17,10 @@ import NavbarContext from 'components/context/NavbarContext';
export default function Backup() { export default function Backup() {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Backup'); setAction(<></>); }, []); useEffect(() => {
setTitle('Backup');
setAction(<></>);
}, []);
const { baseURL } = client.defaults; const { baseURL } = client.defaults;
@@ -27,8 +30,10 @@ export default function Backup() {
formData.append('backup.proto.gz', file); formData.append('backup.proto.gz', file);
makeToast('Restoring backup....', 'info'); makeToast('Restoring backup....', 'info');
client.post('/api/v1/backup/import/file', client
formData, { headers: { 'Content-Type': 'multipart/form-data' } }) .post('/api/v1/backup/import/file', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
.then(() => makeToast('Backup restore finished!', 'success')) .then(() => makeToast('Backup restore finished!', 'success'))
.catch(() => makeToast('Backup restore failed!', 'error')); .catch(() => makeToast('Backup restore failed!', 'error'));
} else if (file.name.toLowerCase().endsWith('json')) { } else if (file.name.toLowerCase().endsWith('json')) {
@@ -81,12 +86,7 @@ export default function Backup() {
/> />
</ListItem> </ListItem>
</List> </List>
<input <input type="file" id="backup-file" style={{ display: 'none' }} />
type="file"
id="backup-file"
style={{ display: 'none' }}
/>
</> </>
); );
} }

View File

@@ -7,18 +7,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { import React, { useMemo, useState, useContext, useEffect } from 'react';
useMemo, useState, useContext, useEffect, import { List, ListItem, ListItemText, ListItemIcon, IconButton } from '@mui/material';
} from 'react';
import { import {
List, DragDropContext,
ListItem, Droppable,
ListItemText, Draggable,
ListItemIcon, DropResult,
IconButton, DraggingStyle,
} from '@mui/material'; NotDraggingStyle,
import {
DragDropContext, Droppable, Draggable, DropResult, DraggingStyle, NotDraggingStyle,
} from 'react-beautiful-dnd'; } from 'react-beautiful-dnd';
import DragHandleIcon from '@mui/icons-material/DragHandle'; import DragHandleIcon from '@mui/icons-material/DragHandle';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
@@ -37,8 +34,11 @@ import FormControlLabel from '@mui/material/FormControlLabel';
import NavbarContext from 'components/context/NavbarContext'; import NavbarContext from 'components/context/NavbarContext';
import client, { useQuery } from 'util/client'; import client, { useQuery } from 'util/client';
const getItemStyle = (isDragging: boolean, const getItemStyle = (
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({ isDragging: boolean,
draggableStyle: DraggingStyle | NotDraggingStyle | undefined,
palette: Palette,
) => ({
// styles we need to apply on draggables // styles we need to apply on draggables
...draggableStyle, ...draggableStyle,
@@ -49,11 +49,14 @@ const getItemStyle = (isDragging: boolean,
export default function Categories() { export default function Categories() {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []); useEffect(() => {
setTitle('Categories');
setAction(<></>);
}, []);
const { data, mutate } = useQuery<ICategory[]>('/api/v1/category/'); const { data, mutate } = useQuery<ICategory[]>('/api/v1/category/');
const categories = useMemo(() => { const categories = useMemo(() => {
const res = [...data ?? []]; const res = [...(data ?? [])];
if (res.length > 0 && res[0].name === 'Default') { if (res.length > 0 && res[0].name === 'Default') {
res.shift(); res.shift();
} }
@@ -84,11 +87,7 @@ export default function Categories() {
return; return;
} }
categoryReorder( categoryReorder(categories, result.source.index, result.destination.index);
categories,
result.source.index,
result.destination.index,
);
}; };
const resetDialog = () => { const resetDialog = () => {
@@ -121,19 +120,16 @@ export default function Categories() {
formData.append('default', dialogDefault.toString()); formData.append('default', dialogDefault.toString());
if (categoryToEdit === -1) { if (categoryToEdit === -1) {
client.post('/api/v1/category/', formData) client.post('/api/v1/category/', formData).finally(() => mutate());
.finally(() => mutate());
} else { } else {
const category = categories[categoryToEdit]; const category = categories[categoryToEdit];
client.patch(`/api/v1/category/${category.id}`, formData) client.patch(`/api/v1/category/${category.id}`, formData).finally(() => mutate());
.finally(() => mutate());
} }
}; };
const deleteCategory = (index: number) => { const deleteCategory = (index: number) => {
const category = categories[index]; const category = categories[index];
client.delete(`/api/v1/category/${category.id}`) client.delete(`/api/v1/category/${category.id}`).finally(() => mutate());
.finally(() => mutate());
}; };
return ( return (
@@ -163,9 +159,7 @@ export default function Categories() {
<ListItemIcon> <ListItemIcon>
<DragHandleIcon /> <DragHandleIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText primary={item.name} />
primary={item.name}
/>
<IconButton <IconButton
onClick={() => { onClick={() => {
handleEditDialogOpen(index); handleEditDialogOpen(index);
@@ -219,13 +213,13 @@ export default function Categories() {
onChange={(e) => setDialogName(e.target.value)} onChange={(e) => setDialogName(e.target.value)}
/> />
<FormControlLabel <FormControlLabel
control={( control={
<Checkbox <Checkbox
checked={dialogDefault} checked={dialogDefault}
onChange={(e) => setDialogDefault(e.target.checked)} onChange={(e) => setDialogDefault(e.target.checked)}
color="default" color="default"
/> />
)} }
label="Default category when adding new manga to library" label="Default category when adding new manga to library"
/> />
</DialogContent> </DialogContent>

View File

@@ -31,13 +31,19 @@ export default function DefaultReaderSettings() {
const { metadata, settings, loading } = useDefaultReaderSettings(); const { metadata, settings, loading } = useDefaultReaderSettings();
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => { const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() => makeToast('Failed to save the default reader settings to the server', 'warning')); requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
makeToast('Failed to save the default reader settings to the server', 'warning'),
);
}; };
if (loading) { if (loading) {
return ( return (
<Box sx={{ <Box
height: '100vh', width: '100vw', display: 'grid', placeItems: 'center', sx={{
height: '100vh',
width: '100vw',
display: 'grid',
placeItems: 'center',
}} }}
> >
<CircularProgress thickness={5} /> <CircularProgress thickness={5} />
@@ -45,8 +51,11 @@ export default function DefaultReaderSettings() {
); );
} }
checkAndHandleMissingStoredReaderSettings({ meta: metadata }, 'server', getDefaultSettings()) checkAndHandleMissingStoredReaderSettings(
.catch(() => {}); { meta: metadata },
'server',
getDefaultSettings(),
).catch(() => {});
return ( return (
<ReaderSettingsOptions <ReaderSettingsOptions

View File

@@ -16,7 +16,6 @@ declare module '@mui/material/styles' {
interface PaletteOptions { interface PaletteOptions {
custom?: PaletteOptions['primary']; custom?: PaletteOptions['primary'];
} }
} }
const createTheme = (dark?: boolean) => { const createTheme = (dark?: boolean) => {
@@ -26,7 +25,8 @@ const createTheme = (dark?: boolean) => {
}, },
}); });
const tachideskTheme = createMuiTheme({ const tachideskTheme = createMuiTheme(
{
palette: { palette: {
custom: { custom: {
main: dark ? baseTheme.palette.common.black : baseTheme.palette.common.white, main: dark ? baseTheme.palette.common.black : baseTheme.palette.common.white,
@@ -48,7 +48,9 @@ const createTheme = (dark?: boolean) => {
`, `,
}, },
}, },
}, baseTheme); },
baseTheme,
);
return tachideskTheme; return tachideskTheme;
}; };

364
src/typings.d.ts vendored
View File

@@ -6,57 +6,57 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
interface IExtension { interface IExtension {
name: string name: string;
pkgName: string pkgName: string;
versionName: string versionName: string;
versionCode: number versionCode: number;
lang: string lang: string;
isNsfw: boolean isNsfw: boolean;
apkName: string apkName: string;
iconUrl: string iconUrl: string;
installed: boolean installed: boolean;
hasUpdate: boolean hasUpdate: boolean;
obsolete: boolean obsolete: boolean;
} }
interface ISource { interface ISource {
id: string id: string;
name: string name: string;
lang: string lang: string;
iconUrl: string iconUrl: string;
supportsLatest: boolean supportsLatest: boolean;
isConfigurable: boolean isConfigurable: boolean;
isNsfw: boolean isNsfw: boolean;
displayName: string displayName: string;
} }
interface ISourceFilters { interface ISourceFilters {
type: string type: string;
filter: ISourceFilter filter: ISourceFilter;
} }
interface ISourceFilter { interface ISourceFilter {
name: string name: string;
state: number | string | boolean | ISourceFilters[] | IState state: number | string | boolean | ISourceFilters[] | IState;
values?: string[] values?: string[];
displayValues?: string[] displayValues?: string[];
selected?: ISelected selected?: ISelected;
} }
interface ISelected { interface ISelected {
displayname: string displayname: string;
value: string value: string;
_value: string _value: string;
} }
interface IState { interface IState {
ascending: boolean ascending: boolean;
index: number index: number;
} }
interface IMetadataMigration { interface IMetadataMigration {
appKeyPrefix?: { oldPrefix: string, newPrefix: string } appKeyPrefix?: { oldPrefix: string; newPrefix: string };
keys?: { oldKey: string, newKey: string }[] keys?: { oldKey: string; newKey: string }[];
} }
interface IMetadata<VALUES extends AllowedMetadataValueTypes = string> { interface IMetadata<VALUES extends AllowedMetadataValueTypes = string> {
@@ -64,7 +64,7 @@ interface IMetadata<VALUES extends AllowedMetadataValueTypes = string> {
} }
interface IMetadataHolder<VALUES extends AllowedMetadataValueTypes = string> { interface IMetadataHolder<VALUES extends AllowedMetadataValueTypes = string> {
meta?: IMetadata<VALUES> meta?: IMetadata<VALUES>;
} }
type AllowedMetadataValueTypes = string | boolean | number | undefined; type AllowedMetadataValueTypes = string | boolean | number | undefined;
@@ -76,211 +76,211 @@ type AppMetadataKeys = MangaMetadataKeys;
type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes]; type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes];
interface IMangaCard { interface IMangaCard {
id: number id: number;
title: string title: string;
thumbnailUrl: string thumbnailUrl: string;
unreadCount?: number unreadCount?: number;
downloadCount?: number downloadCount?: number;
inLibrary?: boolean inLibrary?: boolean;
meta?: IMetadata meta?: IMetadata;
} }
interface IManga { interface IManga {
id: number id: number;
sourceId: string sourceId: string;
url: string url: string;
title: string title: string;
thumbnailUrl: string thumbnailUrl: string;
artist: string artist: string;
author: string author: string;
description: string description: string;
genre: string[] genre: string[];
status: string status: string;
inLibrary: boolean inLibrary: boolean;
source: ISource source: ISource;
meta: IMetadata meta: IMetadata;
realUrl: string realUrl: string;
freshData: boolean freshData: boolean;
unreadCount?: number unreadCount?: number;
downloadCount?: number downloadCount?: number;
age: number age: number;
chaptersAge: number chaptersAge: number;
} }
interface IChapter { interface IChapter {
id: number id: number;
url: string url: string;
name: string name: string;
uploadDate: number uploadDate: number;
chapterNumber: number chapterNumber: number;
scanlator: string scanlator: string;
mangaId: number mangaId: number;
read: boolean read: boolean;
bookmarked: boolean bookmarked: boolean;
lastPageRead: number lastPageRead: number;
lastReadAt: number lastReadAt: number;
index: number index: number;
fetchedAt: number fetchedAt: number;
chapterCount: number chapterCount: number;
pageCount: number pageCount: number;
downloaded: boolean downloaded: boolean;
meta: IAppMetadata meta: IAppMetadata;
} }
interface IMangaChapter { interface IMangaChapter {
manga: IManga manga: IManga;
chapter: IChapter chapter: IChapter;
} }
interface IPartialChapter { interface IPartialChapter {
pageCount: number pageCount: number;
index: number index: number;
chapterCount: number chapterCount: number;
lastPageRead: number lastPageRead: number;
} }
interface ICategory { interface ICategory {
id: number id: number;
order: number order: number;
name: string name: string;
default: boolean default: boolean;
meta: IAppMetadata meta: IAppMetadata;
} }
interface INavbarOverride { interface INavbarOverride {
status: boolean status: boolean;
value: any value: any;
} }
type ReaderType = type ReaderType =
'ContinuesVertical' | | 'ContinuesVertical'
'Webtoon' | | 'Webtoon'
'SingleVertical' | | 'SingleVertical'
'SingleRTL' | | 'SingleRTL'
'SingleLTR' | | 'SingleLTR'
'DoubleVertical' | | 'DoubleVertical'
'DoubleRTL' | | 'DoubleRTL'
'DoubleLTR' | | 'DoubleLTR'
'ContinuesHorizontalLTR' | | 'ContinuesHorizontalLTR'
'ContinuesHorizontalRTL'; | 'ContinuesHorizontalRTL';
interface IReaderSettings { interface IReaderSettings {
staticNav: boolean staticNav: boolean;
showPageNumber: boolean showPageNumber: boolean;
loadNextOnEnding: boolean loadNextOnEnding: boolean;
readerType: ReaderType readerType: ReaderType;
} }
interface IReaderPage { interface IReaderPage {
index: number index: number;
src: string src: string;
} }
interface IReaderProps { interface IReaderProps {
pages: Array<IReaderPage> pages: Array<IReaderPage>;
pageCount: number pageCount: number;
setCurPage: React.Dispatch<React.SetStateAction<number>> setCurPage: React.Dispatch<React.SetStateAction<number>>;
curPage: number curPage: number;
initialPage: number initialPage: number;
settings: IReaderSettings settings: IReaderSettings;
manga: IMangaCard | IManga manga: IMangaCard | IManga;
chapter: IChapter | IPartialChapter chapter: IChapter | IPartialChapter;
nextChapter: () => void nextChapter: () => void;
prevChapter: () => void prevChapter: () => void;
} }
interface IAbout { interface IAbout {
name: string name: string;
version: string version: string;
revision: string revision: string;
buildType: 'Stable' | 'Preview' buildType: 'Stable' | 'Preview';
buildTime: number buildTime: number;
github: string github: string;
discord: string discord: string;
} }
interface IDownloadChapter { interface IDownloadChapter {
chapterIndex: number chapterIndex: number;
mangaId: number mangaId: number;
state: 'Queued' | 'Downloading' | 'Finished' | 'Error' state: 'Queued' | 'Downloading' | 'Finished' | 'Error';
progress: number progress: number;
chapter: IChapter chapter: IChapter;
manga: IManga manga: IManga;
} }
interface IQueue { interface IQueue {
status: 'Stopped' | 'Started' status: 'Stopped' | 'Started';
queue: IDownloadChapter[] queue: IDownloadChapter[];
} }
interface IUpdateStatus { interface IUpdateStatus {
running: boolean running: boolean;
statusMap: { statusMap: {
COMPLETE?: IManga[], COMPLETE?: IManga[];
RUNNING?: IManga[], RUNNING?: IManga[];
PENDING?: IManga[] PENDING?: IManga[];
} };
} }
interface PreferenceProps { interface PreferenceProps {
key: string key: string;
title: string title: string;
summary: string summary: string;
defaultValue: any defaultValue: any;
currentValue: any currentValue: any;
defaultValueType: string defaultValueType: string;
// intetnal props // intetnal props
updateValue: any updateValue: any;
} }
interface TwoStatePreferenceProps extends PreferenceProps { interface TwoStatePreferenceProps extends PreferenceProps {
// intetnal props // intetnal props
type: 'Switch' | 'Checkbox' type: 'Switch' | 'Checkbox';
} }
interface CheckBoxPreferenceProps extends PreferenceProps {} interface CheckBoxPreferenceProps extends PreferenceProps {}
interface SwitchPreferenceCompatProps extends PreferenceProps {} interface SwitchPreferenceCompatProps extends PreferenceProps {}
interface ListPreferenceProps extends PreferenceProps { interface ListPreferenceProps extends PreferenceProps {
entries: string[] entries: string[];
entryValues: string[] entryValues: string[];
} }
interface MultiSelectListPreferenceProps extends PreferenceProps { interface MultiSelectListPreferenceProps extends PreferenceProps {
entries: string[] entries: string[];
entryValues: string[] entryValues: string[];
} }
interface EditTextPreferenceProps extends PreferenceProps { interface EditTextPreferenceProps extends PreferenceProps {
dialogTitle: string dialogTitle: string;
dialogMessage: string dialogMessage: string;
text: string text: string;
} }
interface SourcePreferences { interface SourcePreferences {
type: string type: string;
props: any props: any;
} }
interface NavbarItem { interface NavbarItem {
path: string, path: string;
title:string, title: string;
SelectedIconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>, SelectedIconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
IconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>, IconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
show: 'mobile' | 'desktop' | 'both' show: 'mobile' | 'desktop' | 'both';
} }
interface PaginatedList<T> { interface PaginatedList<T> {
page: T[], page: T[];
hasNextPage: boolean hasNextPage: boolean;
} }
type NullAndUndefined<T> = T | null | undefined; type NullAndUndefined<T> = T | null | undefined;
@@ -288,18 +288,18 @@ type NullAndUndefined<T> = T | null | undefined;
type ChapterSortMode = 'fetchedAt' | 'source'; type ChapterSortMode = 'fetchedAt' | 'source';
interface ChapterListOptions { interface ChapterListOptions {
active: boolean active: boolean;
unread: NullAndUndefined<boolean> unread: NullAndUndefined<boolean>;
downloaded: NullAndUndefined<boolean> downloaded: NullAndUndefined<boolean>;
bookmarked: NullAndUndefined<boolean> bookmarked: NullAndUndefined<boolean>;
reverse: boolean reverse: boolean;
sortBy: ChapterSortMode sortBy: ChapterSortMode;
showChapterNumber: boolean showChapterNumber: boolean;
} }
type ChapterOptionsReducerAction = type ChapterOptionsReducerAction =
{ type: 'filter', filterType:string, filterValue: NullAndUndefined<boolean> } | { type: 'filter'; filterType: string; filterValue: NullAndUndefined<boolean> }
| { type: 'sortBy', sortBy: ChapterSortMode } | { type: 'sortBy'; sortBy: ChapterSortMode }
| { type: 'sortReverse' } | { type: 'sortReverse' }
| { type: 'showChapterNumber' }; | { type: 'showChapterNumber' };
@@ -313,21 +313,21 @@ enum GridLayout {
interface LibraryOptions { interface LibraryOptions {
// display options // display options
showDownloadBadge: boolean showDownloadBadge: boolean;
showUnreadBadge: boolean showUnreadBadge: boolean;
gridLayout: GridLayout gridLayout: GridLayout;
SourcegridLayout: GridLayout SourcegridLayout: GridLayout;
// filter options // filter options
downloaded: NullAndUndefined<boolean> downloaded: NullAndUndefined<boolean>;
unread: NullAndUndefined<boolean> unread: NullAndUndefined<boolean>;
sorts: NullAndUndefined<LibrarySortMode> sorts: NullAndUndefined<LibrarySortMode>;
sortDesc: NullAndUndefined<boolean> sortDesc: NullAndUndefined<boolean>;
} }
interface BatchChaptersChange { interface BatchChaptersChange {
delete?: boolean delete?: boolean;
isRead?: boolean isRead?: boolean;
isBookmarked?: boolean isBookmarked?: boolean;
lastPageRead?: number lastPageRead?: number;
} }

View File

@@ -13,7 +13,11 @@ const { hostname, port, protocol } = window.location;
// if port is 3000 it's probably running from webpack devlopment server // if port is 3000 it's probably running from webpack devlopment server
let inferredPort; let inferredPort;
if (port === '3000') { inferredPort = '4567'; } else { inferredPort = port; } if (port === '3000') {
inferredPort = '4567';
} else {
inferredPort = port;
}
const baseURL = storage.getItem('serverBaseURL', `${protocol}//${hostname}:${inferredPort}`); const baseURL = storage.getItem('serverBaseURL', `${protocol}//${hostname}:${inferredPort}`);
@@ -42,10 +46,7 @@ export async function fetcher<T = any>(path: string) {
return res.data as T; return res.data as T;
} }
export const useQuery = < export const useQuery = <D extends any = any, E extends any = any>(
D extends any = any,
E extends any = any,
>(
key: string, key: string,
config?: SWRConfiguration<D, E>, config?: SWRConfiguration<D, E>,
): SWRResponse<D, E> & { loading: boolean } => { ): SWRResponse<D, E> & { loading: boolean } => {

View File

@@ -47,13 +47,10 @@ export const getUploadDateString = (date: Date | number) => {
const addTimeString = wasUploadedToday || wasUploadedYesterday; const addTimeString = wasUploadedToday || wasUploadedYesterday;
const timeString = addTimeString const timeString = addTimeString
? uploadDate.toLocaleTimeString( ? uploadDate.toLocaleTimeString(undefined, {
undefined,
{
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
}, })
)
: ''; : '';
if (wasUploadedToday) { if (wasUploadedToday) {

View File

@@ -74,7 +74,6 @@ export const ISOLanguages = [
{ code: 'bs', name: 'Bosnian', nativeName: 'bosanski' }, { code: 'bs', name: 'Bosnian', nativeName: 'bosanski' },
{ code: 'sv', name: 'Swedish', nativeName: 'svenska' }, { code: 'sv', name: 'Swedish', nativeName: 'svenska' },
{ code: 'sv', name: 'Swedish', nativeName: 'svenska' }, { code: 'sv', name: 'Swedish', nativeName: 'svenska' },
]; ];
export function langCodeToName(code: string): string { export function langCodeToName(code: string): string {
@@ -84,8 +83,10 @@ export function langCodeToName(code: string): string {
let result = `language with code: ${code}`; let result = `language with code: ${code}`;
for (let i = 0; i < ISOLanguages.length; i++) { for (let i = 0; i < ISOLanguages.length; i++) {
if (ISOLanguages[i].code === proccessedCode if (
|| ISOLanguages[i].code === code.toLocaleLowerCase()) { ISOLanguages[i].code === proccessedCode ||
ISOLanguages[i].code === code.toLocaleLowerCase()
) {
result = ISOLanguages[i].nativeName; result = ISOLanguages[i].nativeName;
} }
} }
@@ -98,23 +99,15 @@ function defaultNativeLang() {
} }
export function extensionDefaultLangs() { export function extensionDefaultLangs() {
return [ return [defaultNativeLang(), 'all'];
defaultNativeLang(),
'all',
];
} }
export function sourceDefualtLangs() { export function sourceDefualtLangs() {
return [ return [defaultNativeLang(), 'localsourcelang'];
defaultNativeLang(),
'localsourcelang',
];
} }
export function sourceForcedDefaultLangs(): string[] { export function sourceForcedDefaultLangs(): string[] {
return [ return ['localsourcelang'];
'localsourcelang',
];
} }
export const langSortCmp = (a: string, b: string) => { export const langSortCmp = (a: string, b: string) => {

View File

@@ -16,7 +16,8 @@ function getItem<T>(key: string, defaultValue: T) : T {
window.localStorage.setItem(key, JSON.stringify(defaultValue)); window.localStorage.setItem(key, JSON.stringify(defaultValue));
/* eslint-disable no-empty */ /* eslint-disable no-empty */
} finally { } } finally {
}
return defaultValue; return defaultValue;
} }
@@ -25,7 +26,8 @@ function setItem<T>(key: string, value: T): void {
window.localStorage.setItem(key, JSON.stringify(value)); window.localStorage.setItem(key, JSON.stringify(value));
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} finally { } } finally {
}
} }
export default { getItem, setItem }; export default { getItem, setItem };

View File

@@ -13,13 +13,12 @@ const APP_METADATA_KEY_PREFIX = 'webUI_';
const migrations: IMetadataMigration[] = [ const migrations: IMetadataMigration[] = [
{ {
keys: [ keys: [{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' }],
{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' },
],
}, },
]; ];
const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => `${appPrefix}${key}`; const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) =>
`${appPrefix}${key}`;
const doesMetadataKeyExistIn = ( const doesMetadataKeyExistIn = (
meta: IMetadata | undefined, meta: IMetadata | undefined,
@@ -27,9 +26,7 @@ const doesMetadataKeyExistIn = (
appPrefix?: string, appPrefix?: string,
): boolean => Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix)); ): boolean => Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix));
const convertValueFromMetadata = < const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes>(
T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes,
>(
value: string, value: string,
): T => { ): T => {
if (!Number.isNaN(+value)) { if (!Number.isNaN(+value)) {
@@ -74,8 +71,9 @@ const applyAppKeyPrefixMigration = (meta: IMetadata, migration: IMetadataMigrati
const oldAppMetadata = getAppMetadataFrom(meta, oldPrefix); const oldAppMetadata = getAppMetadataFrom(meta, oldPrefix);
const newAppMetadata = getAppMetadataFrom(meta, newPrefix); const newAppMetadata = getAppMetadataFrom(meta, newPrefix);
const missingMetadataKeys = Object.keys(oldAppMetadata) const missingMetadataKeys = Object.keys(oldAppMetadata).filter(
.filter((key) => !Object.keys(newAppMetadata).includes(key)); (key) => !Object.keys(newAppMetadata).includes(key),
);
const isMissingOldMetadata = missingMetadataKeys.length; const isMissingOldMetadata = missingMetadataKeys.length;
if (isMissingOldMetadata) { if (isMissingOldMetadata) {
@@ -219,16 +217,25 @@ export const requestUpdateMetadata = async (
keysToValues: [AppMetadataKeys, AllowedMetadataValueTypes][], keysToValues: [AppMetadataKeys, AllowedMetadataValueTypes][],
endpointToMutate?: string, endpointToMutate?: string,
wrapWithMetaKey?: boolean, wrapWithMetaKey?: boolean,
): Promise<void[]> => Promise.all(keysToValues.map( ): Promise<void[]> =>
([key, value]) => requestUpdateMetadataValue( Promise.all(
endpoint, metadataHolder, key, value, endpointToMutate, wrapWithMetaKey, keysToValues.map(([key, value]) =>
requestUpdateMetadataValue(
endpoint,
metadataHolder,
key,
value,
endpointToMutate,
wrapWithMetaKey,
), ),
)); ),
);
export const requestUpdateServerMetadata = async ( export const requestUpdateServerMetadata = async (
serverMetadata: IMetadata, serverMetadata: IMetadata,
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata('', { meta: serverMetadata }, keysToValues, '/meta', false); ): Promise<void[]> =>
requestUpdateMetadata('', { meta: serverMetadata }, keysToValues, '/meta', false);
export const requestUpdateMangaMetadata = async ( export const requestUpdateMangaMetadata = async (
manga: IMangaCard | IManga, manga: IMangaCard | IManga,
@@ -238,7 +245,12 @@ export const requestUpdateMangaMetadata = async (
export const requestUpdateChapterMetadata = async ( export const requestUpdateChapterMetadata = async (
mangaChapter: IMangaChapter, mangaChapter: IMangaChapter,
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(`/manga/${mangaChapter.manga.id}/chapter/${mangaChapter.chapter.index}`, mangaChapter.chapter, keysToValues); ): Promise<void[]> =>
requestUpdateMetadata(
`/manga/${mangaChapter.manga.id}/chapter/${mangaChapter.chapter.index}`,
mangaChapter.chapter,
keysToValues,
);
export const requestUpdateCategoryMetadata = async ( export const requestUpdateCategoryMetadata = async (
category: ICategory, category: ICategory,

View File

@@ -6,10 +6,15 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { getMetadataFrom, requestUpdateMangaMetadata, requestUpdateServerMetadata } from 'util/metadata'; import {
getMetadataFrom,
requestUpdateMangaMetadata,
requestUpdateServerMetadata,
} from 'util/metadata';
import { useQuery } from 'util/client'; import { useQuery } from 'util/client';
export const getDefaultSettings = (forceUndefined: boolean = false) => ({ export const getDefaultSettings = (forceUndefined: boolean = false) =>
({
staticNav: forceUndefined ? undefined : false, staticNav: forceUndefined ? undefined : false,
showPageNumber: forceUndefined ? undefined : true, showPageNumber: forceUndefined ? undefined : true,
continuesPageGap: forceUndefined ? undefined : false, continuesPageGap: forceUndefined ? undefined : false,
@@ -22,11 +27,11 @@ const getReaderSettingsWithDefaultValueFallback = (
defaultSettings?: IReaderSettings, defaultSettings?: IReaderSettings,
applyMetadataMigration: boolean = true, applyMetadataMigration: boolean = true,
): IReaderSettings => ({ ): IReaderSettings => ({
...getMetadataFrom( ...(getMetadataFrom(
{ meta }, { meta },
Object.entries(defaultSettings ?? getDefaultSettings()) as MetadataKeyValuePair[], Object.entries(defaultSettings ?? getDefaultSettings()) as MetadataKeyValuePair[],
applyMetadataMigration, applyMetadataMigration,
) as unknown as IReaderSettings, ) as unknown as IReaderSettings),
}); });
export const getReaderSettingsFromMetadata = ( export const getReaderSettingsFromMetadata = (
@@ -44,9 +49,9 @@ export const getReaderSettingsFor = (
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings, applyMetadataMigration); ): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings, applyMetadataMigration);
export const useDefaultReaderSettings = (): { export const useDefaultReaderSettings = (): {
metadata?: IMetadata, metadata?: IMetadata;
settings: IReaderSettings, settings: IReaderSettings;
loading: boolean loading: boolean;
} => { } => {
const { data: meta, loading } = useQuery<IMetadata>('/api/v1/meta'); const { data: meta, loading } = useQuery<IMetadata>('/api/v1/meta');
const settings = getReaderSettingsWithDefaultValueFallback(meta); const settings = getReaderSettingsWithDefaultValueFallback(meta);
@@ -66,12 +71,13 @@ export const checkAndHandleMissingStoredReaderSettings = async (
metadataHolderType: 'manga' | 'server', metadataHolderType: 'manga' | 'server',
defaultSettings: IReaderSettings, defaultSettings: IReaderSettings,
): Promise<void | void[]> => { ): Promise<void | void[]> => {
const meta = metadataHolder.meta ?? metadataHolder as IMetadata; const meta = metadataHolder.meta ?? (metadataHolder as IMetadata);
const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true), false); const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true), false);
const newSettings = getReaderSettingsFor({ meta }, defaultSettings); const newSettings = getReaderSettingsFor({ meta }, defaultSettings);
const undefinedSettings = Object.entries(settingsToCheck) const undefinedSettings = Object.entries(settingsToCheck).filter(
.filter((setting) => setting[1] === undefined); (setting) => setting[1] === undefined,
);
const settingsToUpdate: MetadataKeyValuePair[] = []; const settingsToUpdate: MetadataKeyValuePair[] = [];
undefinedSettings.forEach((setting) => { undefinedSettings.forEach((setting) => {

View File

@@ -10,7 +10,7 @@ import { useLocation } from 'react-router-dom';
export const BACK = '__BACK__'; export const BACK = '__BACK__';
const useBackTo = (): { url?: string, back: boolean } => { const useBackTo = (): { url?: string; back: boolean } => {
const location = useLocation<{ backLink?: string }>(); const location = useLocation<{ backLink?: string }>();
const { defaultBackTo } = useNavBarContext(); const { defaultBackTo } = useNavBarContext();

View File

@@ -5,14 +5,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { import React, { useState, Dispatch, SetStateAction, useReducer, Reducer, useCallback } from 'react';
useState,
Dispatch,
SetStateAction,
useReducer,
Reducer,
useCallback,
} from 'react';
import storage from 'util/localStorage'; import storage from 'util/localStorage';
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
@@ -21,19 +14,18 @@ export default function useLocalStorage<T>(
defaultValue: T | (() => T), defaultValue: T | (() => T),
): [T, Dispatch<SetStateAction<T>>] { ): [T, Dispatch<SetStateAction<T>>] {
const initialState = defaultValue instanceof Function ? defaultValue() : defaultValue; const initialState = defaultValue instanceof Function ? defaultValue() : defaultValue;
const [storedValue, setStoredValue] = useState<T>( const [storedValue, setStoredValue] = useState<T>(storage.getItem(key, initialState));
storage.getItem(key, initialState),
);
const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>( const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(
((value) => { (value) => {
setStoredValue((prevValue) => { setStoredValue((prevValue) => {
// Allow value to be a function so we have same API as useState // Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(prevValue) : value; const valueToStore = value instanceof Function ? value(prevValue) : value;
storage.setItem(key, valueToStore); storage.setItem(key, valueToStore);
return valueToStore; return valueToStore;
}); });
}), [key], },
[key],
); );
return [storedValue, setValue]; return [storedValue, setValue];

Some files were not shown because too many files have changed in this diff Show More