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

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

View File

@@ -65,22 +65,30 @@ const truncateText = (str: string, maxLength: number) => {
};
interface IProps {
manga: IMangaCard
gridLayout?: GridLayout
dimensions: number
inLibraryIndicator?: boolean
manga: IMangaCard;
gridLayout?: GridLayout;
dimensions: number;
inLibraryIndicator?: boolean;
}
const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
const {
manga: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
id, title, thumbnailUrl, downloadCount, unreadCount: unread, inLibrary,
id,
title,
thumbnailUrl,
downloadCount,
unreadCount: unread,
inLibrary,
},
gridLayout,
dimensions,
inLibraryIndicator,
} = props;
const { options: { showUnreadBadge, showDownloadBadge } } = useLibraryOptionsContext();
const {
options: { showUnreadBadge, showDownloadBadge },
} = useLibraryOptionsContext();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
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);
return (
<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
sx={{
display: 'flex',
@@ -113,7 +124,6 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
height: '100%',
}}
>
<BadgeContainer
sx={{
position: 'absolute',
@@ -128,17 +138,16 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
In library
</Typography>
)}
{ showUnreadBadge && unread! > 0 && (
<Typography
sx={{ backgroundColor: 'primary.dark' }}
>
{showUnreadBadge && unread! > 0 && (
<Typography sx={{ backgroundColor: 'primary.dark' }}>
{unread}
</Typography>
)}
{ showDownloadBadge && downloadCount! > 0 && (
<Typography sx={{
backgroundColor: 'success.dark',
}}
{showDownloadBadge && downloadCount! > 0 && (
<Typography
sx={{
backgroundColor: 'success.dark',
}}
>
{downloadCount}
</Typography>
@@ -147,30 +156,34 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
<SpinnerImage
alt={title}
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
imgStyle={inLibraryIndicator && inLibrary
? {
height: '100%',
width: '100%',
objectFit: 'cover',
filter: 'brightness(0.4)',
}
: {
height: '100%',
width: '100%',
objectFit: 'cover',
}}
imgStyle={
inLibraryIndicator && inLibrary
? {
height: '100%',
width: '100%',
objectFit: 'cover',
filter: 'brightness(0.4)',
}
: {
height: '100%',
width: '100%',
objectFit: 'cover',
}
}
spinnerStyle={{
display: 'grid',
placeItems: 'center',
}}
/>
{(gridLayout === GridLayout.Comfortable) ? (<></>) : (
{gridLayout === GridLayout.Comfortable ? (
<></>
) : (
<>
<BottomGradient />
<BottomGradientDoubledDown />
</>
)}
{(gridLayout === GridLayout.Comfortable) ? (
{gridLayout === GridLayout.Comfortable ? (
<></>
) : (
<MangaTitle
@@ -185,7 +198,7 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
)}
</CardActionArea>
</Card>
{(gridLayout === GridLayout.Comfortable) ? (
{gridLayout === GridLayout.Comfortable ? (
<MangaTitle
sx={{
position: 'relative',
@@ -195,7 +208,9 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
>
{truncateText(title, 61)}
</MangaTitle>
) : (<></>)}
) : (
<></>
)}
</Box>
</Link>
</Grid>
@@ -205,10 +220,7 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
return (
<Grid item xs={12}>
<Card>
<CardActionArea
component={Link}
to={mangaLinkTo}
>
<CardActionArea component={Link} to={mangaLinkTo}>
<CardContent
sx={{
display: 'flex',
@@ -220,22 +232,24 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
>
<Avatar
variant="rounded"
sx={inLibraryIndicator && inLibrary
? {
width: 56,
height: 56,
flex: '0 0 auto',
marginRight: 2,
imageRendering: 'pixelated',
filter: 'brightness(0.4)',
}
: {
width: 56,
height: 56,
flex: '0 0 auto',
marginRight: 2,
imageRendering: 'pixelated',
}}
sx={
inLibraryIndicator && inLibrary
? {
width: 56,
height: 56,
flex: '0 0 auto',
marginRight: 2,
imageRendering: 'pixelated',
filter: 'brightness(0.4)',
}
: {
width: 56,
height: 56,
flex: '0 0 auto',
marginRight: 2,
imageRendering: 'pixelated',
}
}
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
/>
<Box
@@ -252,23 +266,20 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
</Box>
<BadgeContainer>
{inLibraryIndicator && inLibrary && (
<Typography
sx={{ backgroundColor: 'primary.dark' }}
>
<Typography sx={{ backgroundColor: 'primary.dark' }}>
In library
</Typography>
)}
{ showUnreadBadge && unread! > 0 && (
<Typography
sx={{ backgroundColor: 'primary.dark' }}
>
{showUnreadBadge && unread! > 0 && (
<Typography sx={{ backgroundColor: 'primary.dark' }}>
{unread}
</Typography>
)}
{ showDownloadBadge && downloadCount! > 0 && (
<Typography sx={{
backgroundColor: 'success.dark',
}}
{showDownloadBadge && downloadCount! > 0 && (
<Typography
sx={{
backgroundColor: 'success.dark',
}}
>
{downloadCount}
</Typography>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,8 +24,10 @@ const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: IManga
}
};
const downloadedFilter = (downloaded: NullAndUndefined<boolean>,
{ downloadCount }: IMangaCard): boolean => {
const downloadedFilter = (
downloaded: NullAndUndefined<boolean>,
{ downloadCount }: IMangaCard,
): boolean => {
switch (downloaded) {
case true:
return !!downloadCount && downloadCount >= 1;
@@ -46,13 +48,14 @@ const filterManga = (
query: NullAndUndefined<string>,
unread: NullAndUndefined<boolean>,
downloaded: NullAndUndefined<boolean>,
): IMangaCard[] => manga.filter((m) => {
if (query) {
return queryFilter(query, m);
}
): IMangaCard[] =>
manga.filter((m) => {
if (query) {
return queryFilter(query, m);
}
return downloadedFilter(downloaded, m) && unreadFilter(unread, m);
});
return downloadedFilter(downloaded, m) && unreadFilter(unread, m);
});
const sortByUnread = (a: IMangaCard, b: IMangaCard): number =>
// eslint-disable-next-line implicit-arrow-linebreak
@@ -70,10 +73,17 @@ const sortManga = (
const result = [...manga];
switch (sort) {
case 'sortAlph': result.sort(sortByTitle); break;
case 'sortID': result.sort(sortById); break;
case 'sortToRead': result.sort(sortByUnread); break;
default: break;
case 'sortAlph':
result.sort(sortByTitle);
break;
case 'sortID':
result.sort(sortById);
break;
case 'sortToRead':
result.sort(sortByUnread);
break;
default:
break;
}
if (desc === true) {
@@ -84,20 +94,32 @@ const sortManga = (
};
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 { options } = useLibraryOptionsContext();
const { unread, downloaded } = options;
const sortedManga = useMemo(() => sortManga(mangas, options.sorts, options.sortDesc),
[mangas, lastLibraryUpdate, options.sorts, options.sortDesc]);
const sortedManga = useMemo(
() => sortManga(mangas, options.sorts, options.sortDesc),
[mangas, lastLibraryUpdate, options.sorts, options.sortDesc],
);
const filteredManga = useMemo(() => filterManga(sortedManga, query, unread, downloaded),
[sortedManga, lastLibraryUpdate, query, unread, downloaded]);
const filteredManga = useMemo(
() => filterManga(sortedManga, query, unread, downloaded),
[sortedManga, lastLibraryUpdate, query, unread, downloaded],
);
const showFilteredOutMessage = (unread != null || downloaded != null || query)
&& filteredManga.length === 0 && mangas.length > 0;
const showFilteredOutMessage =
(unread != null || downloaded != null || query) &&
filteredManga.length === 0 &&
mangas.length > 0;
return (
<MangaGrid

View File

@@ -27,8 +27,8 @@ const SORT_OPTIONS: [LibrarySortMode, string][] = [
];
interface IProps {
open: boolean,
onClose: () => void,
open: boolean;
onClose: () => void;
}
const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
@@ -51,8 +51,16 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
if (key === 'filter') {
return (
<>
<ThreeStateCheckboxInput label="Unread" checked={options.unread} onChange={(c) => handleFilterChange('unread', c)} />
<ThreeStateCheckboxInput label="Downloaded" checked={options.downloaded} onChange={(c) => handleFilterChange('downloaded', c)} />
<ThreeStateCheckboxInput
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}
checked={options.sorts === mode}
sortDescending={options.sortDesc}
onClick={() => (mode !== options.sorts
? handleFilterChange('sorts', mode)
: handleFilterChange('sortDesc', !options.sortDesc))}
onClick={() =>
mode !== options.sorts
? handleFilterChange('sorts', mode)
: handleFilterChange('sortDesc', !options.sortDesc)
}
/>
));
}
@@ -75,24 +85,44 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
<>
<FormLabel>Display mode</FormLabel>
<RadioGroup
onChange={(e) => handleFilterChange('gridLayout', Number(e.target.value))}
onChange={(e) =>
handleFilterChange('gridLayout', Number(e.target.value))
}
value={gridLayout}
>
<RadioInput label="Compact grid" 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} />
<RadioInput
label="Compact grid"
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>
<FormLabel sx={{ mt: 2 }}>Badges</FormLabel>
<CheckboxInput
label="Unread Badges"
checked={showUnreadBadge === true}
onChange={() => handleFilterChange('showUnreadBadge', !showUnreadBadge)}
onChange={() =>
handleFilterChange('showUnreadBadge', !showUnreadBadge)
}
/>
<CheckboxInput
label="Download Badges"
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
* 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 useLocalStorage from 'util/useLocalStorage';
@@ -14,7 +16,10 @@ interface IProps {
}
const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => {
const [options, setOptions] = useLocalStorage<LibraryOptions>('libraryOptions', DefaultLibraryOptions);
const [options, setOptions] = useLocalStorage<LibraryOptions>(
'libraryOptions',
DefaultLibraryOptions,
);
return (
<LibraryOptionsContext.Provider value={{ options, setOptions }}>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,102 +17,103 @@ import { mutate } from 'swr';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
const useStyles = (inLibrary: boolean) => makeStyles((theme: Theme) => ({
root: {
width: '100%',
[theme.breakpoints.up('md')]: {
position: 'sticky',
top: '64px',
left: '0px',
width: '50vw',
height: 'calc(100vh - 64px)',
alignSelf: 'flex-start',
overflowY: 'auto',
},
},
top: {
padding: '10px',
// [theme.breakpoints.up('md')]: {
// minWidth: '50%',
// },
},
leftRight: {
display: 'flex',
},
leftSide: {
'& img': {
borderRadius: 4,
maxWidth: '100%',
minWidth: '100%',
height: 'auto',
},
maxWidth: '50%',
// [theme.breakpoints.up('md')]: {
// minWidth: '100px',
// },
},
rightSide: {
marginLeft: 15,
maxWidth: '100%',
'& span': {
fontWeight: '400',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
buttons: {
display: 'flex',
justifyContent: 'space-around',
'& button': {
color: inLibrary ? '#2196f3' : 'inherit',
},
'& a': {
textDecoration: 'none',
color: '#858585',
'& button': {
color: 'inherit',
const useStyles = (inLibrary: boolean) =>
makeStyles((theme: Theme) => ({
root: {
width: '100%',
[theme.breakpoints.up('md')]: {
position: 'sticky',
top: '64px',
left: '0px',
width: '50vw',
height: 'calc(100vh - 64px)',
alignSelf: 'flex-start',
overflowY: 'auto',
},
},
},
bottom: {
paddingLeft: '10px',
paddingRight: '10px',
[theme.breakpoints.up('md')]: {
fontSize: '1.2em',
// maxWidth: '50%',
top: {
padding: '10px',
// [theme.breakpoints.up('md')]: {
// minWidth: '50%',
// },
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
leftRight: {
display: 'flex',
},
},
description: {
'& h4': {
marginTop: '1em',
marginBottom: 0,
leftSide: {
'& img': {
borderRadius: 4,
maxWidth: '100%',
minWidth: '100%',
height: 'auto',
},
maxWidth: '50%',
// [theme.breakpoints.up('md')]: {
// minWidth: '100px',
// },
},
'& p': {
textAlign: 'justify',
textJustify: 'inter-word',
rightSide: {
marginLeft: 15,
maxWidth: '100%',
'& span': {
fontWeight: '400',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
},
genre: {
display: 'flex',
flexWrap: 'wrap',
'& h5': {
border: '2px solid #2196f3',
borderRadius: '1.13em',
marginRight: '1em',
marginTop: 0,
marginBottom: '10px',
padding: '0.3em',
color: '#2196f3',
buttons: {
display: 'flex',
justifyContent: 'space-around',
'& button': {
color: inLibrary ? '#2196f3' : 'inherit',
},
'& a': {
textDecoration: 'none',
color: '#858585',
'& button': {
color: 'inherit',
},
},
},
},
}));
bottom: {
paddingLeft: '10px',
paddingRight: '10px',
[theme.breakpoints.up('md')]: {
fontSize: '1.2em',
// maxWidth: '50%',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
description: {
'& h4': {
marginTop: '1em',
marginBottom: 0,
},
'& p': {
textAlign: 'justify',
textJustify: 'inter-word',
},
},
genre: {
display: 'flex',
flexWrap: 'wrap',
'& h5': {
border: '2px solid #2196f3',
borderRadius: '1.13em',
marginRight: '1em',
marginTop: 0,
marginBottom: '10px',
padding: '0.3em',
color: '#2196f3',
},
},
}));
interface IProps{
manga: IManga
interface IProps {
manga: IManga;
}
function getSourceName(source: ISource) {
@@ -133,14 +134,24 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
const classes = useStyles(manga.inLibrary)();
const addToLibrary = () => {
mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`, { ...manga, inLibrary: true }, { revalidate: false });
client.get(`/api/v1/manga/${manga.id}/library/`)
mutate(
`/api/v1/manga/${manga.id}/?onlineFetch=false`,
{ ...manga, inLibrary: true },
{ revalidate: false },
);
client
.get(`/api/v1/manga/${manga.id}/library/`)
.then(() => mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`));
};
const removeFromLibrary = () => {
mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`, { ...manga, inLibrary: false }, { revalidate: false });
client.delete(`/api/v1/manga/${manga.id}/library/`)
mutate(
`/api/v1/manga/${manga.id}/?onlineFetch=false`,
{ ...manga, inLibrary: false },
{ revalidate: false },
);
client
.delete(`/api/v1/manga/${manga.id}/library/`)
.then(() => mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`));
};
@@ -149,12 +160,13 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
<div className={classes.top}>
<div className={classes.leftRight}>
<div className={classes.leftSide}>
<img src={`${serverAddress}${manga.thumbnailUrl}?useCache=${useCache}`} alt="Manga Thumbnail" />
<img
src={`${serverAddress}${manga.thumbnailUrl}?useCache=${useCache}`}
alt="Manga Thumbnail"
/>
</div>
<div className={classes.rightSide}>
<h1>
{manga.title}
</h1>
<h1>{manga.title}</h1>
<h3>
{'Author: '}
<span>{getValueOrUnknown(manga.author)}</span>
@@ -163,20 +175,21 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
{'Artist: '}
<span>{getValueOrUnknown(manga.artist)}</span>
</h3>
<h3>
{`Status: ${manga.status}`}
</h3>
<h3>
{`Source: ${getSourceName(manga.source)}`}
</h3>
<h3>{`Status: ${manga.status}`}</h3>
<h3>{`Source: ${getSourceName(manga.source)}`}</h3>
</div>
</div>
<div className={classes.buttons}>
<div>
<IconButton onClick={manga.inLibrary ? removeFromLibrary : addToLibrary} size="large">
{manga.inLibrary
? <FavoriteIcon sx={{ mr: 1 }} />
: <FavoriteBorderIcon sx={{ mr: 1 }} />}
<IconButton
onClick={manga.inLibrary ? removeFromLibrary : addToLibrary}
size="large"
>
{manga.inLibrary ? (
<FavoriteIcon sx={{ mr: 1 }} />
) : (
<FavoriteBorderIcon sx={{ mr: 1 }} />
)}
<Typography sx={{ fontSize: { xs: '0.75em', sm: '0.85em' } }}>
{manga.inLibrary ? 'In Library' : 'Add To Library'}
</Typography>
@@ -198,7 +211,9 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
<p>{manga.description}</p>
</div>
<div className={classes.genre}>
{manga.genre.map((g) => <h5 key={g}>{g}</h5>)}
{manga.genre.map((g) => (
<h5 key={g}>{g}</h5>
))}
</div>
</div>
</div>

View File

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

View File

@@ -11,23 +11,29 @@ import { Link } from 'react-router-dom';
import { PlayArrow } from '@mui/icons-material';
import { BACK } from 'util/useBackTo';
interface ResumeFABProps{
chapter: IChapter
mangaId: string
interface ResumeFABProps {
chapter: IChapter;
mangaId: string;
}
export default function ResumeFab(props: ResumeFABProps) {
const { chapter: { index, lastPageRead }, mangaId } = props;
const {
chapter: { index, lastPageRead },
mangaId,
} = props;
return (
<Fab
sx={{ position: 'fixed', bottom: '2em', right: '3em' }}
component={Link}
variant="extended"
color="primary"
to={{ pathname: `/manga/${mangaId}/chapter/${index}/page/${lastPageRead}`, state: { backLink: BACK } }}
to={{
pathname: `/manga/${mangaId}/chapter/${index}/page/${lastPageRead}`,
state: { backLink: BACK },
}}
>
<PlayArrow />
{index === 1 ? 'Start' : 'Resume' }
{index === 1 ? 'Start' : 'Resume'}
</Fab>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,11 +8,11 @@
import React, { useState } from 'react';
import NavBarContext from 'components/context/NavbarContext';
interface IProps{
children: React.ReactNode
interface IProps {
children: React.ReactNode;
}
export default function NavBarProvider({ children }:IProps) {
export default function NavBarProvider({ children }: IProps) {
const [defaultBackTo, setDefaultBackTo] = useState<string | undefined>();
const [title, setTitle] = useState<string>('Tachidesk');
const [action, setAction] = useState<any>(<div />);
@@ -36,9 +36,5 @@ export default function NavBarProvider({ children }:IProps) {
override,
setOverride,
};
return (
<NavBarContext.Provider value={value}>
{children}
</NavBarContext.Provider>
);
return <NavBarContext.Provider value={value}>{children}</NavBarContext.Provider>;
}

View File

@@ -104,28 +104,23 @@ const OpenDrawerButton = styled(IconButton)(({ theme }) => ({
}));
interface IProps {
settings: IReaderSettings
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void
manga: IManga | IMangaCard
chapter: IChapter
curPage: number
settings: IReaderSettings;
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
manga: IManga | IMangaCard;
chapter: IChapter;
curPage: number;
}
export default function ReaderNavBar(props: IProps) {
const history = useHistory();
const backTo = useBackTo();
const location = useLocation<{
prevDrawerOpen?: boolean,
prevSettingsCollapseOpen?: boolean
prevDrawerOpen?: boolean;
prevSettingsCollapseOpen?: boolean;
}>();
const {
prevDrawerOpen,
prevSettingsCollapseOpen,
} = location.state ?? {};
const { prevDrawerOpen, prevSettingsCollapseOpen } = location.state ?? {};
const {
settings, setSettingValue, manga, chapter, curPage,
} = props;
const { settings, setSettingValue, manga, chapter, curPage } = props;
const [drawerOpen, setDrawerOpen] = useState(settings.staticNav || prevDrawerOpen);
const [updateDrawerOnRender, setUpdateDrawerOnRender] = useState(true);
@@ -164,8 +159,8 @@ export default function ReaderNavBar(props: IProps) {
useEffect(() => {
window.addEventListener('scroll', handleScroll);
const rootEl:HTMLDivElement = document.querySelector('#root')!;
const mainContainer:HTMLDivElement = document.querySelector('#appMainContainer')!;
const rootEl: HTMLDivElement = document.querySelector('#root')!;
const mainContainer: HTMLDivElement = document.querySelector('#appMainContainer')!;
// main container and root div need to change styles...
rootEl.style.display = 'flex';
@@ -176,7 +171,7 @@ export default function ReaderNavBar(props: IProps) {
mainContainer.style.display = 'block';
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);// handleScroll changes on every render
}, [handleScroll]); // handleScroll changes on every render
const handleClose = () => {
if (backTo.back) history.goBack();
@@ -194,13 +189,13 @@ export default function ReaderNavBar(props: IProps) {
mountOnEnter
unmountOnExit
>
<Root sx={{
position: settings.staticNav ? 'sticky' : 'fixed',
}}
<Root
sx={{
position: settings.staticNav ? 'sticky' : 'fixed',
}}
>
<header>
{!settings.staticNav
&& (
{!settings.staticNav && (
<IconButton
edge="start"
color="inherit"
@@ -212,7 +207,12 @@ export default function ReaderNavBar(props: IProps) {
<KeyboardArrowLeftIcon />
</IconButton>
)}
<Typography variant="h1" textOverflow="ellipsis" overflow="hidden" sx={{ py: 1 }}>
<Typography
variant="h1"
textOverflow="ellipsis"
overflow="hidden"
sx={{ py: 1 }}
>
{chapter.name}
</Typography>
<IconButton
@@ -262,12 +262,9 @@ export default function ReaderNavBar(props: IProps) {
</Collapse>
<Divider sx={{ my: 1, mx: 2 }} />
<Navigation>
<span>
{`Currently on page ${curPage + 1} of ${chapter.pageCount}`}
</span>
<span>{`Currently on page ${curPage + 1} of ${chapter.pageCount}`}</span>
<ChapterNavigation>
{chapter.index > 1
&& (
{chapter.index > 1 && (
<Link
replace
to={{
@@ -287,8 +284,7 @@ export default function ReaderNavBar(props: IProps) {
</Button>
</Link>
)}
{chapter.index < chapter.chapterCount
&& (
{chapter.index < chapter.chapterCount && (
<Link
replace
style={{ gridArea: 'next' }}
@@ -300,10 +296,7 @@ export default function ReaderNavBar(props: IProps) {
},
}}
>
<Button
variant="outlined"
endIcon={<KeyboardArrowRightIcon />}
>
<Button variant="outlined" endIcon={<KeyboardArrowRightIcon />}>
Next Chapter
</Button>
</Link>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,27 +9,28 @@ import React from 'react';
import { Box } from '@mui/system';
interface IProps {
settings: IReaderSettings
curPage: number
pageCount: number
settings: IReaderSettings;
curPage: number;
pageCount: number;
}
export default function PageNumber(props: IProps) {
const { settings, curPage, pageCount } = props;
return (
<Box sx={{
display: settings.showPageNumber ? 'block' : 'none',
position: 'fixed',
bottom: '50px',
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
padding: '2px',
paddingLeft: '4px',
paddingRight: '4px',
textAlign: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
borderRadius: '10px',
}}
<Box
sx={{
display: settings.showPageNumber ? 'block' : 'none',
position: 'fixed',
bottom: '50px',
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
padding: '2px',
paddingLeft: '4px',
paddingRight: '4px',
textAlign: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
borderRadius: '10px',
}}
>
{`${curPage + 1} / ${pageCount}`}
</Box>

View File

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

View File

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

View File

@@ -30,9 +30,7 @@ const isAtEnd = () => {
const isAtStart = () => window.scrollX <= 0;
export default function HorizontalPager(props: IReaderProps) {
const {
pages, curPage, initialPage, settings, setCurPage, prevChapter, nextChapter,
} = props;
const { pages, curPage, initialPage, settings, setCurPage, prevChapter, nextChapter } = props;
const currentPageRef = useRef(initialPage);
const selfRef = useRef<HTMLDivElement>(null);
@@ -78,7 +76,7 @@ export default function HorizontalPager(props: IReaderProps) {
window.scrollBy(mouseXPos.current - e.pageX, 0);
}
function dragControl(e:MouseEvent) {
function dragControl(e: MouseEvent) {
mouseXPos.current = e.pageX;
selfRef.current?.addEventListener('mousemove', dragScreen);
}
@@ -87,7 +85,7 @@ export default function HorizontalPager(props: IReaderProps) {
selfRef.current?.removeEventListener('mousemove', dragScreen);
}
function clickControl(e:MouseEvent) {
function clickControl(e: MouseEvent) {
if (e.clientX >= window.innerWidth * 0.85) {
goRight();
} else if (e.clientX <= window.innerWidth * 0.15) {
@@ -166,8 +164,10 @@ export default function HorizontalPager(props: IReaderProps) {
ref={selfRef}
sx={{
display: 'flex',
flexDirection: (settings.readerType === 'ContinuesHorizontalLTR') ? 'row' : 'row-reverse',
justifyContent: (settings.readerType === 'ContinuesHorizontalLTR') ? 'flex-start' : 'flex-end',
flexDirection:
settings.readerType === 'ContinuesHorizontalLTR' ? 'row' : 'row-reverse',
justifyContent:
settings.readerType === 'ContinuesHorizontalLTR' ? 'flex-start' : 'flex-end',
margin: '0 auto',
width: 'auto',
height: 'auto',
@@ -175,18 +175,18 @@ export default function HorizontalPager(props: IReaderProps) {
userSelect: 'none',
}}
>
{
pages.map((page) => (
<Page
key={page.index}
index={page.index}
src={page.src}
onImageLoad={() => {}}
settings={settings}
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
/>
))
}
{pages.map((page) => (
<Page
key={page.index}
index={page.index}
src={page.src}
onImageLoad={() => {}}
settings={settings}
ref={(e: HTMLDivElement) => {
pagesRef.current[page.index] = e;
}}
/>
))}
</Box>
);
}

View File

@@ -10,9 +10,7 @@ import { Box } from '@mui/system';
import Page from 'components/reader/Page';
export default function PagedReader(props: IReaderProps) {
const {
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
} = props;
const { pages, settings, setCurPage, curPage, nextChapter, prevChapter } = props;
const selfRef = useRef<HTMLDivElement>(null);
@@ -53,7 +51,7 @@ export default function PagedReader(props: IReaderProps) {
}
}
function keyboardControl(e:KeyboardEvent) {
function keyboardControl(e: KeyboardEvent) {
switch (e.code) {
case 'Space':
e.preventDefault();
@@ -70,7 +68,7 @@ export default function PagedReader(props: IReaderProps) {
}
}
function clickControl(e:MouseEvent) {
function clickControl(e: MouseEvent) {
if (e.clientX > window.innerWidth / 2) {
goRight();
} else {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,9 +10,13 @@ import { Typography } from '@mui/material';
import React from 'react';
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;

View File

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

View File

@@ -9,9 +9,13 @@ import { Divider } from '@mui/material';
import React from 'react';
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;

View File

@@ -6,33 +6,23 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { ExpandLess, ExpandMore } from '@mui/icons-material';
import {
Collapse, ListItemButton, ListItemText, Stack,
} from '@mui/material';
import { Collapse, ListItemButton, ListItemText, Stack } from '@mui/material';
import { Box } from '@mui/system';
import SortRadioInput from 'components/atoms/SortRadioInput';
import React from 'react';
interface Props {
values: any
name: string
state: IState
position: number
group: number | undefined
updateFilterValue: Function
update: any
values: any;
name: string;
state: IState;
position: number;
group: number | undefined;
updateFilterValue: Function;
update: any;
}
const SortFilter: React.FC<Props> = (props: Props) => {
const {
values,
name,
state,
position,
group,
updateFilterValue,
update,
} = props;
const { values, name, state, position, group, updateFilterValue, update } = props;
const [val, setval] = React.useState(state);
const [open, setOpen] = React.useState(false);
@@ -51,9 +41,10 @@ const SortFilter: React.FC<Props> = (props: Props) => {
}
tmp.index = index;
setval(tmp);
const upd = update.filter((e: {
position: number; group: number | undefined;
}) => !(position === e.position && group === e.group));
const upd = update.filter(
(e: { position: number; group: number | undefined }) =>
!(position === e.position && group === e.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/.
*/
import SearchIcon from '@mui/icons-material/Search';
import {
FormControl, Input, InputAdornment, InputLabel,
} from '@mui/material';
import { FormControl, Input, InputAdornment, InputLabel } from '@mui/material';
import React from 'react';
interface Props {
state: string
name: string
position: number
group: number | undefined
updateFilterValue: Function
update: any
state: string;
name: string;
position: number;
group: number | undefined;
updateFilterValue: Function;
update: any;
}
const TextFilter: React.FC<Props> = (props) => {
const {
state,
name,
position,
group,
updateFilterValue,
update,
} = props;
const { state, name, position, group, updateFilterValue, update } = props;
const [Search, setsearch] = React.useState(state || '');
let typingTimer: NodeJS.Timeout;
function doneTyping(e: React.ChangeEvent<HTMLInputElement>) {
const upd = update.filter((el: {
position: number; group: number | undefined;
}) => !(position === el.position && group === el.group));
const upd = update.filter(
(el: { position: number; group: number | undefined }) =>
!(position === el.position && group === el.group),
);
updateFilterValue([...upd, { position, state: e.target.value, group }]);
}
@@ -43,29 +35,29 @@ const TextFilter: React.FC<Props> = (props) => {
setsearch(e.target.value);
clearTimeout(typingTimer);
typingTimer = setTimeout(() => { doneTyping(e); }, 2500);
typingTimer = setTimeout(() => {
doneTyping(e);
}, 2500);
}
if (state !== undefined) {
return (
<FormControl sx={{ my: 1 }} variant="standard">
<InputLabel>
{name}
</InputLabel>
<InputLabel>{name}</InputLabel>
<Input
name={name}
value={Search || ''}
onChange={handleChange}
endAdornment={(
endAdornment={
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
)}
}
/>
</FormControl>
);
}
return (<></>);
return <></>;
};
export default TextFilter;

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,14 +13,14 @@ import Switch from '@mui/material/Switch';
import Checkbox from '@mui/material/Checkbox';
function getTwoStateType(type: 'Checkbox' | 'Switch') {
if (type === 'Switch') { return Switch; }
if (type === 'Switch') {
return Switch;
}
return Checkbox;
}
function TwoSatePreference(props: TwoStatePreferenceProps) {
const {
title, summary, currentValue, updateValue, type,
} = props;
const { title, summary, currentValue, updateValue, type } = props;
const [internalCurrentValue, setInternalCurrentValue] = useState<boolean>(currentValue);
useEffect(() => {
@@ -31,17 +31,16 @@ function TwoSatePreference(props: TwoStatePreferenceProps) {
<ListItem>
<ListItemText primary={title} secondary={summary} />
<ListItemSecondaryAction>
{React.createElement(getTwoStateType(type),
{
edge: 'end',
checked: internalCurrentValue,
onChange: () => {
updateValue(!currentValue);
{React.createElement(getTwoStateType(type), {
edge: 'end',
checked: internalCurrentValue,
onChange: () => {
updateValue(!currentValue);
// appear smooth
setInternalCurrentValue(!currentValue);
},
})}
// appear smooth
setInternalCurrentValue(!currentValue);
},
})}
</ListItemSecondaryAction>
</ListItem>
);
@@ -50,6 +49,7 @@ function TwoSatePreference(props: TwoStatePreferenceProps) {
export function CheckBoxPreference(props: CheckBoxPreferenceProps) {
return <TwoSatePreference {...props} type="Checkbox" />;
}
export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) {
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';
interface IProps {
autoOpen?: boolean
autoOpen?: boolean;
}
const defaultProps = {
@@ -29,11 +29,14 @@ const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value === '' ? undefined : e.target.value);
}
const cancelSearch = () => {
setQuery(null);
setSearchOpen(false);
};
const handleBlur = () => { if (!query) setSearchOpen(false); };
const handleBlur = () => {
if (!query) setSearchOpen(false);
};
const openSearch = () => {
setSearchOpen(true);
// 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) => {
if ((e.code === 'F3') || (e.ctrlKey && e.code === 'KeyF')) {
if (e.code === 'F3' || (e.ctrlKey && e.code === 'KeyF')) {
e.preventDefault();
openSearch();
}
@@ -65,26 +68,23 @@ const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
return (
<>
{searchOpen
? (
<Input
value={query || ''}
onChange={handleChange}
onBlur={handleBlur}
inputRef={inputRef}
endAdornment={(
<IconButton
onClick={cancelSearch}
>
<CancelIcon />
</IconButton>
)}
/>
) : (
<IconButton onClick={openSearch}>
<SearchIcon />
</IconButton>
)}
{searchOpen ? (
<Input
value={query || ''}
onChange={handleChange}
onBlur={handleBlur}
inputRef={inputRef}
endAdornment={
<IconButton onClick={cancelSearch}>
<CancelIcon />
</IconButton>
}
/>
) : (
<IconButton onClick={openSearch}>
<SearchIcon />
</IconButton>
)}
</>
);
};

View File

@@ -1,7 +1,8 @@
import React from 'react';
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');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,14 +6,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// 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') {
return `${input}${count === 1 ? '' : 's'}`;
}
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;
return text.replaceAll('%count%', count.toString());
};