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:
21
.eslintrc.js
21
.eslintrc.js
@@ -1,27 +1,16 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: ['airbnb', 'airbnb-typescript', 'prettier'],
|
||||||
'airbnb',
|
plugins: ['@typescript-eslint', 'no-relative-import-paths', 'prettier'],
|
||||||
'airbnb-typescript'
|
|
||||||
],
|
|
||||||
plugins: ['@typescript-eslint', 'no-relative-import-paths'],
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: './tsconfig.json',
|
project: './tsconfig.json',
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// Indent with 4 spaces
|
'prettier/prettier': 'error',
|
||||||
'@typescript-eslint/indent': ['error', 4],
|
|
||||||
|
|
||||||
// Indent JSX with 4 spaces
|
|
||||||
'react/jsx-indent': ['error', 4],
|
|
||||||
|
|
||||||
// Indent props with 4 spaces
|
|
||||||
'react/jsx-indent-props': ['error', 4],
|
|
||||||
|
|
||||||
'no-plusplus': ['error', { 'allowForLoopAfterthoughts': true }],
|
|
||||||
|
|
||||||
|
'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||||
|
|
||||||
// just why
|
// just why
|
||||||
'react/jsx-no-bind' : 'off',
|
'react/jsx-no-bind': 'off',
|
||||||
'react/jsx-props-no-spreading': 'off',
|
'react/jsx-props-no-spreading': 'off',
|
||||||
'react/require-default-props': 'off',
|
'react/require-default-props': 'off',
|
||||||
|
|
||||||
|
|||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
@@ -58,11 +58,14 @@
|
|||||||
"eslint": "^7.2.0",
|
"eslint": "^7.2.0",
|
||||||
"eslint-config-airbnb": "18.2.1",
|
"eslint-config-airbnb": "18.2.1",
|
||||||
"eslint-config-airbnb-typescript": "^14.0.0",
|
"eslint-config-airbnb-typescript": "^14.0.0",
|
||||||
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint-plugin-import": "^2.22.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
"eslint-plugin-no-relative-import-paths": "^1.5.2",
|
"eslint-plugin-no-relative-import-paths": "^1.5.2",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.21.5",
|
"eslint-plugin-react": "^7.21.5",
|
||||||
"eslint-plugin-react-hooks": "^1.7.0",
|
"eslint-plugin-react-hooks": "^1.7.0",
|
||||||
|
"prettier": "^2.8.2",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/App.tsx
21
src/App.tsx
@@ -10,9 +10,7 @@ import CssBaseline from '@mui/material/CssBaseline';
|
|||||||
import AppContext from 'components/context/AppContext';
|
import AppContext from 'components/context/AppContext';
|
||||||
import DefaultNavBar from 'components/navbar/DefaultNavBar';
|
import DefaultNavBar from 'components/navbar/DefaultNavBar';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||||
Redirect, Route, Switch,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
import Browse from 'screens/Browse';
|
import Browse from 'screens/Browse';
|
||||||
import DownloadQueue from 'screens/DownloadQueue';
|
import DownloadQueue from 'screens/DownloadQueue';
|
||||||
import Extensions from 'screens/Extensions';
|
import Extensions from 'screens/Extensions';
|
||||||
@@ -48,13 +46,7 @@ const App: React.FC = () => (
|
|||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
{/* General Routes */}
|
{/* General Routes */}
|
||||||
<Route
|
<Route exact path="/" render={() => <Redirect to="/library" />} />
|
||||||
exact
|
|
||||||
path="/"
|
|
||||||
render={() => (
|
|
||||||
<Redirect to="/library" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route path="/settings/about">
|
<Route path="/settings/about">
|
||||||
<About />
|
<About />
|
||||||
</Route>
|
</Route>
|
||||||
@@ -116,14 +108,7 @@ const App: React.FC = () => (
|
|||||||
path="/manga/:mangaId/chapter/:chapterIndex"
|
path="/manga/:mangaId/chapter/:chapterIndex"
|
||||||
// passing a key re-mounts the reader
|
// passing a key re-mounts the reader
|
||||||
// when changing chapters
|
// when changing chapters
|
||||||
render={(props: any) => (
|
render={(props: any) => <Reader key={props.match.params.chapterIndex} />}
|
||||||
<Reader
|
|
||||||
key={
|
|
||||||
props.match.params
|
|
||||||
.chapterIndex
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</AppContext>
|
</AppContext>
|
||||||
|
|||||||
@@ -16,24 +16,34 @@ import useLocalStorage from 'util/useLocalStorage';
|
|||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
extension: IExtension
|
extension: IExtension;
|
||||||
notifyInstall: () => void
|
notifyInstall: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExtensionCard(props: IProps) {
|
export default function ExtensionCard(props: IProps) {
|
||||||
const {
|
const {
|
||||||
extension: {
|
extension: {
|
||||||
name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl, isNsfw,
|
name,
|
||||||
|
lang,
|
||||||
|
versionName,
|
||||||
|
installed,
|
||||||
|
hasUpdate,
|
||||||
|
obsolete,
|
||||||
|
pkgName,
|
||||||
|
iconUrl,
|
||||||
|
isNsfw,
|
||||||
},
|
},
|
||||||
notifyInstall,
|
notifyInstall,
|
||||||
} = props;
|
} = props;
|
||||||
const [installedState, setInstalledState] = useState<string>(
|
const [installedState, setInstalledState] = useState<string>(() => {
|
||||||
() => {
|
if (obsolete) {
|
||||||
if (obsolete) { return 'obsolete'; }
|
return 'obsolete';
|
||||||
if (hasUpdate) { return 'update'; }
|
}
|
||||||
return (installed ? 'uninstall' : 'install');
|
if (hasUpdate) {
|
||||||
},
|
return 'update';
|
||||||
);
|
}
|
||||||
|
return installed ? 'uninstall' : 'install';
|
||||||
|
});
|
||||||
|
|
||||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
||||||
@@ -42,29 +52,26 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
|
|
||||||
function install() {
|
function install() {
|
||||||
setInstalledState('installing');
|
setInstalledState('installing');
|
||||||
client.get(`/api/v1/extension/install/${pkgName}`)
|
client.get(`/api/v1/extension/install/${pkgName}`).then(() => {
|
||||||
.then(() => {
|
setInstalledState('uninstall');
|
||||||
setInstalledState('uninstall');
|
notifyInstall();
|
||||||
notifyInstall();
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
setInstalledState('updating');
|
setInstalledState('updating');
|
||||||
client.get(`/api/v1/extension/update/${pkgName}`)
|
client.get(`/api/v1/extension/update/${pkgName}`).then(() => {
|
||||||
.then(() => {
|
setInstalledState('uninstall');
|
||||||
setInstalledState('uninstall');
|
notifyInstall();
|
||||||
notifyInstall();
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstall() {
|
function uninstall() {
|
||||||
setInstalledState('uninstalling');
|
setInstalledState('uninstalling');
|
||||||
client.get(`/api/v1/extension/uninstall/${pkgName}`)
|
client.get(`/api/v1/extension/uninstall/${pkgName}`).then(() => {
|
||||||
.then(() => {
|
// setInstalledState('install');
|
||||||
// setInstalledState('install');
|
notifyInstall();
|
||||||
notifyInstall();
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleButtonClick() {
|
function handleButtonClick() {
|
||||||
@@ -89,12 +96,13 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{ margin: '10px' }}>
|
<Card sx={{ margin: '10px' }}>
|
||||||
<CardContent sx={{
|
<CardContent
|
||||||
display: 'flex',
|
sx={{
|
||||||
justifyContent: 'space-between',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
justifyContent: 'space-between',
|
||||||
p: 2,
|
alignItems: 'center',
|
||||||
}}
|
p: 2,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -113,11 +121,14 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
{name}
|
{name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" display="block" gutterBottom>
|
<Typography variant="caption" display="block" gutterBottom>
|
||||||
{langPress}
|
{langPress} {versionName}
|
||||||
{' '}
|
|
||||||
{versionName}
|
|
||||||
{isNsfw && (
|
{isNsfw && (
|
||||||
<Typography variant="caption" display="inline" gutterBottom color="red">
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
display="inline"
|
||||||
|
gutterBottom
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
{' 18+'}
|
{' 18+'}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -131,7 +142,6 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
onClick={() => handleButtonClick()}
|
onClick={() => handleButtonClick()}
|
||||||
>
|
>
|
||||||
{installedState}
|
{installedState}
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -65,22 +65,30 @@ const truncateText = (str: string, maxLength: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
manga: IMangaCard
|
manga: IMangaCard;
|
||||||
gridLayout?: GridLayout
|
gridLayout?: GridLayout;
|
||||||
dimensions: number
|
dimensions: number;
|
||||||
inLibraryIndicator?: boolean
|
inLibraryIndicator?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
|
const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
|
||||||
const {
|
const {
|
||||||
manga: {
|
manga: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
id, title, thumbnailUrl, downloadCount, unreadCount: unread, inLibrary,
|
id,
|
||||||
|
title,
|
||||||
|
thumbnailUrl,
|
||||||
|
downloadCount,
|
||||||
|
unreadCount: unread,
|
||||||
|
inLibrary,
|
||||||
},
|
},
|
||||||
gridLayout,
|
gridLayout,
|
||||||
dimensions,
|
dimensions,
|
||||||
inLibraryIndicator,
|
inLibraryIndicator,
|
||||||
} = props;
|
} = props;
|
||||||
const { options: { showUnreadBadge, showDownloadBadge } } = useLibraryOptionsContext();
|
const {
|
||||||
|
options: { showUnreadBadge, showDownloadBadge },
|
||||||
|
} = useLibraryOptionsContext();
|
||||||
|
|
||||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
||||||
@@ -92,7 +100,10 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
const cols = Math.ceil(dimensions / ItemWidth);
|
const cols = Math.ceil(dimensions / ItemWidth);
|
||||||
return (
|
return (
|
||||||
<Grid item columns={cols} xs={1}>
|
<Grid item columns={cols} xs={1}>
|
||||||
<Link to={mangaLinkTo} style={(gridLayout === GridLayout.Comfortable) ? { textDecoration: 'none' } : {}}>
|
<Link
|
||||||
|
to={mangaLinkTo}
|
||||||
|
style={gridLayout === GridLayout.Comfortable ? { textDecoration: 'none' } : {}}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -113,7 +124,6 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<BadgeContainer
|
<BadgeContainer
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -128,17 +138,16 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
In library
|
In library
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{ showUnreadBadge && unread! > 0 && (
|
{showUnreadBadge && unread! > 0 && (
|
||||||
<Typography
|
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
||||||
sx={{ backgroundColor: 'primary.dark' }}
|
|
||||||
>
|
|
||||||
{unread}
|
{unread}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{ showDownloadBadge && downloadCount! > 0 && (
|
{showDownloadBadge && downloadCount! > 0 && (
|
||||||
<Typography sx={{
|
<Typography
|
||||||
backgroundColor: 'success.dark',
|
sx={{
|
||||||
}}
|
backgroundColor: 'success.dark',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{downloadCount}
|
{downloadCount}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -147,30 +156,34 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
<SpinnerImage
|
<SpinnerImage
|
||||||
alt={title}
|
alt={title}
|
||||||
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
|
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
|
||||||
imgStyle={inLibraryIndicator && inLibrary
|
imgStyle={
|
||||||
? {
|
inLibraryIndicator && inLibrary
|
||||||
height: '100%',
|
? {
|
||||||
width: '100%',
|
height: '100%',
|
||||||
objectFit: 'cover',
|
width: '100%',
|
||||||
filter: 'brightness(0.4)',
|
objectFit: 'cover',
|
||||||
}
|
filter: 'brightness(0.4)',
|
||||||
: {
|
}
|
||||||
height: '100%',
|
: {
|
||||||
width: '100%',
|
height: '100%',
|
||||||
objectFit: 'cover',
|
width: '100%',
|
||||||
}}
|
objectFit: 'cover',
|
||||||
|
}
|
||||||
|
}
|
||||||
spinnerStyle={{
|
spinnerStyle={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
placeItems: 'center',
|
placeItems: 'center',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{(gridLayout === GridLayout.Comfortable) ? (<></>) : (
|
{gridLayout === GridLayout.Comfortable ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<BottomGradient />
|
<BottomGradient />
|
||||||
<BottomGradientDoubledDown />
|
<BottomGradientDoubledDown />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(gridLayout === GridLayout.Comfortable) ? (
|
{gridLayout === GridLayout.Comfortable ? (
|
||||||
<></>
|
<></>
|
||||||
) : (
|
) : (
|
||||||
<MangaTitle
|
<MangaTitle
|
||||||
@@ -185,7 +198,7 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
)}
|
)}
|
||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
</Card>
|
</Card>
|
||||||
{(gridLayout === GridLayout.Comfortable) ? (
|
{gridLayout === GridLayout.Comfortable ? (
|
||||||
<MangaTitle
|
<MangaTitle
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -195,7 +208,9 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
>
|
>
|
||||||
{truncateText(title, 61)}
|
{truncateText(title, 61)}
|
||||||
</MangaTitle>
|
</MangaTitle>
|
||||||
) : (<></>)}
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Link>
|
</Link>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -205,10 +220,7 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
return (
|
return (
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardActionArea
|
<CardActionArea component={Link} to={mangaLinkTo}>
|
||||||
component={Link}
|
|
||||||
to={mangaLinkTo}
|
|
||||||
>
|
|
||||||
<CardContent
|
<CardContent
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -220,22 +232,24 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
sx={inLibraryIndicator && inLibrary
|
sx={
|
||||||
? {
|
inLibraryIndicator && inLibrary
|
||||||
width: 56,
|
? {
|
||||||
height: 56,
|
width: 56,
|
||||||
flex: '0 0 auto',
|
height: 56,
|
||||||
marginRight: 2,
|
flex: '0 0 auto',
|
||||||
imageRendering: 'pixelated',
|
marginRight: 2,
|
||||||
filter: 'brightness(0.4)',
|
imageRendering: 'pixelated',
|
||||||
}
|
filter: 'brightness(0.4)',
|
||||||
: {
|
}
|
||||||
width: 56,
|
: {
|
||||||
height: 56,
|
width: 56,
|
||||||
flex: '0 0 auto',
|
height: 56,
|
||||||
marginRight: 2,
|
flex: '0 0 auto',
|
||||||
imageRendering: 'pixelated',
|
marginRight: 2,
|
||||||
}}
|
imageRendering: 'pixelated',
|
||||||
|
}
|
||||||
|
}
|
||||||
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
|
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box
|
||||||
@@ -252,23 +266,20 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
</Box>
|
</Box>
|
||||||
<BadgeContainer>
|
<BadgeContainer>
|
||||||
{inLibraryIndicator && inLibrary && (
|
{inLibraryIndicator && inLibrary && (
|
||||||
<Typography
|
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
||||||
sx={{ backgroundColor: 'primary.dark' }}
|
|
||||||
>
|
|
||||||
In library
|
In library
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{ showUnreadBadge && unread! > 0 && (
|
{showUnreadBadge && unread! > 0 && (
|
||||||
<Typography
|
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
||||||
sx={{ backgroundColor: 'primary.dark' }}
|
|
||||||
>
|
|
||||||
{unread}
|
{unread}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{ showDownloadBadge && downloadCount! > 0 && (
|
{showDownloadBadge && downloadCount! > 0 && (
|
||||||
<Typography sx={{
|
<Typography
|
||||||
backgroundColor: 'success.dark',
|
sx={{
|
||||||
}}
|
backgroundColor: 'success.dark',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{downloadCount}
|
{downloadCount}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, {
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
useEffect, useLayoutEffect, useRef, useState,
|
|
||||||
} from 'react';
|
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import EmptyView from 'components/util/EmptyView';
|
import EmptyView from 'components/util/EmptyView';
|
||||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||||
@@ -16,24 +14,32 @@ import { Box } from '@mui/system';
|
|||||||
import MangaCard from 'components/MangaCard';
|
import MangaCard from 'components/MangaCard';
|
||||||
import { GridLayout } from 'components/context/LibraryOptionsContext';
|
import { GridLayout } from 'components/context/LibraryOptionsContext';
|
||||||
|
|
||||||
export interface IMangaGridProps{
|
export interface IMangaGridProps {
|
||||||
mangas: IMangaCard[]
|
mangas: IMangaCard[];
|
||||||
isLoading: boolean
|
isLoading: boolean;
|
||||||
message?: string
|
message?: string;
|
||||||
messageExtra?: JSX.Element
|
messageExtra?: JSX.Element;
|
||||||
hasNextPage: boolean
|
hasNextPage: boolean;
|
||||||
lastPageNum: number
|
lastPageNum: number;
|
||||||
setLastPageNum: (lastPageNum: number) => void
|
setLastPageNum: (lastPageNum: number) => void;
|
||||||
gridLayout?: GridLayout
|
gridLayout?: GridLayout;
|
||||||
horisontal?: boolean | undefined
|
horisontal?: boolean | undefined;
|
||||||
noFaces?: boolean | undefined
|
noFaces?: boolean | undefined;
|
||||||
inLibraryIndicator?: boolean
|
inLibraryIndicator?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MangaGrid: React.FC<IMangaGridProps> = (props) => {
|
const MangaGrid: React.FC<IMangaGridProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
mangas, isLoading, message, messageExtra,
|
mangas,
|
||||||
hasNextPage, lastPageNum, setLastPageNum, gridLayout, horisontal, noFaces,
|
isLoading,
|
||||||
|
message,
|
||||||
|
messageExtra,
|
||||||
|
hasNextPage,
|
||||||
|
lastPageNum,
|
||||||
|
setLastPageNum,
|
||||||
|
gridLayout,
|
||||||
|
horisontal,
|
||||||
|
noFaces,
|
||||||
inLibraryIndicator,
|
inLibraryIndicator,
|
||||||
} = props;
|
} = props;
|
||||||
let mapped;
|
let mapped;
|
||||||
@@ -42,7 +48,7 @@ const MangaGrid: React.FC<IMangaGridProps> = (props) => {
|
|||||||
const scrollHandler = () => {
|
const scrollHandler = () => {
|
||||||
if (lastManga.current) {
|
if (lastManga.current) {
|
||||||
const rect = lastManga.current.getBoundingClientRect();
|
const rect = lastManga.current.getBoundingClientRect();
|
||||||
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) {
|
if ((rect.y + rect.height) / window.innerHeight < 2 && hasNextPage) {
|
||||||
setLastPageNum(lastPageNum + 1);
|
setLastPageNum(lastPageNum + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,9 +79,7 @@ const MangaGrid: React.FC<IMangaGridProps> = (props) => {
|
|||||||
|
|
||||||
if (mangas.length === 0) {
|
if (mangas.length === 0) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
mapped = (
|
mapped = <LoadingPlaceholder />;
|
||||||
<LoadingPlaceholder />
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
mapped = noFaces ? (
|
mapped = noFaces ? (
|
||||||
<Box
|
<Box
|
||||||
@@ -83,9 +87,7 @@ const MangaGrid: React.FC<IMangaGridProps> = (props) => {
|
|||||||
margin: 'auto',
|
margin: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h5">
|
<Typography variant="h5">{message}</Typography>
|
||||||
{message}
|
|
||||||
</Typography>
|
|
||||||
{messageExtra}
|
{messageExtra}
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
@@ -110,18 +112,22 @@ const MangaGrid: React.FC<IMangaGridProps> = (props) => {
|
|||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
spacing={1}
|
spacing={1}
|
||||||
style={horisontal ? {
|
style={
|
||||||
margin: 0,
|
horisontal
|
||||||
width: '100%',
|
? {
|
||||||
padding: '5px',
|
margin: 0,
|
||||||
overflowX: 'scroll',
|
width: '100%',
|
||||||
display: '-webkit-inline-box',
|
padding: '5px',
|
||||||
flexWrap: 'nowrap',
|
overflowX: 'scroll',
|
||||||
} : {
|
display: '-webkit-inline-box',
|
||||||
margin: 0,
|
flexWrap: 'nowrap',
|
||||||
width: '100%',
|
}
|
||||||
padding: '5px',
|
: {
|
||||||
}}
|
margin: 0,
|
||||||
|
width: '100%',
|
||||||
|
padding: '5px',
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{mapped}
|
{mapped}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -36,14 +36,12 @@ const WiderWidthButtons = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
source: ISource
|
source: ISource;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SourceCard: React.FC<IProps> = (props: IProps) => {
|
const SourceCard: React.FC<IProps> = (props: IProps) => {
|
||||||
const {
|
const {
|
||||||
source: {
|
source: { id, name, lang, iconUrl, supportsLatest, isNsfw },
|
||||||
id, name, lang, iconUrl, supportsLatest, isNsfw,
|
|
||||||
},
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -64,10 +62,7 @@ const SourceCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
margin: '10px',
|
margin: '10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardActionArea
|
<CardActionArea component={Link} to={`/sources/${id}/popular/`}>
|
||||||
component={Link}
|
|
||||||
to={`/sources/${id}/popular/`}
|
|
||||||
>
|
|
||||||
<CardContent
|
<CardContent
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -76,7 +71,6 @@ const SourceCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
padding: 2,
|
padding: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
@@ -89,7 +83,13 @@ const SourceCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
}}
|
}}
|
||||||
src={`${serverAddress}${iconUrl}?useCache=${useCache}`}
|
src={`${serverAddress}${iconUrl}?useCache=${useCache}`}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography variant="h5" component="h2">
|
<Typography variant="h5" component="h2">
|
||||||
{name}
|
{name}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -97,7 +97,12 @@ const SourceCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
<Typography variant="caption" display="block" gutterBottom>
|
<Typography variant="caption" display="block" gutterBottom>
|
||||||
{langCodeToName(lang)}
|
{langCodeToName(lang)}
|
||||||
{isNsfw && (
|
{isNsfw && (
|
||||||
<Typography variant="caption" display="inline" gutterBottom color="red">
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
display="inline"
|
||||||
|
gutterBottom
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
{' 18+'}
|
{' 18+'}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,19 +10,11 @@ import { Checkbox, CheckboxProps, FormControlLabel } from '@mui/material';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface IProps extends CheckboxProps {
|
interface IProps extends CheckboxProps {
|
||||||
label?: string
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckboxInput: React.FC<IProps> = ({
|
const CheckboxInput: React.FC<IProps> = ({ label, sx, ...rest }) => (
|
||||||
label, sx, ...rest
|
<FormControlLabel control={<Checkbox {...rest} />} label={label} sx={sx} />
|
||||||
}) => (
|
|
||||||
<FormControlLabel
|
|
||||||
control={(
|
|
||||||
<Checkbox {...rest} />
|
|
||||||
)}
|
|
||||||
label={label}
|
|
||||||
sx={sx}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default CheckboxInput;
|
export default CheckboxInput;
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import { CircularProgress, IconButton, IconButtonProps } from '@mui/material';
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
interface IProps extends Omit<IconButtonProps, 'onClick'> {
|
interface IProps extends Omit<IconButtonProps, 'onClick'> {
|
||||||
loading?: boolean
|
loading?: boolean;
|
||||||
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<any>
|
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoadingIconButton = ({
|
const LoadingIconButton = ({ onClick, children, loading: iLoading, ...rest }: IProps) => {
|
||||||
onClick, children, loading: iLoading, ...rest
|
|
||||||
}: IProps) => {
|
|
||||||
const [sLoading, setLoading] = useState(false);
|
const [sLoading, setLoading] = useState(false);
|
||||||
const loading = sLoading || iLoading;
|
const loading = sLoading || iLoading;
|
||||||
|
|
||||||
@@ -19,7 +17,7 @@ const LoadingIconButton = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton disabled={loading} {...rest} onClick={handleClick}>
|
<IconButton disabled={loading} {...rest} onClick={handleClick}>
|
||||||
{loading ? (<CircularProgress size={24} />) : children}
|
{loading ? <CircularProgress size={24} /> : children}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,19 +10,11 @@ import { FormControlLabel, Radio, RadioProps } from '@mui/material';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export interface RadioInputProps extends RadioProps {
|
export interface RadioInputProps extends RadioProps {
|
||||||
label?: string
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RadioInput: React.FC<RadioInputProps> = ({
|
const RadioInput: React.FC<RadioInputProps> = ({ label, sx, ...rest }) => (
|
||||||
label, sx, ...rest
|
<FormControlLabel control={<Radio {...rest} />} label={label} sx={sx} />
|
||||||
}) => (
|
|
||||||
<FormControlLabel
|
|
||||||
control={(
|
|
||||||
<Radio {...rest} />
|
|
||||||
)}
|
|
||||||
label={label}
|
|
||||||
sx={sx}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default RadioInput;
|
export default RadioInput;
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ import React from 'react';
|
|||||||
import RadioInput, { RadioInputProps } from 'components/atoms/RadioInput';
|
import RadioInput, { RadioInputProps } from 'components/atoms/RadioInput';
|
||||||
|
|
||||||
interface IProps extends RadioInputProps {
|
interface IProps extends RadioInputProps {
|
||||||
sortDescending?: boolean | null | undefined
|
sortDescending?: boolean | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortRadioInput: React.FC<IProps> = ({
|
const SortRadioInput: React.FC<IProps> = ({ sortDescending, ...rest }) => (
|
||||||
sortDescending, ...rest
|
|
||||||
}) => (
|
|
||||||
<RadioInput
|
<RadioInput
|
||||||
checkedIcon={sortDescending ? <ArrowDownward color="primary" /> : <ArrowUpward color="primary" />}
|
checkedIcon={
|
||||||
|
sortDescending ? <ArrowDownward color="primary" /> : <ArrowUpward color="primary" />
|
||||||
|
}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ function nextState(state: CheckState): CheckState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ThreeStateCheckboxProps extends Omit<CheckboxProps, 'checked' | 'onChange'> {
|
export interface ThreeStateCheckboxProps extends Omit<CheckboxProps, 'checked' | 'onChange'> {
|
||||||
checked?: boolean | undefined | null
|
checked?: boolean | undefined | null;
|
||||||
onChange?: (checked: boolean | undefined | null) => void
|
onChange?: (checked: boolean | undefined | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,19 +11,11 @@ import React from 'react';
|
|||||||
import ThreeStateCheckbox, { ThreeStateCheckboxProps } from 'components/atoms/ThreeStateCheckbox';
|
import ThreeStateCheckbox, { ThreeStateCheckboxProps } from 'components/atoms/ThreeStateCheckbox';
|
||||||
|
|
||||||
interface IProps extends ThreeStateCheckboxProps {
|
interface IProps extends ThreeStateCheckboxProps {
|
||||||
label?: string
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThreeStateCheckboxInput: React.FC<IProps> = ({
|
const ThreeStateCheckboxInput: React.FC<IProps> = ({ label, sx, ...rest }) => (
|
||||||
label, sx, ...rest
|
<FormControlLabel control={<ThreeStateCheckbox {...rest} />} label={label} sx={sx} />
|
||||||
}) => (
|
|
||||||
<FormControlLabel
|
|
||||||
control={(
|
|
||||||
<ThreeStateCheckbox {...rest} />
|
|
||||||
)}
|
|
||||||
label={label}
|
|
||||||
sx={sx}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ThreeStateCheckboxInput;
|
export default ThreeStateCheckboxInput;
|
||||||
|
|||||||
@@ -6,15 +6,11 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles';
|
||||||
StyledEngineProvider, ThemeProvider,
|
|
||||||
} from '@mui/material/styles';
|
|
||||||
import LibraryOptionsContextProvider from 'components/library/LibraryOptionsProvider';
|
import LibraryOptionsContextProvider from 'components/library/LibraryOptionsProvider';
|
||||||
import NavBarContextProvider from 'components/navbar/NavBarContextProvider';
|
import NavBarContextProvider from 'components/navbar/NavBarContextProvider';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {
|
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||||
BrowserRouter as Router, Route,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
import { SWRConfig } from 'swr';
|
import { SWRConfig } from 'swr';
|
||||||
import createTheme from 'theme';
|
import createTheme from 'theme';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
@@ -23,25 +19,22 @@ import useLocalStorage from 'util/useLocalStorage';
|
|||||||
import DarkTheme from 'components/context/DarkTheme';
|
import DarkTheme from 'components/context/DarkTheme';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppContext: React.FC<Props> = ({ children }) => {
|
const AppContext: React.FC<Props> = ({ children }) => {
|
||||||
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>(
|
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
|
||||||
'darkTheme',
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const darkThemeContext = useMemo(() => ({
|
const darkThemeContext = useMemo(
|
||||||
darkTheme,
|
() => ({
|
||||||
setDarkTheme,
|
darkTheme,
|
||||||
}), [darkTheme]);
|
setDarkTheme,
|
||||||
|
}),
|
||||||
const theme = useMemo(
|
|
||||||
() => createTheme(darkTheme),
|
|
||||||
[darkTheme],
|
[darkTheme],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const theme = useMemo(() => createTheme(darkTheme), [darkTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SWRConfig value={{ fetcher }}>
|
<SWRConfig value={{ fetcher }}>
|
||||||
<Router>
|
<Router>
|
||||||
@@ -50,9 +43,7 @@ const AppContext: React.FC<Props> = ({ children }) => {
|
|||||||
<DarkTheme.Provider value={darkThemeContext}>
|
<DarkTheme.Provider value={darkThemeContext}>
|
||||||
<QueryParamProvider ReactRouterRoute={Route}>
|
<QueryParamProvider ReactRouterRoute={Route}>
|
||||||
<LibraryOptionsContextProvider>
|
<LibraryOptionsContextProvider>
|
||||||
<NavBarContextProvider>
|
<NavBarContextProvider>{children}</NavBarContextProvider>
|
||||||
{children}
|
|
||||||
</NavBarContextProvider>
|
|
||||||
</LibraryOptionsContextProvider>
|
</LibraryOptionsContextProvider>
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</DarkTheme.Provider>
|
</DarkTheme.Provider>
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
darkTheme: boolean
|
darkTheme: boolean;
|
||||||
setDarkTheme: React.Dispatch<React.SetStateAction<boolean>>
|
setDarkTheme: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DarkTheme = React.createContext<ContextType>({
|
const DarkTheme = React.createContext<ContextType>({
|
||||||
darkTheme: true,
|
darkTheme: true,
|
||||||
setDarkTheme: ():void => {},
|
setDarkTheme: (): void => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default DarkTheme;
|
export default DarkTheme;
|
||||||
|
|||||||
@@ -9,31 +9,31 @@ import React, { useContext, useEffect } from 'react';
|
|||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
// Default back button url
|
// Default back button url
|
||||||
defaultBackTo: string | undefined
|
defaultBackTo: string | undefined;
|
||||||
setDefaultBackTo: React.Dispatch<React.SetStateAction<string | undefined>>
|
setDefaultBackTo: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
|
||||||
// AppBar title
|
// AppBar title
|
||||||
title: string
|
title: string;
|
||||||
setTitle: (title: string) => void
|
setTitle: (title: string) => void;
|
||||||
|
|
||||||
// AppBar action buttons
|
// AppBar action buttons
|
||||||
action: any
|
action: any;
|
||||||
setAction: React.Dispatch<React.SetStateAction<any>>
|
setAction: React.Dispatch<React.SetStateAction<any>>;
|
||||||
|
|
||||||
// Allow default navbar to be overrided
|
// Allow default navbar to be overrided
|
||||||
override: INavbarOverride
|
override: INavbarOverride;
|
||||||
setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>>
|
setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NavBarContext = React.createContext<ContextType>({
|
const NavBarContext = React.createContext<ContextType>({
|
||||||
defaultBackTo: undefined,
|
defaultBackTo: undefined,
|
||||||
setDefaultBackTo: ():void => {},
|
setDefaultBackTo: (): void => {},
|
||||||
title: 'Tachidesk',
|
title: 'Tachidesk',
|
||||||
setTitle: ():void => {},
|
setTitle: (): void => {},
|
||||||
action: <div />,
|
action: <div />,
|
||||||
setAction: ():void => {},
|
setAction: (): void => {},
|
||||||
override: { status: false, value: <div /> },
|
override: { status: false, value: <div /> },
|
||||||
setOverride: ():void => {},
|
setOverride: (): void => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default NavBarContext;
|
export default NavBarContext;
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: IManga
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadedFilter = (downloaded: NullAndUndefined<boolean>,
|
const downloadedFilter = (
|
||||||
{ downloadCount }: IMangaCard): boolean => {
|
downloaded: NullAndUndefined<boolean>,
|
||||||
|
{ downloadCount }: IMangaCard,
|
||||||
|
): boolean => {
|
||||||
switch (downloaded) {
|
switch (downloaded) {
|
||||||
case true:
|
case true:
|
||||||
return !!downloadCount && downloadCount >= 1;
|
return !!downloadCount && downloadCount >= 1;
|
||||||
@@ -46,13 +48,14 @@ const filterManga = (
|
|||||||
query: NullAndUndefined<string>,
|
query: NullAndUndefined<string>,
|
||||||
unread: NullAndUndefined<boolean>,
|
unread: NullAndUndefined<boolean>,
|
||||||
downloaded: NullAndUndefined<boolean>,
|
downloaded: NullAndUndefined<boolean>,
|
||||||
): IMangaCard[] => manga.filter((m) => {
|
): IMangaCard[] =>
|
||||||
if (query) {
|
manga.filter((m) => {
|
||||||
return queryFilter(query, 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 =>
|
const sortByUnread = (a: IMangaCard, b: IMangaCard): number =>
|
||||||
// eslint-disable-next-line implicit-arrow-linebreak
|
// eslint-disable-next-line implicit-arrow-linebreak
|
||||||
@@ -70,10 +73,17 @@ const sortManga = (
|
|||||||
const result = [...manga];
|
const result = [...manga];
|
||||||
|
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case 'sortAlph': result.sort(sortByTitle); break;
|
case 'sortAlph':
|
||||||
case 'sortID': result.sort(sortById); break;
|
result.sort(sortByTitle);
|
||||||
case 'sortToRead': result.sort(sortByUnread); break;
|
break;
|
||||||
default: break;
|
case 'sortID':
|
||||||
|
result.sort(sortById);
|
||||||
|
break;
|
||||||
|
case 'sortToRead':
|
||||||
|
result.sort(sortByUnread);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (desc === true) {
|
if (desc === true) {
|
||||||
@@ -84,20 +94,32 @@ const sortManga = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LibraryMangaGrid: React.FC<IMangaGridProps & { lastLibraryUpdate: number }> = ({
|
const LibraryMangaGrid: React.FC<IMangaGridProps & { lastLibraryUpdate: number }> = ({
|
||||||
mangas, isLoading, hasNextPage, lastPageNum, setLastPageNum, message, lastLibraryUpdate,
|
mangas,
|
||||||
|
isLoading,
|
||||||
|
hasNextPage,
|
||||||
|
lastPageNum,
|
||||||
|
setLastPageNum,
|
||||||
|
message,
|
||||||
|
lastLibraryUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
const [query] = useQueryParam('query', StringParam);
|
const [query] = useQueryParam('query', StringParam);
|
||||||
const { options } = useLibraryOptionsContext();
|
const { options } = useLibraryOptionsContext();
|
||||||
const { unread, downloaded } = options;
|
const { unread, downloaded } = options;
|
||||||
|
|
||||||
const sortedManga = useMemo(() => sortManga(mangas, options.sorts, options.sortDesc),
|
const sortedManga = useMemo(
|
||||||
[mangas, lastLibraryUpdate, options.sorts, options.sortDesc]);
|
() => sortManga(mangas, options.sorts, options.sortDesc),
|
||||||
|
[mangas, lastLibraryUpdate, options.sorts, options.sortDesc],
|
||||||
|
);
|
||||||
|
|
||||||
const filteredManga = useMemo(() => filterManga(sortedManga, query, unread, downloaded),
|
const filteredManga = useMemo(
|
||||||
[sortedManga, lastLibraryUpdate, query, unread, downloaded]);
|
() => filterManga(sortedManga, query, unread, downloaded),
|
||||||
|
[sortedManga, lastLibraryUpdate, query, unread, downloaded],
|
||||||
|
);
|
||||||
|
|
||||||
const showFilteredOutMessage = (unread != null || downloaded != null || query)
|
const showFilteredOutMessage =
|
||||||
&& filteredManga.length === 0 && mangas.length > 0;
|
(unread != null || downloaded != null || query) &&
|
||||||
|
filteredManga.length === 0 &&
|
||||||
|
mangas.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MangaGrid
|
<MangaGrid
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ const SORT_OPTIONS: [LibrarySortMode, string][] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
open: boolean,
|
open: boolean;
|
||||||
onClose: () => void,
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
||||||
@@ -51,8 +51,16 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
|||||||
if (key === 'filter') {
|
if (key === 'filter') {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ThreeStateCheckboxInput label="Unread" checked={options.unread} onChange={(c) => handleFilterChange('unread', c)} />
|
<ThreeStateCheckboxInput
|
||||||
<ThreeStateCheckboxInput label="Downloaded" checked={options.downloaded} onChange={(c) => handleFilterChange('downloaded', c)} />
|
label="Unread"
|
||||||
|
checked={options.unread}
|
||||||
|
onChange={(c) => handleFilterChange('unread', c)}
|
||||||
|
/>
|
||||||
|
<ThreeStateCheckboxInput
|
||||||
|
label="Downloaded"
|
||||||
|
checked={options.downloaded}
|
||||||
|
onChange={(c) => handleFilterChange('downloaded', c)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -63,9 +71,11 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
|||||||
label={label}
|
label={label}
|
||||||
checked={options.sorts === mode}
|
checked={options.sorts === mode}
|
||||||
sortDescending={options.sortDesc}
|
sortDescending={options.sortDesc}
|
||||||
onClick={() => (mode !== options.sorts
|
onClick={() =>
|
||||||
? handleFilterChange('sorts', mode)
|
mode !== options.sorts
|
||||||
: handleFilterChange('sortDesc', !options.sortDesc))}
|
? handleFilterChange('sorts', mode)
|
||||||
|
: handleFilterChange('sortDesc', !options.sortDesc)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -75,24 +85,44 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
|||||||
<>
|
<>
|
||||||
<FormLabel>Display mode</FormLabel>
|
<FormLabel>Display mode</FormLabel>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
onChange={(e) => handleFilterChange('gridLayout', Number(e.target.value))}
|
onChange={(e) =>
|
||||||
|
handleFilterChange('gridLayout', Number(e.target.value))
|
||||||
|
}
|
||||||
value={gridLayout}
|
value={gridLayout}
|
||||||
>
|
>
|
||||||
<RadioInput label="Compact grid" value={GridLayout.Compact} checked={gridLayout == null || gridLayout === GridLayout.Compact} />
|
<RadioInput
|
||||||
<RadioInput label="Comfortable grid" value={GridLayout.Comfortable} checked={gridLayout === GridLayout.Comfortable} />
|
label="Compact grid"
|
||||||
<RadioInput label="List" value={GridLayout.List} checked={gridLayout === GridLayout.List} />
|
value={GridLayout.Compact}
|
||||||
|
checked={
|
||||||
|
gridLayout == null || gridLayout === GridLayout.Compact
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<RadioInput
|
||||||
|
label="Comfortable grid"
|
||||||
|
value={GridLayout.Comfortable}
|
||||||
|
checked={gridLayout === GridLayout.Comfortable}
|
||||||
|
/>
|
||||||
|
<RadioInput
|
||||||
|
label="List"
|
||||||
|
value={GridLayout.List}
|
||||||
|
checked={gridLayout === GridLayout.List}
|
||||||
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
<FormLabel sx={{ mt: 2 }}>Badges</FormLabel>
|
<FormLabel sx={{ mt: 2 }}>Badges</FormLabel>
|
||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
label="Unread Badges"
|
label="Unread Badges"
|
||||||
checked={showUnreadBadge === true}
|
checked={showUnreadBadge === true}
|
||||||
onChange={() => handleFilterChange('showUnreadBadge', !showUnreadBadge)}
|
onChange={() =>
|
||||||
|
handleFilterChange('showUnreadBadge', !showUnreadBadge)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
label="Download Badges"
|
label="Download Badges"
|
||||||
checked={showDownloadBadge === true}
|
checked={showDownloadBadge === true}
|
||||||
onChange={() => handleFilterChange('showDownloadBadge', !showDownloadBadge)}
|
onChange={() =>
|
||||||
|
handleFilterChange('showDownloadBadge', !showDownloadBadge)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import LibraryOptionsContext, { DefaultLibraryOptions } from 'components/context/LibraryOptionsContext';
|
import LibraryOptionsContext, {
|
||||||
|
DefaultLibraryOptions,
|
||||||
|
} from 'components/context/LibraryOptionsContext';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
|
|
||||||
@@ -14,7 +16,10 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => {
|
const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => {
|
||||||
const [options, setOptions] = useLocalStorage<LibraryOptions>('libraryOptions', DefaultLibraryOptions);
|
const [options, setOptions] = useLocalStorage<LibraryOptions>(
|
||||||
|
'libraryOptions',
|
||||||
|
DefaultLibraryOptions,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LibraryOptionsContext.Provider value={{ options, setOptions }}>
|
<LibraryOptionsContext.Provider value={{ options, setOptions }}>
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ const LibraryToolbarMenu: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton onClick={() => setOpen(!open)} color={active ? 'warning' : 'default'}>
|
||||||
onClick={() => setOpen(!open)}
|
|
||||||
color={active ? 'warning' : 'default'}
|
|
||||||
>
|
|
||||||
<FilterList />
|
<FilterList />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<LibraryOptionsPanel open={open} onClose={() => setOpen(false)} />
|
<LibraryOptionsPanel open={open} onClose={() => setOpen(false)} />
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import client from 'util/client';
|
|||||||
import makeToast from 'components/util/Toast';
|
import makeToast from 'components/util/Toast';
|
||||||
|
|
||||||
interface IProgressProps {
|
interface IProgressProps {
|
||||||
progress: number
|
progress: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Progress({ progress }: IProgressProps) {
|
function Progress({ progress }: IProgressProps) {
|
||||||
@@ -16,19 +16,19 @@ function Progress({ progress }: IProgressProps) {
|
|||||||
<Box sx={{ display: 'grid', placeItems: 'center', position: 'relative' }}>
|
<Box sx={{ display: 'grid', placeItems: 'center', position: 'relative' }}>
|
||||||
<CircularProgress variant="determinate" value={progress} />
|
<CircularProgress variant="determinate" value={progress} />
|
||||||
<Box sx={{ position: 'absolute' }}>
|
<Box sx={{ position: 'absolute' }}>
|
||||||
<Typography fontSize="0.8rem">
|
<Typography fontSize="0.8rem">{`${Math.round(progress)}%`}</Typography>
|
||||||
{`${Math.round(progress)}%`}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
|
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace(
|
||||||
|
'http',
|
||||||
|
'ws',
|
||||||
|
);
|
||||||
|
|
||||||
interface IUpdateCheckerProps {
|
interface IUpdateCheckerProps {
|
||||||
handleFinishedUpdate: (time: number) => void
|
handleFinishedUpdate: (time: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
|
function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
|
||||||
@@ -58,9 +58,8 @@ function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
|
|||||||
const { running, statusMap } = JSON.parse(e.data) as IUpdateStatus;
|
const { running, statusMap } = JSON.parse(e.data) as IUpdateStatus;
|
||||||
const { COMPLETE = [], RUNNING = [], PENDING = [] } = statusMap;
|
const { COMPLETE = [], RUNNING = [], PENDING = [] } = statusMap;
|
||||||
|
|
||||||
const currentProgress = 100 * (
|
const currentProgress =
|
||||||
COMPLETE.length / (COMPLETE.length + RUNNING.length + PENDING.length)
|
100 * (COMPLETE.length / (COMPLETE.length + RUNNING.length + PENDING.length));
|
||||||
);
|
|
||||||
|
|
||||||
const isUpdateFinished = currentProgress === 100;
|
const isUpdateFinished = currentProgress === 100;
|
||||||
const ignoreFaultyMessage = !updateStarted && !running && isUpdateFinished;
|
const ignoreFaultyMessage = !updateStarted && !running && isUpdateFinished;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
|
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace(
|
||||||
|
'http',
|
||||||
|
'ws',
|
||||||
|
);
|
||||||
|
|
||||||
const useSubscription = <T>(path: string, callback?: (newValue: T) => boolean | void) => {
|
const useSubscription = <T>(path: string, callback?: (newValue: T) => boolean | void) => {
|
||||||
const [state, setState] = useState<T | undefined>();
|
const [state, setState] = useState<T | undefined>();
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ import DoneAll from '@mui/icons-material/DoneAll';
|
|||||||
import Download from '@mui/icons-material/Download';
|
import Download from '@mui/icons-material/Download';
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
import RemoveDone from '@mui/icons-material/RemoveDone';
|
import RemoveDone from '@mui/icons-material/RemoveDone';
|
||||||
import {
|
import { CardActionArea, Checkbox, ListItemIcon, ListItemText, Stack } from '@mui/material';
|
||||||
CardActionArea, Checkbox, ListItemIcon, ListItemText, Stack,
|
|
||||||
} from '@mui/material';
|
|
||||||
import Card from '@mui/material/Card';
|
import Card from '@mui/material/Card';
|
||||||
import CardContent from '@mui/material/CardContent';
|
import CardContent from '@mui/material/CardContent';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
@@ -33,19 +31,24 @@ import { BACK } from 'util/useBackTo';
|
|||||||
import { getUploadDateString } from 'util/date';
|
import { getUploadDateString } from 'util/date';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
chapter: IChapter
|
chapter: IChapter;
|
||||||
triggerChaptersUpdate: () => void
|
triggerChaptersUpdate: () => void;
|
||||||
downloadChapter: IDownloadChapter | undefined
|
downloadChapter: IDownloadChapter | undefined;
|
||||||
showChapterNumber: boolean
|
showChapterNumber: boolean;
|
||||||
onSelect: (selected: boolean) => void
|
onSelect: (selected: boolean) => void;
|
||||||
selected: boolean | null
|
selected: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
chapter, triggerChaptersUpdate, downloadChapter: dc, showChapterNumber, onSelect, selected,
|
chapter,
|
||||||
|
triggerChaptersUpdate,
|
||||||
|
downloadChapter: dc,
|
||||||
|
showChapterNumber,
|
||||||
|
onSelect,
|
||||||
|
selected,
|
||||||
} = props;
|
} = props;
|
||||||
const isSelecting = selected !== null;
|
const isSelecting = selected !== null;
|
||||||
|
|
||||||
@@ -71,7 +74,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
if (key === 'read') {
|
if (key === 'read') {
|
||||||
formData.append('lastPageRead', '1');
|
formData.append('lastPageRead', '1');
|
||||||
}
|
}
|
||||||
client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData)
|
client
|
||||||
|
.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData)
|
||||||
.then(() => triggerChaptersUpdate());
|
.then(() => triggerChaptersUpdate());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,7 +85,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteChapter = () => {
|
const deleteChapter = () => {
|
||||||
client.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`)
|
client
|
||||||
|
.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`)
|
||||||
.then(() => triggerChaptersUpdate());
|
.then(() => triggerChaptersUpdate());
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
@@ -112,7 +117,10 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
>
|
>
|
||||||
<CardActionArea
|
<CardActionArea
|
||||||
component={Link}
|
component={Link}
|
||||||
to={{ pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`, state: { backLink: BACK } }}
|
to={{
|
||||||
|
pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`,
|
||||||
|
state: { backLink: BACK },
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
color: theme.palette.text[chapter.read ? 'disabled' : 'primary'],
|
color: theme.palette.text[chapter.read ? 'disabled' : 'primary'],
|
||||||
}}
|
}}
|
||||||
@@ -130,13 +138,16 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
<Stack direction="column" flex={1}>
|
<Stack direction="column" flex={1}>
|
||||||
<Typography variant="h5" component="h2">
|
<Typography variant="h5" component="h2">
|
||||||
{chapter.bookmarked && (
|
{chapter.bookmarked && (
|
||||||
<BookmarkIcon color="primary" sx={{ mr: 0.5, position: 'relative', top: '0.15em' }} />
|
<BookmarkIcon
|
||||||
|
color="primary"
|
||||||
|
sx={{ mr: 0.5, position: 'relative', top: '0.15em' }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{showChapterNumber ? `Chapter ${chapter.chapterNumber}` : chapter.name}
|
{showChapterNumber
|
||||||
</Typography>
|
? `Chapter ${chapter.chapterNumber}`
|
||||||
<Typography variant="caption">
|
: chapter.name}
|
||||||
{chapter.scanlator}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography variant="caption">{chapter.scanlator}</Typography>
|
||||||
<Typography variant="caption">
|
<Typography variant="caption">
|
||||||
{getUploadDateString(chapter.uploadDate)}
|
{getUploadDateString(chapter.uploadDate)}
|
||||||
{isDownloaded && ' • Downloaded'}
|
{isDownloaded && ' • Downloaded'}
|
||||||
@@ -164,18 +175,14 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<CheckBoxOutlineBlank fontSize="small" />
|
<CheckBoxOutlineBlank fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>Select</ListItemText>
|
||||||
Select
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{isDownloaded && (
|
{isDownloaded && (
|
||||||
<MenuItem onClick={deleteChapter}>
|
<MenuItem onClick={deleteChapter}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Delete fontSize="small" />
|
<Delete fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>Delete</ListItemText>
|
||||||
Delete
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{canBeDownloaded && (
|
{canBeDownloaded && (
|
||||||
@@ -183,9 +190,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Download fontSize="small" />
|
<Download fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>Download</ListItemText>
|
||||||
Download
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
||||||
@@ -212,9 +217,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DoneAll fontSize="small" />
|
<DoneAll fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>Mark previous as Read</ListItemText>
|
||||||
Mark previous as Read
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import {
|
import { Button, CircularProgress, Stack } from '@mui/material';
|
||||||
Button, CircularProgress, Stack,
|
|
||||||
} from '@mui/material';
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { styled } from '@mui/system';
|
import { styled } from '@mui/system';
|
||||||
import useSubscription from 'components/library/useSubscription';
|
import useSubscription from 'components/library/useSubscription';
|
||||||
@@ -17,10 +15,7 @@ import { filterAndSortChapters, useChapterOptions } from 'components/manga/util'
|
|||||||
import EmptyView from 'components/util/EmptyView';
|
import EmptyView from 'components/util/EmptyView';
|
||||||
import { interpolate } from 'components/util/helpers';
|
import { interpolate } from 'components/util/helpers';
|
||||||
import makeToast from 'components/util/Toast';
|
import makeToast from 'components/util/Toast';
|
||||||
import React, {
|
import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
ComponentProps,
|
|
||||||
useEffect, useMemo, useRef, useState,
|
|
||||||
} from 'react';
|
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import client, { useQuery } from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
import ChaptersToolbarMenu from 'components/manga/ChaptersToolbarMenu';
|
import ChaptersToolbarMenu from 'components/manga/ChaptersToolbarMenu';
|
||||||
@@ -66,13 +61,13 @@ const actionsStrings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface IChapterWithMeta {
|
export interface IChapterWithMeta {
|
||||||
chapter: IChapter
|
chapter: IChapter;
|
||||||
downloadChapter: IDownloadChapter | undefined
|
downloadChapter: IDownloadChapter | undefined;
|
||||||
selected: boolean | null
|
selected: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mangaId: string
|
mangaId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||||
@@ -92,9 +87,9 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
if (prevQueueRef.current && queue) {
|
if (prevQueueRef.current && queue) {
|
||||||
const prevQueue = prevQueueRef.current;
|
const prevQueue = prevQueueRef.current;
|
||||||
const changedDownloads = queue.filter((cd) => {
|
const changedDownloads = queue.filter((cd) => {
|
||||||
const prevChapterDownload = prevQueue
|
const prevChapterDownload = prevQueue.find(
|
||||||
.find((pcd) => cd.chapterIndex === pcd.chapterIndex
|
(pcd) => cd.chapterIndex === pcd.chapterIndex && cd.mangaId === pcd.mangaId,
|
||||||
&& cd.mangaId === pcd.mangaId);
|
);
|
||||||
if (!prevChapterDownload) return true;
|
if (!prevChapterDownload) return true;
|
||||||
return cd.state !== prevChapterDownload.state;
|
return cd.state !== prevChapterDownload.state;
|
||||||
});
|
});
|
||||||
@@ -107,13 +102,19 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
prevQueueRef.current = queue;
|
prevQueueRef.current = queue;
|
||||||
}, [queue]);
|
}, [queue]);
|
||||||
|
|
||||||
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), //
|
const visibleChapters = useMemo(
|
||||||
[chapters, options]);
|
() => filterAndSortChapters(chapters, options), //
|
||||||
|
[chapters, options],
|
||||||
|
);
|
||||||
|
|
||||||
const firstUnreadChapter = useMemo(() => visibleChapters.slice()
|
const firstUnreadChapter = useMemo(
|
||||||
.reverse()
|
() =>
|
||||||
.find((c) => c.read === false),
|
visibleChapters
|
||||||
[visibleChapters]);
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.find((c) => c.read === false),
|
||||||
|
[visibleChapters],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelection = (index: number) => {
|
const handleSelection = (index: number) => {
|
||||||
const chapter = visibleChapters[index];
|
const chapter = visibleChapters[index];
|
||||||
@@ -139,7 +140,10 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
setSelection(null);
|
setSelection(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFabAction: ComponentProps<typeof SelectionFAB>['onAction'] = (action, actionChapters) => {
|
const handleFabAction: ComponentProps<typeof SelectionFAB>['onAction'] = (
|
||||||
|
action,
|
||||||
|
actionChapters,
|
||||||
|
) => {
|
||||||
if (actionChapters.length === 0) return;
|
if (actionChapters.length === 0) return;
|
||||||
const chapterIds = actionChapters.map(({ chapter }) => chapter.id);
|
const chapterIds = actionChapters.map(({ chapter }) => chapter.id);
|
||||||
|
|
||||||
@@ -160,18 +164,26 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actionPromise
|
actionPromise
|
||||||
.then(() => makeToast(interpolate(chapterIds.length, actionsStrings[action].success), 'success'))
|
.then(() =>
|
||||||
|
makeToast(
|
||||||
|
interpolate(chapterIds.length, actionsStrings[action].success),
|
||||||
|
'success',
|
||||||
|
),
|
||||||
|
)
|
||||||
.then(() => mutate())
|
.then(() => mutate())
|
||||||
.catch(() => makeToast(interpolate(chapterIds.length, actionsStrings[action].error), 'error'));
|
.catch(() =>
|
||||||
|
makeToast(interpolate(chapterIds.length, actionsStrings[action].error), 'error'),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
margin: '10px auto',
|
style={{
|
||||||
display: 'flex',
|
margin: '10px auto',
|
||||||
justifyContent: 'center',
|
display: 'flex',
|
||||||
}}
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress thickness={5} />
|
<CircularProgress thickness={5} />
|
||||||
</div>
|
</div>
|
||||||
@@ -183,8 +195,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
|
|
||||||
const chaptersWithMeta: IChapterWithMeta[] = visibleChapters.map((chapter) => {
|
const chaptersWithMeta: IChapterWithMeta[] = visibleChapters.map((chapter) => {
|
||||||
const downloadChapter = queue?.find(
|
const downloadChapter = queue?.find(
|
||||||
(cd) => cd.chapterIndex === chapter.index
|
(cd) => cd.chapterIndex === chapter.index && cd.mangaId === chapter.mangaId,
|
||||||
&& cd.mangaId === chapter.mangaId,
|
|
||||||
);
|
);
|
||||||
const selected = selection?.includes(chapter.id) ?? null;
|
const selected = selection?.includes(chapter.id) ?? null;
|
||||||
return {
|
return {
|
||||||
@@ -194,9 +205,10 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedChapters = (selection === null)
|
const selectedChapters =
|
||||||
? null
|
selection === null
|
||||||
: chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id));
|
? null
|
||||||
|
: chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -206,38 +218,44 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
sx={{
|
sx={{
|
||||||
m: 1, mb: 0, mr: 2, minHeight: 40,
|
m: 1,
|
||||||
|
mb: 0,
|
||||||
|
mr: 2,
|
||||||
|
minHeight: 40,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h5">
|
<Typography variant="h5">
|
||||||
{`${visibleChapters.length} Chapter${visibleChapters.length === 1 ? '' : 's'}`}
|
{`${visibleChapters.length} Chapter${
|
||||||
|
visibleChapters.length === 1 ? '' : 's'
|
||||||
|
}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{selection === null ? (
|
{selection === null ? (
|
||||||
<ChaptersToolbarMenu options={options} optionsDispatch={dispatch} />
|
<ChaptersToolbarMenu options={options} optionsDispatch={dispatch} />
|
||||||
) : (
|
) : (
|
||||||
<Stack direction="row">
|
<Stack direction="row">
|
||||||
<Button size="small" onClick={handleSelectAll}>Select all</Button>
|
<Button size="small" onClick={handleSelectAll}>
|
||||||
<Button size="small" onClick={handleClear}>Clear</Button>
|
Select all
|
||||||
|
</Button>
|
||||||
|
<Button size="small" onClick={handleClear}>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{noChaptersFound && (
|
{noChaptersFound && <EmptyView message="No chapters found" />}
|
||||||
<EmptyView message="No chapters found" />
|
{noChaptersMatchingFilter && <EmptyView message="No chapters matching filter" />}
|
||||||
)}
|
|
||||||
{noChaptersMatchingFilter && (
|
|
||||||
<EmptyView message="No chapters matching filter" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<StyledVirtuoso
|
<StyledVirtuoso
|
||||||
style={{ // override Virtuoso default values and set them with class
|
style={{
|
||||||
|
// override Virtuoso default values and set them with class
|
||||||
height: 'undefined',
|
height: 'undefined',
|
||||||
// 900 is the md breakpoint in MUI
|
// 900 is the md breakpoint in MUI
|
||||||
overflowY: window.innerWidth < 900 ? 'visible' : 'auto',
|
overflowY: window.innerWidth < 900 ? 'visible' : 'auto',
|
||||||
}}
|
}}
|
||||||
totalCount={visibleChapters.length}
|
totalCount={visibleChapters.length}
|
||||||
itemContent={(index:number) => (
|
itemContent={(index: number) => (
|
||||||
<ChapterCard
|
<ChapterCard
|
||||||
{...chaptersWithMeta[index]}
|
{...chaptersWithMeta[index]}
|
||||||
showChapterNumber={options.showChapterNumber}
|
showChapterNumber={options.showChapterNumber}
|
||||||
@@ -250,10 +268,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
{selectedChapters !== null ? (
|
{selectedChapters !== null ? (
|
||||||
<SelectionFAB
|
<SelectionFAB selectedChapters={selectedChapters} onAction={handleFabAction} />
|
||||||
selectedChapters={selectedChapters}
|
|
||||||
onAction={handleFabAction}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={mangaId} />
|
firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={mangaId} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import React from 'react';
|
|||||||
import { SORT_OPTIONS } from 'components/manga/util';
|
import { SORT_OPTIONS } from 'components/manga/util';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
open: boolean
|
open: boolean;
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
options: ChapterListOptions
|
options: ChapterListOptions;
|
||||||
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>
|
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TITLES = {
|
const TITLES = {
|
||||||
@@ -26,9 +26,7 @@ const TITLES = {
|
|||||||
display: 'Display',
|
display: 'Display',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChapterOptions: React.FC<IProps> = ({
|
const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispatch }) => (
|
||||||
open, onClose, options, optionsDispatch,
|
|
||||||
}) => (
|
|
||||||
<OptionsTabs<'filter' | 'sort' | 'display'>
|
<OptionsTabs<'filter' | 'sort' | 'display'>
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
@@ -39,9 +37,39 @@ const ChapterOptions: React.FC<IProps> = ({
|
|||||||
if (key === 'filter') {
|
if (key === 'filter') {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ThreeStateCheckboxInput label="Unread" checked={options.unread} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'unread', filterValue: c })} />
|
<ThreeStateCheckboxInput
|
||||||
<ThreeStateCheckboxInput label="Downloaded" checked={options.downloaded} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'downloaded', filterValue: c })} />
|
label="Unread"
|
||||||
<ThreeStateCheckboxInput label="Bookmarked" checked={options.bookmarked} onChange={(c) => optionsDispatch({ type: 'filter', filterType: 'bookmarked', filterValue: c })} />
|
checked={options.unread}
|
||||||
|
onChange={(c) =>
|
||||||
|
optionsDispatch({
|
||||||
|
type: 'filter',
|
||||||
|
filterType: 'unread',
|
||||||
|
filterValue: c,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ThreeStateCheckboxInput
|
||||||
|
label="Downloaded"
|
||||||
|
checked={options.downloaded}
|
||||||
|
onChange={(c) =>
|
||||||
|
optionsDispatch({
|
||||||
|
type: 'filter',
|
||||||
|
filterType: 'downloaded',
|
||||||
|
filterValue: c,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ThreeStateCheckboxInput
|
||||||
|
label="Bookmarked"
|
||||||
|
checked={options.bookmarked}
|
||||||
|
onChange={(c) =>
|
||||||
|
optionsDispatch({
|
||||||
|
type: 'filter',
|
||||||
|
filterType: 'bookmarked',
|
||||||
|
filterValue: c,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -52,15 +80,20 @@ const ChapterOptions: React.FC<IProps> = ({
|
|||||||
label={label}
|
label={label}
|
||||||
checked={options.sortBy === mode}
|
checked={options.sortBy === mode}
|
||||||
sortDescending={options.reverse}
|
sortDescending={options.reverse}
|
||||||
onClick={() => (mode !== options.sortBy
|
onClick={() =>
|
||||||
? optionsDispatch({ type: 'sortBy', sortBy: mode })
|
mode !== options.sortBy
|
||||||
: optionsDispatch({ type: 'sortReverse' }))}
|
? optionsDispatch({ type: 'sortBy', sortBy: mode })
|
||||||
|
: optionsDispatch({ type: 'sortReverse' })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if (key === 'display') {
|
if (key === 'display') {
|
||||||
return (
|
return (
|
||||||
<RadioGroup onChange={() => optionsDispatch({ type: 'showChapterNumber' })} value={options.showChapterNumber}>
|
<RadioGroup
|
||||||
|
onChange={() => optionsDispatch({ type: 'showChapterNumber' })}
|
||||||
|
value={options.showChapterNumber}
|
||||||
|
>
|
||||||
<RadioInput label="Source Title" value={false} />
|
<RadioInput label="Source Title" value={false} />
|
||||||
<RadioInput label="Chapter Number" value />
|
<RadioInput label="Chapter Number" value />
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import ChapterOptions from 'components/manga/ChapterOptions';
|
|||||||
import { isFilterActive } from 'components/manga/util';
|
import { isFilterActive } from 'components/manga/util';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
options: ChapterListOptions
|
options: ChapterListOptions;
|
||||||
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>
|
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChaptersToolbarMenu = ({ options, optionsDispatch }: IProps) => {
|
const ChaptersToolbarMenu = ({ options, optionsDispatch }: IProps) => {
|
||||||
|
|||||||
@@ -17,102 +17,103 @@ import { mutate } from 'swr';
|
|||||||
import client from 'util/client';
|
import client from 'util/client';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
|
|
||||||
const useStyles = (inLibrary: boolean) => makeStyles((theme: Theme) => ({
|
const useStyles = (inLibrary: boolean) =>
|
||||||
root: {
|
makeStyles((theme: Theme) => ({
|
||||||
width: '100%',
|
root: {
|
||||||
[theme.breakpoints.up('md')]: {
|
width: '100%',
|
||||||
position: 'sticky',
|
[theme.breakpoints.up('md')]: {
|
||||||
top: '64px',
|
position: 'sticky',
|
||||||
left: '0px',
|
top: '64px',
|
||||||
width: '50vw',
|
left: '0px',
|
||||||
height: 'calc(100vh - 64px)',
|
width: '50vw',
|
||||||
alignSelf: 'flex-start',
|
height: 'calc(100vh - 64px)',
|
||||||
overflowY: 'auto',
|
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',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
top: {
|
||||||
bottom: {
|
padding: '10px',
|
||||||
paddingLeft: '10px',
|
// [theme.breakpoints.up('md')]: {
|
||||||
paddingRight: '10px',
|
// minWidth: '50%',
|
||||||
[theme.breakpoints.up('md')]: {
|
// },
|
||||||
fontSize: '1.2em',
|
|
||||||
// maxWidth: '50%',
|
|
||||||
},
|
},
|
||||||
[theme.breakpoints.up('lg')]: {
|
leftRight: {
|
||||||
fontSize: '1.3em',
|
display: 'flex',
|
||||||
},
|
},
|
||||||
},
|
leftSide: {
|
||||||
description: {
|
'& img': {
|
||||||
'& h4': {
|
borderRadius: 4,
|
||||||
marginTop: '1em',
|
maxWidth: '100%',
|
||||||
marginBottom: 0,
|
minWidth: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
maxWidth: '50%',
|
||||||
|
// [theme.breakpoints.up('md')]: {
|
||||||
|
// minWidth: '100px',
|
||||||
|
// },
|
||||||
},
|
},
|
||||||
'& p': {
|
rightSide: {
|
||||||
textAlign: 'justify',
|
marginLeft: 15,
|
||||||
textJustify: 'inter-word',
|
maxWidth: '100%',
|
||||||
|
'& span': {
|
||||||
|
fontWeight: '400',
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('lg')]: {
|
||||||
|
fontSize: '1.3em',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
buttons: {
|
||||||
genre: {
|
display: 'flex',
|
||||||
display: 'flex',
|
justifyContent: 'space-around',
|
||||||
flexWrap: 'wrap',
|
'& button': {
|
||||||
'& h5': {
|
color: inLibrary ? '#2196f3' : 'inherit',
|
||||||
border: '2px solid #2196f3',
|
},
|
||||||
borderRadius: '1.13em',
|
'& a': {
|
||||||
marginRight: '1em',
|
textDecoration: 'none',
|
||||||
marginTop: 0,
|
color: '#858585',
|
||||||
marginBottom: '10px',
|
'& button': {
|
||||||
padding: '0.3em',
|
color: 'inherit',
|
||||||
color: '#2196f3',
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
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{
|
interface IProps {
|
||||||
manga: IManga
|
manga: IManga;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSourceName(source: ISource) {
|
function getSourceName(source: ISource) {
|
||||||
@@ -133,14 +134,24 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
const classes = useStyles(manga.inLibrary)();
|
const classes = useStyles(manga.inLibrary)();
|
||||||
|
|
||||||
const addToLibrary = () => {
|
const addToLibrary = () => {
|
||||||
mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`, { ...manga, inLibrary: true }, { revalidate: false });
|
mutate(
|
||||||
client.get(`/api/v1/manga/${manga.id}/library/`)
|
`/api/v1/manga/${manga.id}/?onlineFetch=false`,
|
||||||
|
{ ...manga, inLibrary: true },
|
||||||
|
{ revalidate: false },
|
||||||
|
);
|
||||||
|
client
|
||||||
|
.get(`/api/v1/manga/${manga.id}/library/`)
|
||||||
.then(() => mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`));
|
.then(() => mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFromLibrary = () => {
|
const removeFromLibrary = () => {
|
||||||
mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`, { ...manga, inLibrary: false }, { revalidate: false });
|
mutate(
|
||||||
client.delete(`/api/v1/manga/${manga.id}/library/`)
|
`/api/v1/manga/${manga.id}/?onlineFetch=false`,
|
||||||
|
{ ...manga, inLibrary: false },
|
||||||
|
{ revalidate: false },
|
||||||
|
);
|
||||||
|
client
|
||||||
|
.delete(`/api/v1/manga/${manga.id}/library/`)
|
||||||
.then(() => mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`));
|
.then(() => mutate(`/api/v1/manga/${manga.id}/?onlineFetch=false`));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,12 +160,13 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
<div className={classes.top}>
|
<div className={classes.top}>
|
||||||
<div className={classes.leftRight}>
|
<div className={classes.leftRight}>
|
||||||
<div className={classes.leftSide}>
|
<div className={classes.leftSide}>
|
||||||
<img src={`${serverAddress}${manga.thumbnailUrl}?useCache=${useCache}`} alt="Manga Thumbnail" />
|
<img
|
||||||
|
src={`${serverAddress}${manga.thumbnailUrl}?useCache=${useCache}`}
|
||||||
|
alt="Manga Thumbnail"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.rightSide}>
|
<div className={classes.rightSide}>
|
||||||
<h1>
|
<h1>{manga.title}</h1>
|
||||||
{manga.title}
|
|
||||||
</h1>
|
|
||||||
<h3>
|
<h3>
|
||||||
{'Author: '}
|
{'Author: '}
|
||||||
<span>{getValueOrUnknown(manga.author)}</span>
|
<span>{getValueOrUnknown(manga.author)}</span>
|
||||||
@@ -163,20 +175,21 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
{'Artist: '}
|
{'Artist: '}
|
||||||
<span>{getValueOrUnknown(manga.artist)}</span>
|
<span>{getValueOrUnknown(manga.artist)}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<h3>
|
<h3>{`Status: ${manga.status}`}</h3>
|
||||||
{`Status: ${manga.status}`}
|
<h3>{`Source: ${getSourceName(manga.source)}`}</h3>
|
||||||
</h3>
|
|
||||||
<h3>
|
|
||||||
{`Source: ${getSourceName(manga.source)}`}
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.buttons}>
|
<div className={classes.buttons}>
|
||||||
<div>
|
<div>
|
||||||
<IconButton onClick={manga.inLibrary ? removeFromLibrary : addToLibrary} size="large">
|
<IconButton
|
||||||
{manga.inLibrary
|
onClick={manga.inLibrary ? removeFromLibrary : addToLibrary}
|
||||||
? <FavoriteIcon sx={{ mr: 1 }} />
|
size="large"
|
||||||
: <FavoriteBorderIcon sx={{ mr: 1 }} />}
|
>
|
||||||
|
{manga.inLibrary ? (
|
||||||
|
<FavoriteIcon sx={{ mr: 1 }} />
|
||||||
|
) : (
|
||||||
|
<FavoriteBorderIcon sx={{ mr: 1 }} />
|
||||||
|
)}
|
||||||
<Typography sx={{ fontSize: { xs: '0.75em', sm: '0.85em' } }}>
|
<Typography sx={{ fontSize: { xs: '0.75em', sm: '0.85em' } }}>
|
||||||
{manga.inLibrary ? 'In Library' : 'Add To Library'}
|
{manga.inLibrary ? 'In Library' : 'Add To Library'}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -198,7 +211,9 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
<p>{manga.description}</p>
|
<p>{manga.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.genre}>
|
<div className={classes.genre}>
|
||||||
{manga.genre.map((g) => <h5 key={g}>{g}</h5>)}
|
{manga.genre.map((g) => (
|
||||||
|
<h5 key={g}>{g}</h5>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ import Label from '@mui/icons-material/Label';
|
|||||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||||
import Refresh from '@mui/icons-material/Refresh';
|
import Refresh from '@mui/icons-material/Refresh';
|
||||||
import {
|
import {
|
||||||
IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip, useMediaQuery, useTheme,
|
IconButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Tooltip,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import CategorySelect from 'components/navbar/action/CategorySelect';
|
import CategorySelect from 'components/navbar/action/CategorySelect';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
@@ -37,13 +44,22 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
{isLargeScreen && (
|
{isLargeScreen && (
|
||||||
<>
|
<>
|
||||||
<Tooltip title="Reload data from source">
|
<Tooltip title="Reload data from source">
|
||||||
<IconButton onClick={() => { onRefresh(); }} disabled={refreshing}>
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
onRefresh();
|
||||||
|
}}
|
||||||
|
disabled={refreshing}
|
||||||
|
>
|
||||||
<Refresh />
|
<Refresh />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{manga.inLibrary && (
|
{manga.inLibrary && (
|
||||||
<Tooltip title="Edit manga categories">
|
<Tooltip title="Edit manga categories">
|
||||||
<IconButton onClick={() => { setEditCategories(true); }}>
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
setEditCategories(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Label />
|
<Label />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -71,37 +87,35 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => { onRefresh(); handleClose(); }}
|
onClick={() => {
|
||||||
|
onRefresh();
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
disabled={refreshing}
|
disabled={refreshing}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Refresh fontSize="small" />
|
<Refresh fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>Reload data from source</ListItemText>
|
||||||
Reload data from source
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{manga.inLibrary && (
|
{manga.inLibrary && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => { setEditCategories(true); handleClose(); }}
|
onClick={() => {
|
||||||
|
setEditCategories(true);
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Label fontSize="small" />
|
<Label fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>Edit manga categories</ListItemText>
|
||||||
Edit manga categories
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CategorySelect
|
<CategorySelect open={editCategories} setOpen={setEditCategories} mangaId={manga.id} />
|
||||||
open={editCategories}
|
|
||||||
setOpen={setEditCategories}
|
|
||||||
mangaId={manga.id}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,23 +11,29 @@ import { Link } from 'react-router-dom';
|
|||||||
import { PlayArrow } from '@mui/icons-material';
|
import { PlayArrow } from '@mui/icons-material';
|
||||||
import { BACK } from 'util/useBackTo';
|
import { BACK } from 'util/useBackTo';
|
||||||
|
|
||||||
interface ResumeFABProps{
|
interface ResumeFABProps {
|
||||||
chapter: IChapter
|
chapter: IChapter;
|
||||||
mangaId: string
|
mangaId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ResumeFab(props: ResumeFABProps) {
|
export default function ResumeFab(props: ResumeFABProps) {
|
||||||
const { chapter: { index, lastPageRead }, mangaId } = props;
|
const {
|
||||||
|
chapter: { index, lastPageRead },
|
||||||
|
mangaId,
|
||||||
|
} = props;
|
||||||
return (
|
return (
|
||||||
<Fab
|
<Fab
|
||||||
sx={{ position: 'fixed', bottom: '2em', right: '3em' }}
|
sx={{ position: 'fixed', bottom: '2em', right: '3em' }}
|
||||||
component={Link}
|
component={Link}
|
||||||
variant="extended"
|
variant="extended"
|
||||||
color="primary"
|
color="primary"
|
||||||
to={{ pathname: `/manga/${mangaId}/chapter/${index}/page/${lastPageRead}`, state: { backLink: BACK } }}
|
to={{
|
||||||
|
pathname: `/manga/${mangaId}/chapter/${index}/page/${lastPageRead}`,
|
||||||
|
state: { backLink: BACK },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PlayArrow />
|
<PlayArrow />
|
||||||
{index === 1 ? 'Start' : 'Resume' }
|
{index === 1 ? 'Start' : 'Resume'}
|
||||||
</Fab>
|
</Fab>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,24 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||||
import {
|
import { Fab, Menu } from '@mui/material';
|
||||||
Fab, Menu,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import { pluralize } from 'components/util/helpers';
|
import { pluralize } from 'components/util/helpers';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import type { IChapterWithMeta } from 'components/manga/ChapterList';
|
import type { IChapterWithMeta } from 'components/manga/ChapterList';
|
||||||
import SelectionFABActionItem from 'components/manga/SelectionFABActionItem';
|
import SelectionFABActionItem from 'components/manga/SelectionFABActionItem';
|
||||||
|
|
||||||
export type SelectionAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread';
|
export type SelectionAction =
|
||||||
|
| 'download'
|
||||||
|
| 'delete'
|
||||||
|
| 'bookmark'
|
||||||
|
| 'unbookmark'
|
||||||
|
| 'mark_as_read'
|
||||||
|
| 'mark_as_unread';
|
||||||
|
|
||||||
interface SelectionFABProps{
|
interface SelectionFABProps {
|
||||||
selectedChapters: IChapterWithMeta[]
|
selectedChapters: IChapterWithMeta[];
|
||||||
onAction: (action: SelectionAction, chapters: IChapterWithMeta[]) => void
|
onAction: (action: SelectionAction, chapters: IChapterWithMeta[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
||||||
@@ -38,7 +42,10 @@ const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'fixed', bottom: '2em', right: '3em', pt: 1,
|
position: 'fixed',
|
||||||
|
bottom: '2em',
|
||||||
|
right: '3em',
|
||||||
|
pt: 1,
|
||||||
}}
|
}}
|
||||||
ref={anchorEl}
|
ref={anchorEl}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import type { IChapterWithMeta } from 'components/manga/ChapterList';
|
|||||||
import type { SelectionAction } from 'components/manga/SelectionFAB';
|
import type { SelectionAction } from 'components/manga/SelectionFAB';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
action: SelectionAction
|
action: SelectionAction;
|
||||||
matchingChapters: IChapterWithMeta[]
|
matchingChapters: IChapterWithMeta[];
|
||||||
title: string
|
title: string;
|
||||||
onClick: (action: SelectionAction, chapters: IChapterWithMeta[]) => void
|
onClick: (action: SelectionAction, chapters: IChapterWithMeta[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
@@ -32,16 +32,11 @@ const ICONS = {
|
|||||||
mark_as_unread: RemoveDone,
|
mark_as_unread: RemoveDone,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectionFABActionItem: React.FC<IProps> = ({
|
const SelectionFABActionItem: React.FC<IProps> = ({ action, matchingChapters, onClick, title }) => {
|
||||||
action, matchingChapters, onClick, title,
|
|
||||||
}) => {
|
|
||||||
const count = matchingChapters.length;
|
const count = matchingChapters.length;
|
||||||
const Icon = ICONS[action];
|
const Icon = ICONS[action];
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem onClick={() => onClick(action, matchingChapters)} disabled={count === 0}>
|
||||||
onClick={() => onClick(action, matchingChapters)}
|
|
||||||
disabled={count === 0}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Icon fontSize="small" />
|
<Icon fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ export const useRefreshManga = (mangaId: string) => {
|
|||||||
const handleRefresh = useCallback(async () => {
|
const handleRefresh = useCallback(async () => {
|
||||||
setFetchingOnline(true);
|
setFetchingOnline(true);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetcher(`/api/v1/manga/${mangaId}/?onlineFetch=true`)
|
fetcher(`/api/v1/manga/${mangaId}/?onlineFetch=true`).then((res) =>
|
||||||
.then((res) => mutate(`/api/v1/manga/${mangaId}/?onlineFetch=false`, res, { revalidate: false })),
|
mutate(`/api/v1/manga/${mangaId}/?onlineFetch=false`, res, { revalidate: false }),
|
||||||
fetcher(`/api/v1/manga/${mangaId}/chapters?onlineFetch=true`)
|
),
|
||||||
.then((res) => mutate(`/api/v1/manga/${mangaId}/chapters?onlineFetch=false`, res, { revalidate: false })),
|
fetcher(`/api/v1/manga/${mangaId}/chapters?onlineFetch=true`).then((res) =>
|
||||||
|
mutate(`/api/v1/manga/${mangaId}/chapters?onlineFetch=false`, res, {
|
||||||
|
revalidate: false,
|
||||||
|
}),
|
||||||
|
),
|
||||||
]).finally(() => setFetchingOnline(false));
|
]).finally(() => setFetchingOnline(false));
|
||||||
}, [mangaId]);
|
}, [mangaId]);
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ const defaultChapterOptions: ChapterListOptions = {
|
|||||||
showChapterNumber: false,
|
showChapterNumber: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function chapterOptionsReducer(state: ChapterListOptions,
|
function chapterOptionsReducer(
|
||||||
actions: ChapterOptionsReducerAction)
|
state: ChapterListOptions,
|
||||||
: ChapterListOptions {
|
actions: ChapterOptionsReducerAction,
|
||||||
|
): ChapterListOptions {
|
||||||
switch (actions.type) {
|
switch (actions.type) {
|
||||||
case 'filter':
|
case 'filter':
|
||||||
// eslint-disable-next-line no-case-declarations
|
// eslint-disable-next-line no-case-declarations
|
||||||
const active = state.unread !== false
|
const active =
|
||||||
&& state.downloaded !== false
|
state.unread !== false && state.downloaded !== false && state.bookmarked !== false;
|
||||||
&& state.bookmarked !== false;
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
active,
|
active,
|
||||||
@@ -53,8 +53,10 @@ export function unreadFilter(unread: NullAndUndefined<boolean>, { read: isChapte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFilter(downloaded: NullAndUndefined<boolean>,
|
function downloadFilter(
|
||||||
{ downloaded: chapterDownload }: IChapter) {
|
downloaded: NullAndUndefined<boolean>,
|
||||||
|
{ downloaded: chapterDownload }: IChapter,
|
||||||
|
) {
|
||||||
switch (downloaded) {
|
switch (downloaded) {
|
||||||
case true:
|
case true:
|
||||||
return chapterDownload;
|
return chapterDownload;
|
||||||
@@ -65,8 +67,10 @@ function downloadFilter(downloaded: NullAndUndefined<boolean>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>,
|
function bookmarkedFilter(
|
||||||
{ bookmarked: chapterBookmarked }: IChapter) {
|
bookmarked: NullAndUndefined<boolean>,
|
||||||
|
{ bookmarked: chapterBookmarked }: IChapter,
|
||||||
|
) {
|
||||||
switch (bookmarked) {
|
switch (bookmarked) {
|
||||||
case true:
|
case true:
|
||||||
return chapterBookmarked;
|
return chapterBookmarked;
|
||||||
@@ -77,29 +81,34 @@ function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterAndSortChapters(chapters: IChapter[], options: ChapterListOptions)
|
export function filterAndSortChapters(
|
||||||
: IChapter[] {
|
chapters: IChapter[],
|
||||||
|
options: ChapterListOptions,
|
||||||
|
): IChapter[] {
|
||||||
const filtered = options.active
|
const filtered = options.active
|
||||||
? chapters.filter((chp) => unreadFilter(options.unread, chp)
|
? chapters.filter(
|
||||||
&& downloadFilter(options.downloaded, chp)
|
(chp) =>
|
||||||
&& bookmarkedFilter(options.bookmarked, chp))
|
unreadFilter(options.unread, chp) &&
|
||||||
|
downloadFilter(options.downloaded, chp) &&
|
||||||
|
bookmarkedFilter(options.bookmarked, chp),
|
||||||
|
)
|
||||||
: [...chapters];
|
: [...chapters];
|
||||||
const Sorted = options.sortBy === 'fetchedAt'
|
const Sorted =
|
||||||
? filtered.sort((a, b) => a.fetchedAt - b.fetchedAt)
|
options.sortBy === 'fetchedAt'
|
||||||
: filtered;
|
? filtered.sort((a, b) => a.fetchedAt - b.fetchedAt)
|
||||||
|
: filtered;
|
||||||
if (options.reverse) {
|
if (options.reverse) {
|
||||||
Sorted.reverse();
|
Sorted.reverse();
|
||||||
}
|
}
|
||||||
return Sorted;
|
return Sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useChapterOptions = (mangaId: string) => useReducerLocalStorage<
|
export const useChapterOptions = (mangaId: string) =>
|
||||||
ChapterListOptions,
|
useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>(
|
||||||
ChapterOptionsReducerAction
|
chapterOptionsReducer,
|
||||||
>(
|
`${mangaId}filterOptions`,
|
||||||
chapterOptionsReducer,
|
defaultChapterOptions,
|
||||||
`${mangaId}filterOptions`, defaultChapterOptions,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
export const SORT_OPTIONS: [ChapterSortMode, string][] = [
|
export const SORT_OPTIONS: [ChapterSortMode, string][] = [
|
||||||
['source', 'By Source'],
|
['source', 'By Source'],
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Box } from '@mui/system';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface DownloadStateIndicatorProps {
|
interface DownloadStateIndicatorProps {
|
||||||
download: IDownloadChapter
|
download: IDownloadChapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ download }) => (
|
const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ download }) => (
|
||||||
@@ -25,10 +25,7 @@ const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ downloa
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{download.progress !== 0 && (
|
{download.progress !== 0 && (
|
||||||
<CircularProgress
|
<CircularProgress variant="determinate" value={download.progress * 100} />
|
||||||
variant="determinate"
|
|
||||||
value={download.progress * 100}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -5,22 +5,18 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import {
|
import { Drawer } from '@mui/material';
|
||||||
Drawer,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
open: boolean
|
open: boolean;
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
minHeight?: number
|
minHeight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OptionsPanel: React.FC<IProps> = ({
|
const OptionsPanel: React.FC<IProps> = ({ open, onClose, children, minHeight }) => (
|
||||||
open, onClose, children, minHeight,
|
|
||||||
}) => (
|
|
||||||
<Drawer
|
<Drawer
|
||||||
anchor="bottom"
|
anchor="bottom"
|
||||||
open={open}
|
open={open}
|
||||||
@@ -34,9 +30,7 @@ const OptionsPanel: React.FC<IProps> = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>{children}</Box>
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -10,26 +10,27 @@ import TabPanel from 'components/util/TabPanel';
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import OptionsPanel from 'components/molecules/OptionsPanel';
|
import OptionsPanel from 'components/molecules/OptionsPanel';
|
||||||
|
|
||||||
interface IProps<T = string>{
|
interface IProps<T = string> {
|
||||||
open: boolean
|
open: boolean;
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
tabs: T[]
|
tabs: T[];
|
||||||
tabTitle: (key: T) => React.ReactNode
|
tabTitle: (key: T) => React.ReactNode;
|
||||||
tabContent: (key: T) => React.ReactNode
|
tabContent: (key: T) => React.ReactNode;
|
||||||
minHeight?: number
|
minHeight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OptionsTabs = <T extends string = string>({
|
const OptionsTabs = <T extends string = string>({
|
||||||
open, onClose, tabs, tabTitle, tabContent, minHeight,
|
open,
|
||||||
|
onClose,
|
||||||
|
tabs,
|
||||||
|
tabTitle,
|
||||||
|
tabContent,
|
||||||
|
minHeight,
|
||||||
}: IProps<T>) => {
|
}: IProps<T>) => {
|
||||||
const [tabNum, setTabNum] = useState(0);
|
const [tabNum, setTabNum] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionsPanel
|
<OptionsPanel open={open} onClose={onClose} minHeight={minHeight}>
|
||||||
open={open}
|
|
||||||
onClose={onClose}
|
|
||||||
minHeight={minHeight}
|
|
||||||
>
|
|
||||||
<Tabs
|
<Tabs
|
||||||
value={tabNum}
|
value={tabNum}
|
||||||
variant="fullWidth"
|
variant="fullWidth"
|
||||||
@@ -43,9 +44,7 @@ const OptionsTabs = <T extends string = string>({
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
{tabs.map((tab, tabIndex) => (
|
{tabs.map((tab, tabIndex) => (
|
||||||
<TabPanel key={tab} index={tabIndex} currentIndex={tabNum}>
|
<TabPanel key={tab} index={tabIndex} currentIndex={tabNum}>
|
||||||
<Stack sx={{ px: 3, py: 1, minHeight }}>
|
<Stack sx={{ px: 3, py: 1, minHeight }}>{tabContent(tab)}</Stack>
|
||||||
{tabContent(tab)}
|
|
||||||
</Stack>
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
))}
|
))}
|
||||||
</OptionsPanel>
|
</OptionsPanel>
|
||||||
|
|||||||
@@ -39,37 +39,43 @@ const navbarItems: Array<NavbarItem> = [
|
|||||||
SelectedIconComponent: CollectionsBookmarkIcon,
|
SelectedIconComponent: CollectionsBookmarkIcon,
|
||||||
IconComponent: CollectionsOutlinedBookmarkIcon,
|
IconComponent: CollectionsOutlinedBookmarkIcon,
|
||||||
show: 'both',
|
show: 'both',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
path: '/updates',
|
path: '/updates',
|
||||||
title: 'Updates',
|
title: 'Updates',
|
||||||
SelectedIconComponent: NewReleasesIcon,
|
SelectedIconComponent: NewReleasesIcon,
|
||||||
IconComponent: NewReleasesOutlinedIcon,
|
IconComponent: NewReleasesOutlinedIcon,
|
||||||
show: 'both',
|
show: 'both',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
path: '/extensions',
|
path: '/extensions',
|
||||||
title: 'Extensions',
|
title: 'Extensions',
|
||||||
SelectedIconComponent: ExtensionIcon,
|
SelectedIconComponent: ExtensionIcon,
|
||||||
IconComponent: ExtensionOutlinedIcon,
|
IconComponent: ExtensionOutlinedIcon,
|
||||||
show: 'desktop',
|
show: 'desktop',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
path: '/sources',
|
path: '/sources',
|
||||||
title: 'Sources',
|
title: 'Sources',
|
||||||
SelectedIconComponent: ExploreIcon,
|
SelectedIconComponent: ExploreIcon,
|
||||||
IconComponent: ExploreOutlinedIcon,
|
IconComponent: ExploreOutlinedIcon,
|
||||||
show: 'desktop',
|
show: 'desktop',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
path: '/browse',
|
path: '/browse',
|
||||||
title: 'Browse',
|
title: 'Browse',
|
||||||
SelectedIconComponent: ExploreIcon,
|
SelectedIconComponent: ExploreIcon,
|
||||||
IconComponent: ExploreOutlinedIcon,
|
IconComponent: ExploreOutlinedIcon,
|
||||||
show: 'mobile',
|
show: 'mobile',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
path: '/downloads',
|
path: '/downloads',
|
||||||
title: 'Downloads',
|
title: 'Downloads',
|
||||||
SelectedIconComponent: GetAppIcon,
|
SelectedIconComponent: GetAppIcon,
|
||||||
IconComponent: GetAppOutlinedIcon,
|
IconComponent: GetAppOutlinedIcon,
|
||||||
show: 'both',
|
show: 'both',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
SelectedIconComponent: SettingsIcon,
|
SelectedIconComponent: SettingsIcon,
|
||||||
@@ -94,7 +100,9 @@ export default function DefaultNavBar() {
|
|||||||
let navbar = <></>;
|
let navbar = <></>;
|
||||||
if (isMobileWidth) {
|
if (isMobileWidth) {
|
||||||
if (isMainRoute) {
|
if (isMainRoute) {
|
||||||
navbar = <MobileBottomBar navBarItems={navbarItems.filter((it) => it.show !== 'desktop')} />;
|
navbar = (
|
||||||
|
<MobileBottomBar navBarItems={navbarItems.filter((it) => it.show !== 'desktop')} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
navbar = <DesktopSideBar navBarItems={navbarItems.filter((it) => it.show !== 'mobile')} />;
|
navbar = <DesktopSideBar navBarItems={navbarItems.filter((it) => it.show !== 'mobile')} />;
|
||||||
@@ -124,7 +132,12 @@ export default function DefaultNavBar() {
|
|||||||
<ArrowBack />
|
<ArrowBack />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
<Typography variant={isMobileWidth ? 'h6' : 'h5'} sx={{ flexGrow: 1 }} noWrap textOverflow="ellipsis">
|
<Typography
|
||||||
|
variant={isMobileWidth ? 'h6' : 'h5'}
|
||||||
|
sx={{ flexGrow: 1 }}
|
||||||
|
noWrap
|
||||||
|
textOverflow="ellipsis"
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
{action}
|
{action}
|
||||||
@@ -137,7 +150,7 @@ export default function DefaultNavBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface INavbarToolbarProps {
|
interface INavbarToolbarProps {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavbarToolbar: React.FC<INavbarToolbarProps> = ({ children }) => {
|
export const NavbarToolbar: React.FC<INavbarToolbarProps> = ({ children }) => {
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import NavBarContext from 'components/context/NavbarContext';
|
import NavBarContext from 'components/context/NavbarContext';
|
||||||
|
|
||||||
interface IProps{
|
interface IProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NavBarProvider({ children }:IProps) {
|
export default function NavBarProvider({ children }: IProps) {
|
||||||
const [defaultBackTo, setDefaultBackTo] = useState<string | undefined>();
|
const [defaultBackTo, setDefaultBackTo] = useState<string | undefined>();
|
||||||
const [title, setTitle] = useState<string>('Tachidesk');
|
const [title, setTitle] = useState<string>('Tachidesk');
|
||||||
const [action, setAction] = useState<any>(<div />);
|
const [action, setAction] = useState<any>(<div />);
|
||||||
@@ -36,9 +36,5 @@ export default function NavBarProvider({ children }:IProps) {
|
|||||||
override,
|
override,
|
||||||
setOverride,
|
setOverride,
|
||||||
};
|
};
|
||||||
return (
|
return <NavBarContext.Provider value={value}>{children}</NavBarContext.Provider>;
|
||||||
<NavBarContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</NavBarContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,28 +104,23 @@ const OpenDrawerButton = styled(IconButton)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
settings: IReaderSettings
|
settings: IReaderSettings;
|
||||||
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void
|
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
|
||||||
manga: IManga | IMangaCard
|
manga: IManga | IMangaCard;
|
||||||
chapter: IChapter
|
chapter: IChapter;
|
||||||
curPage: number
|
curPage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ReaderNavBar(props: IProps) {
|
export default function ReaderNavBar(props: IProps) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const backTo = useBackTo();
|
const backTo = useBackTo();
|
||||||
const location = useLocation<{
|
const location = useLocation<{
|
||||||
prevDrawerOpen?: boolean,
|
prevDrawerOpen?: boolean;
|
||||||
prevSettingsCollapseOpen?: boolean
|
prevSettingsCollapseOpen?: boolean;
|
||||||
}>();
|
}>();
|
||||||
const {
|
const { prevDrawerOpen, prevSettingsCollapseOpen } = location.state ?? {};
|
||||||
prevDrawerOpen,
|
|
||||||
prevSettingsCollapseOpen,
|
|
||||||
} = location.state ?? {};
|
|
||||||
|
|
||||||
const {
|
const { settings, setSettingValue, manga, chapter, curPage } = props;
|
||||||
settings, setSettingValue, manga, chapter, curPage,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(settings.staticNav || prevDrawerOpen);
|
const [drawerOpen, setDrawerOpen] = useState(settings.staticNav || prevDrawerOpen);
|
||||||
const [updateDrawerOnRender, setUpdateDrawerOnRender] = useState(true);
|
const [updateDrawerOnRender, setUpdateDrawerOnRender] = useState(true);
|
||||||
@@ -164,8 +159,8 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
const rootEl:HTMLDivElement = document.querySelector('#root')!;
|
const rootEl: HTMLDivElement = document.querySelector('#root')!;
|
||||||
const mainContainer:HTMLDivElement = document.querySelector('#appMainContainer')!;
|
const mainContainer: HTMLDivElement = document.querySelector('#appMainContainer')!;
|
||||||
|
|
||||||
// main container and root div need to change styles...
|
// main container and root div need to change styles...
|
||||||
rootEl.style.display = 'flex';
|
rootEl.style.display = 'flex';
|
||||||
@@ -176,7 +171,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
mainContainer.style.display = 'block';
|
mainContainer.style.display = 'block';
|
||||||
window.removeEventListener('scroll', handleScroll);
|
window.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
}, [handleScroll]);// handleScroll changes on every render
|
}, [handleScroll]); // handleScroll changes on every render
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (backTo.back) history.goBack();
|
if (backTo.back) history.goBack();
|
||||||
@@ -194,13 +189,13 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
mountOnEnter
|
mountOnEnter
|
||||||
unmountOnExit
|
unmountOnExit
|
||||||
>
|
>
|
||||||
<Root sx={{
|
<Root
|
||||||
position: settings.staticNav ? 'sticky' : 'fixed',
|
sx={{
|
||||||
}}
|
position: settings.staticNav ? 'sticky' : 'fixed',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
{!settings.staticNav
|
{!settings.staticNav && (
|
||||||
&& (
|
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@@ -212,7 +207,12 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
<KeyboardArrowLeftIcon />
|
<KeyboardArrowLeftIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
<Typography variant="h1" textOverflow="ellipsis" overflow="hidden" sx={{ py: 1 }}>
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
textOverflow="ellipsis"
|
||||||
|
overflow="hidden"
|
||||||
|
sx={{ py: 1 }}
|
||||||
|
>
|
||||||
{chapter.name}
|
{chapter.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -262,12 +262,9 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
</Collapse>
|
</Collapse>
|
||||||
<Divider sx={{ my: 1, mx: 2 }} />
|
<Divider sx={{ my: 1, mx: 2 }} />
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<span>
|
<span>{`Currently on page ${curPage + 1} of ${chapter.pageCount}`}</span>
|
||||||
{`Currently on page ${curPage + 1} of ${chapter.pageCount}`}
|
|
||||||
</span>
|
|
||||||
<ChapterNavigation>
|
<ChapterNavigation>
|
||||||
{chapter.index > 1
|
{chapter.index > 1 && (
|
||||||
&& (
|
|
||||||
<Link
|
<Link
|
||||||
replace
|
replace
|
||||||
to={{
|
to={{
|
||||||
@@ -287,8 +284,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{chapter.index < chapter.chapterCount
|
{chapter.index < chapter.chapterCount && (
|
||||||
&& (
|
|
||||||
<Link
|
<Link
|
||||||
replace
|
replace
|
||||||
style={{ gridArea: 'next' }}
|
style={{ gridArea: 'next' }}
|
||||||
@@ -300,10 +296,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button variant="outlined" endIcon={<KeyboardArrowRightIcon />}>
|
||||||
variant="outlined"
|
|
||||||
endIcon={<KeyboardArrowRightIcon />}
|
|
||||||
>
|
|
||||||
Next Chapter
|
Next Chapter
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -17,15 +17,17 @@ import FormGroup from '@mui/material/FormGroup';
|
|||||||
import client, { useQuery } from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
open: boolean
|
open: boolean;
|
||||||
setOpen: (value: boolean) => void
|
setOpen: (value: boolean) => void;
|
||||||
mangaId: number
|
mangaId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CategorySelect(props: IProps) {
|
export default function CategorySelect(props: IProps) {
|
||||||
const { open, setOpen, mangaId } = props;
|
const { open, setOpen, mangaId } = props;
|
||||||
|
|
||||||
const { data: mangaCategoriesData, mutate } = useQuery<ICategory[]>(`/api/v1/manga/${mangaId}/category`);
|
const { data: mangaCategoriesData, mutate } = useQuery<ICategory[]>(
|
||||||
|
`/api/v1/manga/${mangaId}/category`,
|
||||||
|
);
|
||||||
const { data: categoriesData } = useQuery<ICategory[]>('/api/v1/category');
|
const { data: categoriesData } = useQuery<ICategory[]>('/api/v1/category');
|
||||||
|
|
||||||
const allCategories = useMemo(() => {
|
const allCategories = useMemo(() => {
|
||||||
@@ -50,8 +52,7 @@ export default function CategorySelect(props: IProps) {
|
|||||||
const { checked } = event.target as HTMLInputElement;
|
const { checked } = event.target as HTMLInputElement;
|
||||||
|
|
||||||
const method = checked ? client.get : client.delete;
|
const method = checked ? client.get : client.delete;
|
||||||
method(`/api/v1/manga/${mangaId}/category/${categoryId}`)
|
method(`/api/v1/manga/${mangaId}/category/${categoryId}`).then(() => mutate());
|
||||||
.then(() => mutate());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -68,29 +69,27 @@ export default function CategorySelect(props: IProps) {
|
|||||||
<DialogTitle>Set categories</DialogTitle>
|
<DialogTitle>Set categories</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{allCategories.length === 0
|
{allCategories.length === 0 && (
|
||||||
&& (
|
<span>
|
||||||
<span>
|
No categories found!
|
||||||
No categories found!
|
<br />
|
||||||
<br />
|
You should make some from settings.
|
||||||
You should make some from settings.
|
</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
|
||||||
{allCategories.map((category) => (
|
{allCategories.map((category) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={(
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedIds.includes(category.id)}
|
checked={selectedIds.includes(category.id)}
|
||||||
onChange={(e) => handleChange(e, category.id)}
|
onChange={(e) => handleChange(e, category.id)}
|
||||||
color="default"
|
color="default"
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
label={category.name}
|
label={category.name}
|
||||||
key={category.id}
|
key={category.id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button autoFocus onClick={handleCancel} color="primary">
|
<Button autoFocus onClick={handleCancel} color="primary">
|
||||||
|
|||||||
@@ -31,16 +31,14 @@ function removeAll(firstList: any[], secondList: any[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
shownLangs: string[]
|
shownLangs: string[];
|
||||||
setShownLangs: (arg0: string[]) => void
|
setShownLangs: (arg0: string[]) => void;
|
||||||
allLangs: string[]
|
allLangs: string[];
|
||||||
forcedLangs?: string[]
|
forcedLangs?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LangSelect(props: IProps) {
|
export default function LangSelect(props: IProps) {
|
||||||
const {
|
const { shownLangs, setShownLangs, allLangs, forcedLangs } = props;
|
||||||
shownLangs, setShownLangs, allLangs, forcedLangs,
|
|
||||||
} = props;
|
|
||||||
// hold a copy and only sate state on parent when OK pressed, improves performance
|
// hold a copy and only sate state on parent when OK pressed, improves performance
|
||||||
const [mShownLangs, setMShownLangs] = useState(
|
const [mShownLangs, setMShownLangs] = useState(
|
||||||
removeAll(cloneObject(shownLangs), forcedLangs!),
|
removeAll(cloneObject(shownLangs), forcedLangs!),
|
||||||
@@ -102,11 +100,9 @@ export default function LangSelect(props: IProps) {
|
|||||||
onChange={(e) => handleChange(e, lang)}
|
onChange={(e) => handleChange(e, lang)}
|
||||||
/>
|
/>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button autoFocus onClick={handleCancel} color="primary">
|
<Button autoFocus onClick={handleCancel} color="primary">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const SideNavBarContainer = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
navBarItems: Array<NavbarItem>
|
navBarItems: Array<NavbarItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DesktopSideBar({ navBarItems }: IProps) {
|
export default function DesktopSideBar({ navBarItems }: IProps) {
|
||||||
@@ -32,27 +32,37 @@ export default function DesktopSideBar({ navBarItems }: IProps) {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const iconFor = (path: string, IconComponent: any, SelectedIconComponent: any) => {
|
const iconFor = (path: string, IconComponent: any, SelectedIconComponent: any) => {
|
||||||
if (location.pathname === path) return <SelectedIconComponent sx={{ color: 'primary.main' }} fontSize="large" />;
|
if (location.pathname === path)
|
||||||
return <IconComponent sx={{ color: (theme.palette.mode === 'dark') ? 'grey.A400' : 'grey.600' }} fontSize="large" />;
|
return <SelectedIconComponent sx={{ color: 'primary.main' }} fontSize="large" />;
|
||||||
|
return (
|
||||||
|
<IconComponent
|
||||||
|
sx={{ color: theme.palette.mode === 'dark' ? 'grey.A400' : 'grey.600' }}
|
||||||
|
fontSize="large"
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SideNavBarContainer>
|
<SideNavBarContainer>
|
||||||
{
|
{
|
||||||
// eslint-disable-next-line react/destructuring-assignment
|
// eslint-disable-next-line react/destructuring-assignment
|
||||||
navBarItems.map(({
|
navBarItems.map(
|
||||||
path, title, IconComponent, SelectedIconComponent,
|
({ path, title, IconComponent, SelectedIconComponent }: NavbarItem) => (
|
||||||
}: NavbarItem) => (
|
<Link
|
||||||
<Link to={path} style={{ color: 'inherit', textDecoration: 'none' }} key={path}>
|
to={path}
|
||||||
<ListItem disableRipple button key={title}>
|
style={{ color: 'inherit', textDecoration: 'none' }}
|
||||||
<ListItemIcon sx={{ minWidth: '0' }}>
|
key={path}
|
||||||
<Tooltip placement="right" title={title}>
|
>
|
||||||
{iconFor(path, IconComponent, SelectedIconComponent)}
|
<ListItem disableRipple button key={title}>
|
||||||
</Tooltip>
|
<ListItemIcon sx={{ minWidth: '0' }}>
|
||||||
</ListItemIcon>
|
<Tooltip placement="right" title={title}>
|
||||||
</ListItem>
|
{iconFor(path, IconComponent, SelectedIconComponent)}
|
||||||
</Link>
|
</Tooltip>
|
||||||
))
|
</ListItemIcon>
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</SideNavBarContainer>
|
</SideNavBarContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const Link = styled(RRDLink)({
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
navBarItems: Array<NavbarItem>
|
navBarItems: Array<NavbarItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MobileBottomBar({ navBarItems }: IProps) {
|
export default function MobileBottomBar({ navBarItems }: IProps) {
|
||||||
@@ -41,43 +41,48 @@ export default function MobileBottomBar({ navBarItems }: IProps) {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const iconFor = (path: string, IconComponent: any, SelectedIconComponent: any) => {
|
const iconFor = (path: string, IconComponent: any, SelectedIconComponent: any) => {
|
||||||
if (location.pathname === path) return <SelectedIconComponent sx={{ color: 'primary.main' }} fontSize="medium" />;
|
if (location.pathname === path)
|
||||||
return <IconComponent sx={{ color: (theme.palette.mode === 'dark') ? 'grey.A400' : 'grey.600' }} fontSize="medium" />;
|
return <SelectedIconComponent sx={{ color: 'primary.main' }} fontSize="medium" />;
|
||||||
|
return (
|
||||||
|
<IconComponent
|
||||||
|
sx={{ color: theme.palette.mode === 'dark' ? 'grey.A400' : 'grey.600' }}
|
||||||
|
fontSize="medium"
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomNavContainer>
|
<BottomNavContainer>
|
||||||
{
|
{navBarItems.map(
|
||||||
navBarItems.map((
|
({ path, title, IconComponent, SelectedIconComponent }: NavbarItem) => (
|
||||||
{
|
|
||||||
path, title, IconComponent, SelectedIconComponent,
|
|
||||||
}: NavbarItem,
|
|
||||||
) => (
|
|
||||||
<Link to={path} key={path}>
|
<Link to={path} key={path}>
|
||||||
<ListItem disableRipple button sx={{ justifyContent: 'center', padding: '8px' }} key={title}>
|
<ListItem
|
||||||
<Box
|
disableRipple
|
||||||
display="flex"
|
button
|
||||||
flexDirection="column"
|
sx={{ justifyContent: 'center', padding: '8px' }}
|
||||||
alignItems="center"
|
key={title}
|
||||||
>
|
>
|
||||||
|
<Box display="flex" flexDirection="column" alignItems="center">
|
||||||
{iconFor(path, IconComponent, SelectedIconComponent)}
|
{iconFor(path, IconComponent, SelectedIconComponent)}
|
||||||
<Box sx={{
|
<Box
|
||||||
fontSize: '0.65rem',
|
sx={{
|
||||||
// eslint-disable-next-line no-nested-ternary
|
fontSize: '0.65rem',
|
||||||
color: location.pathname === path
|
color:
|
||||||
? 'primary.main'
|
// eslint-disable-next-line no-nested-ternary
|
||||||
: ((theme.palette.mode === 'dark')
|
location.pathname === path
|
||||||
? 'grey.A400'
|
? 'primary.main'
|
||||||
: 'grey.600'),
|
: theme.palette.mode === 'dark'
|
||||||
}}
|
? 'grey.A400'
|
||||||
|
: 'grey.600',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Link>
|
</Link>
|
||||||
))
|
),
|
||||||
}
|
)}
|
||||||
</BottomNavContainer>
|
</BottomNavContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,16 +18,14 @@ const Image = styled('img')({
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
index: number
|
index: number;
|
||||||
image1src: string
|
image1src: string;
|
||||||
image2src: string
|
image2src: string;
|
||||||
settings: IReaderSettings
|
settings: IReaderSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DoublePage = React.forwardRef((props: IProps, ref: any) => {
|
const DoublePage = React.forwardRef((props: IProps, ref: any) => {
|
||||||
const {
|
const { image1src, image2src, index, settings } = props;
|
||||||
image1src, image2src, index, settings,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -42,14 +40,8 @@ const DoublePage = React.forwardRef((props: IProps, ref: any) => {
|
|||||||
overflowX: 'scroll',
|
overflowX: 'scroll',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image src={image1src} alt={`Page #${index}`} />
|
||||||
src={image1src}
|
<Image src={image2src} alt={`Page #${index + 1}`} />
|
||||||
alt={`Page #${index}`}
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src={image2src}
|
|
||||||
alt={`Page #${index + 1}`}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,15 +22,18 @@ function imageStyle(settings: IReaderSettings): any {
|
|||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
if (settings.readerType === 'DoubleLTR'
|
if (
|
||||||
|| settings.readerType === 'DoubleRTL'
|
settings.readerType === 'DoubleLTR' ||
|
||||||
|| settings.readerType === 'ContinuesHorizontalLTR'
|
settings.readerType === 'DoubleRTL' ||
|
||||||
|| settings.readerType === 'ContinuesHorizontalRTL') {
|
settings.readerType === 'ContinuesHorizontalLTR' ||
|
||||||
|
settings.readerType === 'ContinuesHorizontalRTL'
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
marginLeft: '7px',
|
marginLeft: '7px',
|
||||||
@@ -54,16 +57,14 @@ function imageStyle(settings: IReaderSettings): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
src: string
|
src: string;
|
||||||
index: number
|
index: number;
|
||||||
onImageLoad: () => void
|
onImageLoad: () => void;
|
||||||
settings: IReaderSettings
|
settings: IReaderSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page = React.forwardRef((props: IProps, ref: any) => {
|
const Page = React.forwardRef((props: IProps, ref: any) => {
|
||||||
const {
|
const { src, index, onImageLoad, settings } = props;
|
||||||
src, index, onImageLoad, settings,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
||||||
|
|
||||||
|
|||||||
@@ -9,27 +9,28 @@ import React from 'react';
|
|||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
settings: IReaderSettings
|
settings: IReaderSettings;
|
||||||
curPage: number
|
curPage: number;
|
||||||
pageCount: number
|
pageCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PageNumber(props: IProps) {
|
export default function PageNumber(props: IProps) {
|
||||||
const { settings, curPage, pageCount } = props;
|
const { settings, curPage, pageCount } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box
|
||||||
display: settings.showPageNumber ? 'block' : 'none',
|
sx={{
|
||||||
position: 'fixed',
|
display: settings.showPageNumber ? 'block' : 'none',
|
||||||
bottom: '50px',
|
position: 'fixed',
|
||||||
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
|
bottom: '50px',
|
||||||
padding: '2px',
|
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
|
||||||
paddingLeft: '4px',
|
padding: '2px',
|
||||||
paddingRight: '4px',
|
paddingLeft: '4px',
|
||||||
textAlign: 'center',
|
paddingRight: '4px',
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
textAlign: 'center',
|
||||||
borderRadius: '10px',
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||||
}}
|
borderRadius: '10px',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{`${curPage + 1} / ${pageCount}`}
|
{`${curPage + 1} / ${pageCount}`}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -6,20 +6,22 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { List, ListItem, ListItemText, Switch } from '@mui/material';
|
||||||
List, ListItem, ListItemText, Switch,
|
|
||||||
} from '@mui/material';
|
|
||||||
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
|
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
|
||||||
import Select from '@mui/material/Select';
|
import Select from '@mui/material/Select';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface IProps extends IReaderSettings {
|
interface IProps extends IReaderSettings {
|
||||||
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void
|
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ReaderSettingsOptions({
|
export default function ReaderSettingsOptions({
|
||||||
staticNav, loadNextOnEnding, readerType, showPageNumber, setSettingValue,
|
staticNav,
|
||||||
|
loadNextOnEnding,
|
||||||
|
readerType,
|
||||||
|
showPageNumber,
|
||||||
|
setSettingValue,
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -62,33 +64,17 @@ export default function ReaderSettingsOptions({
|
|||||||
onChange={(e) => setSettingValue('readerType', e.target.value)}
|
onChange={(e) => setSettingValue('readerType', e.target.value)}
|
||||||
sx={{ p: 0 }}
|
sx={{ p: 0 }}
|
||||||
>
|
>
|
||||||
<MenuItem value="SingleLTR">
|
<MenuItem value="SingleLTR">Single Page (LTR)</MenuItem>
|
||||||
Single Page (LTR)
|
<MenuItem value="SingleRTL">Single Page (RTL)</MenuItem>
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="SingleRTL">
|
|
||||||
Single Page (RTL)
|
|
||||||
</MenuItem>
|
|
||||||
{/* <MenuItem value="SingleVertical">
|
{/* <MenuItem value="SingleVertical">
|
||||||
Vertical(WIP)
|
Vertical(WIP)
|
||||||
</MenuItem> */}
|
</MenuItem> */}
|
||||||
<MenuItem value="DoubleLTR">
|
<MenuItem value="DoubleLTR">Double Page (LTR)</MenuItem>
|
||||||
Double Page (LTR)
|
<MenuItem value="DoubleRTL">Double Page (RTL)</MenuItem>
|
||||||
</MenuItem>
|
<MenuItem value="Webtoon">Webtoon</MenuItem>
|
||||||
<MenuItem value="DoubleRTL">
|
<MenuItem value="ContinuesVertical">Continues Vertical</MenuItem>
|
||||||
Double Page (RTL)
|
<MenuItem value="ContinuesHorizontalLTR">Horizontal (LTR)</MenuItem>
|
||||||
</MenuItem>
|
<MenuItem value="ContinuesHorizontalRTL">Horizontal (RTL)</MenuItem>
|
||||||
<MenuItem value="Webtoon">
|
|
||||||
Webtoon
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="ContinuesVertical">
|
|
||||||
Continues Vertical
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="ContinuesHorizontalLTR">
|
|
||||||
Horizontal (LTR)
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="ContinuesHorizontalRTL">
|
|
||||||
Horizontal (RTL)
|
|
||||||
</MenuItem>
|
|
||||||
</Select>
|
</Select>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ const isSinglePage = (index: number, spreadPages: boolean[]): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function DoublePagedPager(props: IReaderProps) {
|
export default function DoublePagedPager(props: IReaderProps) {
|
||||||
const {
|
const { pages, settings, setCurPage, curPage, nextChapter, prevChapter } = props;
|
||||||
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const selfRef = useRef<HTMLDivElement>(null);
|
const selfRef = useRef<HTMLDivElement>(null);
|
||||||
const pagesRef = useRef<HTMLImageElement[]>([]);
|
const pagesRef = useRef<HTMLImageElement[]>([]);
|
||||||
@@ -74,7 +72,7 @@ export default function DoublePagedPager(props: IReaderProps) {
|
|||||||
<Page
|
<Page
|
||||||
key={curPage}
|
key={curPage}
|
||||||
index={curPage}
|
index={curPage}
|
||||||
src={(pagesDisplayed.current === 1) ? pages[curPage].src : ''}
|
src={pagesDisplayed.current === 1 ? pages[curPage].src : ''}
|
||||||
onImageLoad={() => {}}
|
onImageLoad={() => {}}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
/>,
|
/>,
|
||||||
@@ -101,7 +99,7 @@ export default function DoublePagedPager(props: IReaderProps) {
|
|||||||
function nextPage() {
|
function nextPage() {
|
||||||
if (curPage < pages.length - 1) {
|
if (curPage < pages.length - 1) {
|
||||||
const nextCurPage = curPage + pagesDisplayed.current;
|
const nextCurPage = curPage + pagesDisplayed.current;
|
||||||
setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage);
|
setCurPage(nextCurPage >= pages.length ? pages.length - 1 : nextCurPage);
|
||||||
} else if (settings.loadNextOnEnding) {
|
} else if (settings.loadNextOnEnding) {
|
||||||
nextChapter();
|
nextChapter();
|
||||||
}
|
}
|
||||||
@@ -110,7 +108,7 @@ export default function DoublePagedPager(props: IReaderProps) {
|
|||||||
function prevPage() {
|
function prevPage() {
|
||||||
if (curPage > 0) {
|
if (curPage > 0) {
|
||||||
const nextCurPage = curPage - pagesToGoBack();
|
const nextCurPage = curPage - pagesToGoBack();
|
||||||
setCurPage((nextCurPage < 0) ? 0 : nextCurPage);
|
setCurPage(nextCurPage < 0 ? 0 : nextCurPage);
|
||||||
} else {
|
} else {
|
||||||
prevChapter();
|
prevChapter();
|
||||||
}
|
}
|
||||||
@@ -132,7 +130,7 @@ export default function DoublePagedPager(props: IReaderProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyboardControl(e:KeyboardEvent) {
|
function keyboardControl(e: KeyboardEvent) {
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case 'Space':
|
case 'Space':
|
||||||
e.preventDefault();
|
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) {
|
if (e.clientX > window.innerWidth / 2) {
|
||||||
goRight();
|
goRight();
|
||||||
} else {
|
} else {
|
||||||
@@ -167,9 +165,11 @@ export default function DoublePagedPager(props: IReaderProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const retryDisplay = setInterval(() => {
|
const retryDisplay = setInterval(() => {
|
||||||
const isLastPage = (curPage === pages.length - 1);
|
const isLastPage = curPage === pages.length - 1;
|
||||||
if ((!isLastPage && pageLoaded.current[curPage] && pageLoaded.current[curPage + 1])
|
if (
|
||||||
|| pageLoaded.current[curPage]) {
|
(!isLastPage && pageLoaded.current[curPage] && pageLoaded.current[curPage + 1]) ||
|
||||||
|
pageLoaded.current[curPage]
|
||||||
|
) {
|
||||||
setPagesToDisplay();
|
setPagesToDisplay();
|
||||||
displayPages();
|
displayPages();
|
||||||
clearInterval(retryDisplay);
|
clearInterval(retryDisplay);
|
||||||
@@ -189,23 +189,23 @@ export default function DoublePagedPager(props: IReaderProps) {
|
|||||||
return (
|
return (
|
||||||
<Box ref={selfRef}>
|
<Box ref={selfRef}>
|
||||||
<Box id="preload" sx={{ display: 'none' }}>
|
<Box id="preload" sx={{ display: 'none' }}>
|
||||||
{
|
{pages.map((page) => (
|
||||||
pages.map((page) => (
|
<img
|
||||||
<img
|
ref={(e: HTMLImageElement) => {
|
||||||
ref={(e:HTMLImageElement) => { pagesRef.current[page.index] = e; }}
|
pagesRef.current[page.index] = e;
|
||||||
key={`${page.index}`}
|
}}
|
||||||
src={page.src}
|
key={`${page.index}`}
|
||||||
onLoad={handleImageLoad(page.index)}
|
src={page.src}
|
||||||
alt={`${page.index}`}
|
onLoad={handleImageLoad(page.index)}
|
||||||
/>
|
alt={`${page.index}`}
|
||||||
))
|
/>
|
||||||
}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
id="display"
|
id="display"
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: (settings.readerType === 'DoubleLTR') ? 'row' : 'row-reverse',
|
flexDirection: settings.readerType === 'DoubleLTR' ? 'row' : 'row-reverse',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ const isAtEnd = () => {
|
|||||||
const isAtStart = () => window.scrollX <= 0;
|
const isAtStart = () => window.scrollX <= 0;
|
||||||
|
|
||||||
export default function HorizontalPager(props: IReaderProps) {
|
export default function HorizontalPager(props: IReaderProps) {
|
||||||
const {
|
const { pages, curPage, initialPage, settings, setCurPage, prevChapter, nextChapter } = props;
|
||||||
pages, curPage, initialPage, settings, setCurPage, prevChapter, nextChapter,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const currentPageRef = useRef(initialPage);
|
const currentPageRef = useRef(initialPage);
|
||||||
const selfRef = useRef<HTMLDivElement>(null);
|
const selfRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -78,7 +76,7 @@ export default function HorizontalPager(props: IReaderProps) {
|
|||||||
window.scrollBy(mouseXPos.current - e.pageX, 0);
|
window.scrollBy(mouseXPos.current - e.pageX, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragControl(e:MouseEvent) {
|
function dragControl(e: MouseEvent) {
|
||||||
mouseXPos.current = e.pageX;
|
mouseXPos.current = e.pageX;
|
||||||
selfRef.current?.addEventListener('mousemove', dragScreen);
|
selfRef.current?.addEventListener('mousemove', dragScreen);
|
||||||
}
|
}
|
||||||
@@ -87,7 +85,7 @@ export default function HorizontalPager(props: IReaderProps) {
|
|||||||
selfRef.current?.removeEventListener('mousemove', dragScreen);
|
selfRef.current?.removeEventListener('mousemove', dragScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickControl(e:MouseEvent) {
|
function clickControl(e: MouseEvent) {
|
||||||
if (e.clientX >= window.innerWidth * 0.85) {
|
if (e.clientX >= window.innerWidth * 0.85) {
|
||||||
goRight();
|
goRight();
|
||||||
} else if (e.clientX <= window.innerWidth * 0.15) {
|
} else if (e.clientX <= window.innerWidth * 0.15) {
|
||||||
@@ -166,8 +164,10 @@ export default function HorizontalPager(props: IReaderProps) {
|
|||||||
ref={selfRef}
|
ref={selfRef}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: (settings.readerType === 'ContinuesHorizontalLTR') ? 'row' : 'row-reverse',
|
flexDirection:
|
||||||
justifyContent: (settings.readerType === 'ContinuesHorizontalLTR') ? 'flex-start' : 'flex-end',
|
settings.readerType === 'ContinuesHorizontalLTR' ? 'row' : 'row-reverse',
|
||||||
|
justifyContent:
|
||||||
|
settings.readerType === 'ContinuesHorizontalLTR' ? 'flex-start' : 'flex-end',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
@@ -175,18 +175,18 @@ export default function HorizontalPager(props: IReaderProps) {
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{
|
{pages.map((page) => (
|
||||||
pages.map((page) => (
|
<Page
|
||||||
<Page
|
key={page.index}
|
||||||
key={page.index}
|
index={page.index}
|
||||||
index={page.index}
|
src={page.src}
|
||||||
src={page.src}
|
onImageLoad={() => {}}
|
||||||
onImageLoad={() => {}}
|
settings={settings}
|
||||||
settings={settings}
|
ref={(e: HTMLDivElement) => {
|
||||||
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
|
pagesRef.current[page.index] = e;
|
||||||
/>
|
}}
|
||||||
))
|
/>
|
||||||
}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import { Box } from '@mui/system';
|
|||||||
import Page from 'components/reader/Page';
|
import Page from 'components/reader/Page';
|
||||||
|
|
||||||
export default function PagedReader(props: IReaderProps) {
|
export default function PagedReader(props: IReaderProps) {
|
||||||
const {
|
const { pages, settings, setCurPage, curPage, nextChapter, prevChapter } = props;
|
||||||
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const selfRef = useRef<HTMLDivElement>(null);
|
const selfRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -53,7 +51,7 @@ export default function PagedReader(props: IReaderProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyboardControl(e:KeyboardEvent) {
|
function keyboardControl(e: KeyboardEvent) {
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case 'Space':
|
case 'Space':
|
||||||
e.preventDefault();
|
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) {
|
if (e.clientX > window.innerWidth / 2) {
|
||||||
goRight();
|
goRight();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ const isAtBottom = () => {
|
|||||||
const isAtTop = () => window.scrollY <= 0;
|
const isAtTop = () => window.scrollY <= 0;
|
||||||
|
|
||||||
export default function VerticalPager(props: IReaderProps) {
|
export default function VerticalPager(props: IReaderProps) {
|
||||||
const {
|
const { pages, settings, setCurPage, initialPage, nextChapter, prevChapter } = props;
|
||||||
pages, settings, setCurPage, initialPage, nextChapter, prevChapter,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const currentPageRef = useRef(initialPage);
|
const currentPageRef = useRef(initialPage);
|
||||||
const selfRef = useRef<HTMLDivElement>(null);
|
const selfRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -74,25 +72,30 @@ export default function VerticalPager(props: IReaderProps) {
|
|||||||
};
|
};
|
||||||
}, [settings.loadNextOnEnding]);
|
}, [settings.loadNextOnEnding]);
|
||||||
|
|
||||||
const go = useCallback((direction: 'up' | 'down') => {
|
const go = useCallback(
|
||||||
if (direction === 'down' && isAtBottom()) {
|
(direction: 'up' | 'down') => {
|
||||||
nextChapter();
|
if (direction === 'down' && isAtBottom()) {
|
||||||
return;
|
nextChapter();
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (direction === 'up' && isAtTop()) {
|
if (direction === 'up' && isAtTop()) {
|
||||||
prevChapter();
|
prevChapter();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.scroll({
|
window.scroll({
|
||||||
top: window.scrollY + (window.innerHeight * SCROLL_OFFSET) * (direction === 'up' ? -1 : 1),
|
top:
|
||||||
behavior: SCROLL_BEHAVIOR,
|
window.scrollY +
|
||||||
});
|
window.innerHeight * SCROLL_OFFSET * (direction === 'up' ? -1 : 1),
|
||||||
}, [nextChapter, prevChapter]);
|
behavior: SCROLL_BEHAVIOR,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[nextChapter, prevChapter],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyboard = (e:KeyboardEvent) => {
|
const handleKeyboard = (e: KeyboardEvent) => {
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case 'Space':
|
case 'Space':
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
@@ -134,18 +137,18 @@ export default function VerticalPager(props: IReaderProps) {
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => go(e.clientX > window.innerWidth / 2 ? 'down' : 'up')}
|
onClick={(e) => go(e.clientX > window.innerWidth / 2 ? 'down' : 'up')}
|
||||||
>
|
>
|
||||||
{
|
{pages.map((page) => (
|
||||||
pages.map((page) => (
|
<Page
|
||||||
<Page
|
key={page.index}
|
||||||
key={page.index}
|
index={page.index}
|
||||||
index={page.index}
|
src={page.src}
|
||||||
src={page.src}
|
onImageLoad={() => {}}
|
||||||
onImageLoad={() => {}}
|
settings={settings}
|
||||||
settings={settings}
|
ref={(e: HTMLDivElement) => {
|
||||||
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
|
pagesRef.current[page.index] = e;
|
||||||
/>
|
}}
|
||||||
))
|
/>
|
||||||
}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,17 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import {
|
import { IconButton, Menu, MenuItem, FormControlLabel, Radio } from '@mui/material';
|
||||||
IconButton, Menu, MenuItem, FormControlLabel, Radio,
|
|
||||||
} from '@mui/material';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||||
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||||
|
|
||||||
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button
|
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button
|
||||||
export default function SourceGridLayout() {
|
export default function SourceGridLayout() {
|
||||||
const { options: { SourcegridLayout }, setOptions } = useLibraryOptionsContext();
|
const {
|
||||||
|
options: { SourcegridLayout },
|
||||||
|
setOptions,
|
||||||
|
} = useLibraryOptionsContext();
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
@@ -25,10 +26,7 @@ export default function SourceGridLayout() {
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
function setGridContextOptions(
|
function setGridContextOptions(e: React.ChangeEvent<HTMLInputElement>, checked: boolean) {
|
||||||
e: React.ChangeEvent<HTMLInputElement>,
|
|
||||||
checked: boolean,
|
|
||||||
) {
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setOptions((prev: any) => ({ ...prev, SourcegridLayout: parseInt(e.target.name, 10) }));
|
setOptions((prev: any) => ({ ...prev, SourcegridLayout: parseInt(e.target.name, 10) }));
|
||||||
}
|
}
|
||||||
@@ -57,40 +55,40 @@ export default function SourceGridLayout() {
|
|||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label="Compact grid"
|
label="Compact grid"
|
||||||
value={GridLayout.Compact}
|
value={GridLayout.Compact}
|
||||||
control={(
|
control={
|
||||||
<Radio
|
<Radio
|
||||||
name={GridLayout.Compact.toString()}
|
name={GridLayout.Compact.toString()}
|
||||||
checked={
|
checked={
|
||||||
SourcegridLayout === GridLayout.Compact
|
SourcegridLayout === GridLayout.Compact ||
|
||||||
|| SourcegridLayout === undefined
|
SourcegridLayout === undefined
|
||||||
}
|
}
|
||||||
onChange={setGridContextOptions}
|
onChange={setGridContextOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleClose}>
|
<MenuItem onClick={handleClose}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label="Comfortable grid"
|
label="Comfortable grid"
|
||||||
control={(
|
control={
|
||||||
<Radio
|
<Radio
|
||||||
name={GridLayout.Comfortable.toString()}
|
name={GridLayout.Comfortable.toString()}
|
||||||
checked={SourcegridLayout === GridLayout.Comfortable}
|
checked={SourcegridLayout === GridLayout.Comfortable}
|
||||||
onChange={setGridContextOptions}
|
onChange={setGridContextOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleClose}>
|
<MenuItem onClick={handleClose}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label="List"
|
label="List"
|
||||||
control={(
|
control={
|
||||||
<Radio
|
<Radio
|
||||||
name={GridLayout.List.toString()}
|
name={GridLayout.List.toString()}
|
||||||
checked={SourcegridLayout === GridLayout.List}
|
checked={SourcegridLayout === GridLayout.List}
|
||||||
onChange={setGridContextOptions}
|
onChange={setGridContextOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -17,8 +17,14 @@ function filterManga(mangas: IMangaCard[]): IMangaCard[] {
|
|||||||
|
|
||||||
export default function SourceMangaGrid(props: IMangaGridProps) {
|
export default function SourceMangaGrid(props: IMangaGridProps) {
|
||||||
const {
|
const {
|
||||||
mangas, isLoading, hasNextPage, lastPageNum,
|
mangas,
|
||||||
setLastPageNum, message, messageExtra, gridLayout,
|
isLoading,
|
||||||
|
hasNextPage,
|
||||||
|
lastPageNum,
|
||||||
|
setLastPageNum,
|
||||||
|
message,
|
||||||
|
messageExtra,
|
||||||
|
gridLayout,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const filteredManga = filterManga(mangas);
|
const filteredManga = filterManga(mangas);
|
||||||
|
|||||||
@@ -23,33 +23,29 @@ import GroupFilter from 'components/source/filters/GroupFilter';
|
|||||||
import SeperatorFilter from 'components/source/filters/SeparatorFilter';
|
import SeperatorFilter from 'components/source/filters/SeparatorFilter';
|
||||||
|
|
||||||
interface IFilters {
|
interface IFilters {
|
||||||
sourceFilter: ISourceFilters[]
|
sourceFilter: ISourceFilters[];
|
||||||
updateFilterValue: Function
|
updateFilterValue: Function;
|
||||||
group: number | undefined
|
group: number | undefined;
|
||||||
update: any
|
update: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IFilters1 {
|
interface IFilters1 {
|
||||||
sourceFilter: ISourceFilters[]
|
sourceFilter: ISourceFilters[];
|
||||||
updateFilterValue: Function
|
updateFilterValue: Function;
|
||||||
resetFilterValue: Function
|
resetFilterValue: Function;
|
||||||
setTriggerUpdate: Function
|
setTriggerUpdate: Function;
|
||||||
setSearch: Function
|
setSearch: Function;
|
||||||
update: any
|
update: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Options({
|
export function Options({ sourceFilter, group, updateFilterValue, update }: IFilters) {
|
||||||
sourceFilter,
|
|
||||||
group,
|
|
||||||
updateFilterValue,
|
|
||||||
update,
|
|
||||||
}: IFilters) {
|
|
||||||
return (
|
return (
|
||||||
<Stack key={`filters ${group}`}>
|
<Stack key={`filters ${group}`}>
|
||||||
{ sourceFilter.map((e: ISourceFilters, index) => {
|
{sourceFilter.map((e: ISourceFilters, index) => {
|
||||||
let checkif = update.find((el: {
|
let checkif = update.find(
|
||||||
group: number | undefined; position: number;
|
(el: { group: number | undefined; position: number }) =>
|
||||||
}) => el.group === group && el.position === index);
|
el.group === group && el.position === index,
|
||||||
|
);
|
||||||
checkif = checkif ? checkif.state : checkif;
|
checkif = checkif ? checkif.state : checkif;
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case 'CheckBox':
|
case 'CheckBox':
|
||||||
@@ -57,7 +53,7 @@ export function Options({
|
|||||||
<CheckBoxFilter
|
<CheckBoxFilter
|
||||||
key={`filters ${e.filter.name}`}
|
key={`filters ${e.filter.name}`}
|
||||||
name={e.filter.name}
|
name={e.filter.name}
|
||||||
state={checkif === 'false' || e.filter.state as boolean}
|
state={checkif === 'false' || (e.filter.state as boolean)}
|
||||||
position={index}
|
position={index}
|
||||||
group={group}
|
group={group}
|
||||||
updateFilterValue={updateFilterValue}
|
updateFilterValue={updateFilterValue}
|
||||||
@@ -77,10 +73,7 @@ export function Options({
|
|||||||
);
|
);
|
||||||
case 'Header':
|
case 'Header':
|
||||||
return (
|
return (
|
||||||
<HeaderFilter
|
<HeaderFilter key={`filters ${e.filter.name}`} name={e.filter.name} />
|
||||||
key={`filters ${e.filter.name}`}
|
|
||||||
name={e.filter.name}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
case 'Select':
|
case 'Select':
|
||||||
return (
|
return (
|
||||||
@@ -88,7 +81,7 @@ export function Options({
|
|||||||
key={`filters ${e.filter.name}`}
|
key={`filters ${e.filter.name}`}
|
||||||
name={e.filter.name}
|
name={e.filter.name}
|
||||||
values={e.filter.displayValues}
|
values={e.filter.displayValues}
|
||||||
state={parseInt(checkif, 10) || e.filter.state as number}
|
state={parseInt(checkif, 10) || (e.filter.state as number)}
|
||||||
selected={e.filter.selected}
|
selected={e.filter.selected}
|
||||||
position={index}
|
position={index}
|
||||||
group={group}
|
group={group}
|
||||||
@@ -109,7 +102,7 @@ export function Options({
|
|||||||
key={`filters ${e.filter.name}`}
|
key={`filters ${e.filter.name}`}
|
||||||
name={e.filter.name}
|
name={e.filter.name}
|
||||||
values={e.filter.values}
|
values={e.filter.values}
|
||||||
state={checkif ? JSON.parse(checkif) : e.filter.state as IState}
|
state={checkif ? JSON.parse(checkif) : (e.filter.state as IState)}
|
||||||
position={index}
|
position={index}
|
||||||
group={group}
|
group={group}
|
||||||
updateFilterValue={updateFilterValue}
|
updateFilterValue={updateFilterValue}
|
||||||
@@ -121,7 +114,7 @@ export function Options({
|
|||||||
<TextFilter
|
<TextFilter
|
||||||
key={`filters ${e.filter.name}`}
|
key={`filters ${e.filter.name}`}
|
||||||
name={e.filter.name}
|
name={e.filter.name}
|
||||||
state={checkif || e.filter.state as string}
|
state={checkif || (e.filter.state as string)}
|
||||||
position={index}
|
position={index}
|
||||||
group={group}
|
group={group}
|
||||||
updateFilterValue={updateFilterValue}
|
updateFilterValue={updateFilterValue}
|
||||||
@@ -133,7 +126,7 @@ export function Options({
|
|||||||
<TriStateFilter
|
<TriStateFilter
|
||||||
key={`filters ${e.filter.name}`}
|
key={`filters ${e.filter.name}`}
|
||||||
name={e.filter.name}
|
name={e.filter.name}
|
||||||
state={parseInt(checkif, 10) || e.filter.state as number}
|
state={parseInt(checkif, 10) || (e.filter.state as number)}
|
||||||
position={index}
|
position={index}
|
||||||
group={group}
|
group={group}
|
||||||
updateFilterValue={updateFilterValue}
|
updateFilterValue={updateFilterValue}
|
||||||
@@ -141,7 +134,7 @@ export function Options({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (<Box key={`${e.filter.name}null`} />);
|
return <Box key={`${e.filter.name}null`} />;
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -181,21 +174,10 @@ export default function SourceOptions({
|
|||||||
Filter
|
Filter
|
||||||
</Fab>
|
</Fab>
|
||||||
|
|
||||||
<OptionsPanel
|
<OptionsPanel open={FilterOptions} onClose={() => setFilterOptions(false)}>
|
||||||
open={FilterOptions}
|
|
||||||
onClose={() => setFilterOptions(false)}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: 'flex', p: 2, pb: 0 }}>
|
<Box sx={{ display: 'flex', p: 2, pb: 0 }}>
|
||||||
<Button
|
<Button onClick={handleReset}>Reset</Button>
|
||||||
onClick={handleReset}
|
<Button sx={{ marginLeft: 'auto' }} variant="contained" onClick={handleSubmit}>
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
sx={{ marginLeft: 'auto' }}
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
>
|
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -9,37 +9,29 @@ import CheckboxInput from 'components/atoms/CheckboxInput';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
state: boolean
|
state: boolean;
|
||||||
name: string
|
name: string;
|
||||||
position: number
|
position: number;
|
||||||
group: number | undefined
|
group: number | undefined;
|
||||||
updateFilterValue: Function
|
updateFilterValue: Function;
|
||||||
update: any
|
update: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckBoxFilter: React.FC<Props> = (props: Props) => {
|
const CheckBoxFilter: React.FC<Props> = (props: Props) => {
|
||||||
const {
|
const { state, name, position, group, updateFilterValue, update } = props;
|
||||||
state,
|
|
||||||
name,
|
|
||||||
position,
|
|
||||||
group,
|
|
||||||
updateFilterValue,
|
|
||||||
update,
|
|
||||||
} = props;
|
|
||||||
const [val, setval] = React.useState(state);
|
const [val, setval] = React.useState(state);
|
||||||
|
|
||||||
const handleChange = (event: { target: { name: any; checked: any; }; }) => {
|
const handleChange = (event: { target: { name: any; checked: any } }) => {
|
||||||
setval(event.target.checked);
|
setval(event.target.checked);
|
||||||
const upd = update.filter((e: {
|
const upd = update.filter(
|
||||||
position: number; group: number | undefined;
|
(e: { position: number; group: number | undefined }) =>
|
||||||
}) => !(position === e.position && group === e.group));
|
!(position === e.position && group === e.group),
|
||||||
|
);
|
||||||
updateFilterValue([...upd, { position, state: event.target.checked.toString(), group }]);
|
updateFilterValue([...upd, { position, state: event.target.checked.toString(), group }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (state !== undefined) {
|
if (state !== undefined) {
|
||||||
return (
|
return <CheckboxInput label={name} checked={val} onChange={handleChange} />;
|
||||||
<CheckboxInput label={name} checked={val} onChange={handleChange} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,30 +6,22 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||||
import {
|
import { Collapse, ListItemButton, ListItemText, Stack } from '@mui/material';
|
||||||
Collapse, ListItemButton, ListItemText, Stack,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { Options } from 'components/source/SourceOptions';
|
import { Options } from 'components/source/SourceOptions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
state: ISourceFilters[]
|
state: ISourceFilters[];
|
||||||
name: string
|
name: string;
|
||||||
position: number
|
position: number;
|
||||||
updateFilterValue: Function
|
updateFilterValue: Function;
|
||||||
update: any
|
update: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupFilter: React.FC<Props> = (props: Props) => {
|
const GroupFilter: React.FC<Props> = (props: Props) => {
|
||||||
const {
|
const { state, name, position, updateFilterValue, update } = props;
|
||||||
state,
|
|
||||||
name,
|
|
||||||
position,
|
|
||||||
updateFilterValue,
|
|
||||||
update,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import { Typography } from '@mui/material';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderFilter: React.FC<Props> = ({ name }) => (<Typography key={name} sx={{ mt: 2 }} variant="subtitle2">{name}</Typography>);
|
const HeaderFilter: React.FC<Props> = ({ name }) => (
|
||||||
|
<Typography key={name} sx={{ mt: 2 }} variant="subtitle2">
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
|
||||||
export default HeaderFilter;
|
export default HeaderFilter;
|
||||||
|
|||||||
@@ -13,20 +13,20 @@ import MenuItem from '@mui/material/MenuItem';
|
|||||||
import Select from '@mui/material/Select';
|
import Select from '@mui/material/Select';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
values: any
|
values: any;
|
||||||
name: string
|
name: string;
|
||||||
state: number
|
state: number;
|
||||||
selected: Selected | undefined
|
selected: Selected | undefined;
|
||||||
position: number
|
position: number;
|
||||||
updateFilterValue: Function
|
updateFilterValue: Function;
|
||||||
group: number | undefined
|
group: number | undefined;
|
||||||
update: any
|
update: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Selected {
|
interface Selected {
|
||||||
displayname: string
|
displayname: string;
|
||||||
value: string
|
value: string;
|
||||||
_value: string
|
_value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSelect(
|
function hasSelect(
|
||||||
@@ -40,30 +40,24 @@ function hasSelect(
|
|||||||
) {
|
) {
|
||||||
const [val, setval] = React.useState(state);
|
const [val, setval] = React.useState(state);
|
||||||
if (values) {
|
if (values) {
|
||||||
const handleChange = (event: { target: { name: any; value: any; }; }) => {
|
const handleChange = (event: { target: { name: any; value: any } }) => {
|
||||||
const vall = values.map((e) => e.displayname).indexOf(`${event.target.value}`);
|
const vall = values.map((e) => e.displayname).indexOf(`${event.target.value}`);
|
||||||
setval(vall);
|
setval(vall);
|
||||||
const upd = update.filter((e: {
|
const upd = update.filter(
|
||||||
position: number; group: number | undefined;
|
(e: { position: number; group: number | undefined }) =>
|
||||||
}) => !(position === e.position && group === e.group));
|
!(position === e.position && group === e.group),
|
||||||
|
);
|
||||||
updateFilterValue([...upd, { position, state: vall.toString(), group }]);
|
updateFilterValue([...upd, { position, state: vall.toString(), group }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rett = values.map((e: Selected) => (
|
const rett = values.map((e: Selected) => (
|
||||||
<MenuItem
|
<MenuItem key={`${name} ${e.displayname}`} value={e.displayname}>
|
||||||
key={`${name} ${e.displayname}`}
|
{e.displayname}
|
||||||
value={e.displayname}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
e.displayname
|
|
||||||
}
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
));
|
));
|
||||||
return (
|
return (
|
||||||
<FormControl sx={{ my: 1 }} variant="standard">
|
<FormControl sx={{ my: 1 }} variant="standard">
|
||||||
<InputLabel>
|
<InputLabel>{name}</InputLabel>
|
||||||
{name}
|
|
||||||
</InputLabel>
|
|
||||||
<Select
|
<Select
|
||||||
name={name}
|
name={name}
|
||||||
value={values[val].displayname}
|
value={values[val].displayname}
|
||||||
@@ -90,27 +84,25 @@ function noSelect(
|
|||||||
const [val, setval] = React.useState(state);
|
const [val, setval] = React.useState(state);
|
||||||
|
|
||||||
if (values) {
|
if (values) {
|
||||||
const handleChange = (event: { target: { name: any; value: any; }; }) => {
|
const handleChange = (event: { target: { name: any; value: any } }) => {
|
||||||
const vall = values.indexOf(`${event.target.value}`);
|
const vall = values.indexOf(`${event.target.value}`);
|
||||||
setval(vall);
|
setval(vall);
|
||||||
const upd = update.filter((e: {
|
const upd = update.filter(
|
||||||
position: number; group: number | undefined;
|
(e: { position: number; group: number | undefined }) =>
|
||||||
}) => !(position === e.position && group === e.group));
|
!(position === e.position && group === e.group),
|
||||||
|
);
|
||||||
updateFilterValue([...upd, { position, state: vall.toString(), group }]);
|
updateFilterValue([...upd, { position, state: vall.toString(), group }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rett = values.map((value: string) => (<MenuItem key={`${name} ${value}`} value={value}>{value}</MenuItem>));
|
const rett = values.map((value: string) => (
|
||||||
|
<MenuItem key={`${name} ${value}`} value={value}>
|
||||||
|
{value}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
return (
|
return (
|
||||||
<FormControl sx={{ my: 1 }} variant="standard">
|
<FormControl sx={{ my: 1 }} variant="standard">
|
||||||
<InputLabel>
|
<InputLabel>{name}</InputLabel>
|
||||||
{name}
|
<Select name={name} value={values[val]} label={name} onChange={handleChange}>
|
||||||
</InputLabel>
|
|
||||||
<Select
|
|
||||||
name={name}
|
|
||||||
value={values[val]}
|
|
||||||
label={name}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
{rett}
|
{rett}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -130,26 +122,10 @@ const SelectFilter: React.FC<Props> = ({
|
|||||||
group,
|
group,
|
||||||
}) => {
|
}) => {
|
||||||
if (selected === undefined) {
|
if (selected === undefined) {
|
||||||
return noSelect(
|
return noSelect(values, name, state, position, updateFilterValue, update, group);
|
||||||
values,
|
|
||||||
name,
|
|
||||||
state,
|
|
||||||
position,
|
|
||||||
updateFilterValue,
|
|
||||||
update,
|
|
||||||
group,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasSelect(
|
return hasSelect(values, name, state, position, updateFilterValue, update, group);
|
||||||
values,
|
|
||||||
name,
|
|
||||||
state,
|
|
||||||
position,
|
|
||||||
updateFilterValue,
|
|
||||||
update,
|
|
||||||
group,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SelectFilter;
|
export default SelectFilter;
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ import { Divider } from '@mui/material';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SeparatorFilter: React.FC<Props> = ({ name }) => (<Divider key={name} sx={{ my: 1 }} textAlign="center">{name}</Divider>);
|
const SeparatorFilter: React.FC<Props> = ({ name }) => (
|
||||||
|
<Divider key={name} sx={{ my: 1 }} textAlign="center">
|
||||||
|
{name}
|
||||||
|
</Divider>
|
||||||
|
);
|
||||||
|
|
||||||
export default SeparatorFilter;
|
export default SeparatorFilter;
|
||||||
|
|||||||
@@ -6,33 +6,23 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||||
import {
|
import { Collapse, ListItemButton, ListItemText, Stack } from '@mui/material';
|
||||||
Collapse, ListItemButton, ListItemText, Stack,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import SortRadioInput from 'components/atoms/SortRadioInput';
|
import SortRadioInput from 'components/atoms/SortRadioInput';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
values: any
|
values: any;
|
||||||
name: string
|
name: string;
|
||||||
state: IState
|
state: IState;
|
||||||
position: number
|
position: number;
|
||||||
group: number | undefined
|
group: number | undefined;
|
||||||
updateFilterValue: Function
|
updateFilterValue: Function;
|
||||||
update: any
|
update: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortFilter: React.FC<Props> = (props: Props) => {
|
const SortFilter: React.FC<Props> = (props: Props) => {
|
||||||
const {
|
const { values, name, state, position, group, updateFilterValue, update } = props;
|
||||||
values,
|
|
||||||
name,
|
|
||||||
state,
|
|
||||||
position,
|
|
||||||
group,
|
|
||||||
updateFilterValue,
|
|
||||||
update,
|
|
||||||
} = props;
|
|
||||||
const [val, setval] = React.useState(state);
|
const [val, setval] = React.useState(state);
|
||||||
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
@@ -51,9 +41,10 @@ const SortFilter: React.FC<Props> = (props: Props) => {
|
|||||||
}
|
}
|
||||||
tmp.index = index;
|
tmp.index = index;
|
||||||
setval(tmp);
|
setval(tmp);
|
||||||
const upd = update.filter((e: {
|
const upd = update.filter(
|
||||||
position: number; group: number | undefined;
|
(e: { position: number; group: number | undefined }) =>
|
||||||
}) => !(position === e.position && group === e.group));
|
!(position === e.position && group === e.group),
|
||||||
|
);
|
||||||
updateFilterValue([...upd, { position, state: JSON.stringify(tmp), group }]);
|
updateFilterValue([...upd, { position, state: JSON.stringify(tmp), group }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,36 +6,28 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import {
|
import { FormControl, Input, InputAdornment, InputLabel } from '@mui/material';
|
||||||
FormControl, Input, InputAdornment, InputLabel,
|
|
||||||
} from '@mui/material';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
state: string
|
state: string;
|
||||||
name: string
|
name: string;
|
||||||
position: number
|
position: number;
|
||||||
group: number | undefined
|
group: number | undefined;
|
||||||
updateFilterValue: Function
|
updateFilterValue: Function;
|
||||||
update: any
|
update: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextFilter: React.FC<Props> = (props) => {
|
const TextFilter: React.FC<Props> = (props) => {
|
||||||
const {
|
const { state, name, position, group, updateFilterValue, update } = props;
|
||||||
state,
|
|
||||||
name,
|
|
||||||
position,
|
|
||||||
group,
|
|
||||||
updateFilterValue,
|
|
||||||
update,
|
|
||||||
} = props;
|
|
||||||
const [Search, setsearch] = React.useState(state || '');
|
const [Search, setsearch] = React.useState(state || '');
|
||||||
let typingTimer: NodeJS.Timeout;
|
let typingTimer: NodeJS.Timeout;
|
||||||
|
|
||||||
function doneTyping(e: React.ChangeEvent<HTMLInputElement>) {
|
function doneTyping(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const upd = update.filter((el: {
|
const upd = update.filter(
|
||||||
position: number; group: number | undefined;
|
(el: { position: number; group: number | undefined }) =>
|
||||||
}) => !(position === el.position && group === el.group));
|
!(position === el.position && group === el.group),
|
||||||
|
);
|
||||||
updateFilterValue([...upd, { position, state: e.target.value, group }]);
|
updateFilterValue([...upd, { position, state: e.target.value, group }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,29 +35,29 @@ const TextFilter: React.FC<Props> = (props) => {
|
|||||||
setsearch(e.target.value);
|
setsearch(e.target.value);
|
||||||
|
|
||||||
clearTimeout(typingTimer);
|
clearTimeout(typingTimer);
|
||||||
typingTimer = setTimeout(() => { doneTyping(e); }, 2500);
|
typingTimer = setTimeout(() => {
|
||||||
|
doneTyping(e);
|
||||||
|
}, 2500);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state !== undefined) {
|
if (state !== undefined) {
|
||||||
return (
|
return (
|
||||||
<FormControl sx={{ my: 1 }} variant="standard">
|
<FormControl sx={{ my: 1 }} variant="standard">
|
||||||
<InputLabel>
|
<InputLabel>{name}</InputLabel>
|
||||||
{name}
|
|
||||||
</InputLabel>
|
|
||||||
<Input
|
<Input
|
||||||
name={name}
|
name={name}
|
||||||
value={Search || ''}
|
value={Search || ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
endAdornment={(
|
endAdornment={
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (<></>);
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TextFilter;
|
export default TextFilter;
|
||||||
|
|||||||
@@ -9,37 +9,34 @@ import ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
state: number
|
state: number;
|
||||||
name: string
|
name: string;
|
||||||
position: number
|
position: number;
|
||||||
group: number | undefined
|
group: number | undefined;
|
||||||
updateFilterValue: Function
|
updateFilterValue: Function;
|
||||||
update: any
|
update: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TriStateFilter: React.FC<Props> = (props) => {
|
const TriStateFilter: React.FC<Props> = (props) => {
|
||||||
const {
|
const { state, name, position, group, updateFilterValue, update } = props;
|
||||||
state,
|
|
||||||
name,
|
|
||||||
position,
|
|
||||||
group,
|
|
||||||
updateFilterValue,
|
|
||||||
update,
|
|
||||||
} = props;
|
|
||||||
const [val, setval] = React.useState<number>(Number(state));
|
const [val, setval] = React.useState<number>(Number(state));
|
||||||
|
|
||||||
const handleChange = (checked: boolean | null | undefined) => {
|
const handleChange = (checked: boolean | null | undefined) => {
|
||||||
// eslint-disable-next-line no-nested-ternary
|
// eslint-disable-next-line no-nested-ternary
|
||||||
const newState = checked === undefined ? 0 : checked ? 1 : 2;
|
const newState = checked === undefined ? 0 : checked ? 1 : 2;
|
||||||
setval(newState);
|
setval(newState);
|
||||||
const upd = update.filter((e: {
|
const upd = update.filter(
|
||||||
position: number; group: number | undefined;
|
(e: { position: number; group: number | undefined }) =>
|
||||||
}) => !(position === e.position && group === e.group));
|
!(position === e.position && group === e.group),
|
||||||
updateFilterValue([...upd, {
|
);
|
||||||
position,
|
updateFilterValue([
|
||||||
state: newState.toString(),
|
...upd,
|
||||||
group,
|
{
|
||||||
}]);
|
position,
|
||||||
|
state: newState.toString(),
|
||||||
|
group,
|
||||||
|
},
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (state !== undefined) {
|
if (state !== undefined) {
|
||||||
@@ -51,7 +48,7 @@ const TriStateFilter: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (<></>);
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TriStateFilter;
|
export default TriStateFilter;
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ import TextField from '@mui/material/TextField';
|
|||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
export default function EditTextPreference(props: EditTextPreferenceProps) {
|
export default function EditTextPreference(props: EditTextPreferenceProps) {
|
||||||
const {
|
const { title, summary, dialogTitle, dialogMessage, currentValue, updateValue } = props;
|
||||||
title, summary, dialogTitle, dialogMessage, currentValue, updateValue,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
|
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
|
||||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
@@ -39,23 +37,13 @@ export default function EditTextPreference(props: EditTextPreferenceProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem button onClick={() => setDialogOpen(true)}>
|
||||||
button
|
<ListItemText primary={title} secondary={summary} />
|
||||||
onClick={() => setDialogOpen(true)}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
primary={title}
|
|
||||||
secondary={summary}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||||
<DialogTitle>
|
<DialogTitle>{dialogTitle}</DialogTitle>
|
||||||
{dialogTitle}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>{dialogMessage}</DialogContentText>
|
||||||
{dialogMessage}
|
|
||||||
</DialogContentText>
|
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
|
|||||||
@@ -17,18 +17,16 @@ import Radio from '@mui/material/Radio';
|
|||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
interface IListDialogProps{
|
interface IListDialogProps {
|
||||||
value: string
|
value: string;
|
||||||
open: boolean
|
open: boolean;
|
||||||
onClose: (arg0: string | null) => void
|
onClose: (arg0: string | null) => void;
|
||||||
options: string[]
|
options: string[];
|
||||||
title: string
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListDialog(props: IListDialogProps) {
|
function ListDialog(props: IListDialogProps) {
|
||||||
const {
|
const { value: valueProp, open, onClose, options, title } = props;
|
||||||
value: valueProp, open, onClose, options, title,
|
|
||||||
} = props;
|
|
||||||
const [value, setValue] = React.useState(valueProp);
|
const [value, setValue] = React.useState(valueProp);
|
||||||
const radioGroupRef = React.useRef<HTMLDivElement>(null);
|
const radioGroupRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -65,11 +63,7 @@ function ListDialog(props: IListDialogProps) {
|
|||||||
>
|
>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<RadioGroup
|
<RadioGroup ref={radioGroupRef} value={value} onChange={handleChange}>
|
||||||
ref={radioGroupRef}
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={option}
|
value={option}
|
||||||
@@ -91,9 +85,7 @@ function ListDialog(props: IListDialogProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ListPreference(props: ListPreferenceProps) {
|
export default function ListPreference(props: ListPreferenceProps) {
|
||||||
const {
|
const { title, summary, currentValue, updateValue, entryValues, entries } = props;
|
||||||
title, summary, currentValue, updateValue, entryValues, entries,
|
|
||||||
} = props;
|
|
||||||
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
|
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
|
||||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
@@ -131,10 +123,7 @@ export default function ListPreference(props: ListPreferenceProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem button onClick={() => setDialogOpen(true)}>
|
||||||
button
|
|
||||||
onClick={() => setDialogOpen(true)}
|
|
||||||
>
|
|
||||||
<ListItemText primary={title} secondary={getSummary()} />
|
<ListItemText primary={title} secondary={getSummary()} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListDialog
|
<ListDialog
|
||||||
|
|||||||
@@ -18,18 +18,16 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
|||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import cloneObject from 'util/cloneObject';
|
import cloneObject from 'util/cloneObject';
|
||||||
|
|
||||||
interface IListDialogProps{
|
interface IListDialogProps {
|
||||||
selectedValues: string[]
|
selectedValues: string[];
|
||||||
open: boolean
|
open: boolean;
|
||||||
onClose: (arg0: string[] | null) => void
|
onClose: (arg0: string[] | null) => void;
|
||||||
values: string[]
|
values: string[];
|
||||||
title: string
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListDialog(props: IListDialogProps) {
|
function ListDialog(props: IListDialogProps) {
|
||||||
const {
|
const { selectedValues: selectedValuesProp, open, onClose, values, title } = props;
|
||||||
selectedValues: selectedValuesProp, open, onClose, values, title,
|
|
||||||
} = props;
|
|
||||||
const [selectedValues, setSelectedValues] = React.useState(selectedValuesProp);
|
const [selectedValues, setSelectedValues] = React.useState(selectedValuesProp);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -56,7 +54,8 @@ function ListDialog(props: IListDialogProps) {
|
|||||||
selectedValuesClone.push(value);
|
selectedValuesClone.push(value);
|
||||||
setSelectedValues(selectedValuesClone);
|
setSelectedValues(selectedValuesClone);
|
||||||
}
|
}
|
||||||
} else if (hasEntry) { // not checked and has entry
|
} else if (hasEntry) {
|
||||||
|
// not checked and has entry
|
||||||
const selectedValuesClone = cloneObject(selectedValues) as string[];
|
const selectedValuesClone = cloneObject(selectedValues) as string[];
|
||||||
const index = selectedValuesClone.indexOf(value);
|
const index = selectedValuesClone.indexOf(value);
|
||||||
selectedValuesClone.splice(index, 1);
|
selectedValuesClone.splice(index, 1);
|
||||||
@@ -75,7 +74,7 @@ function ListDialog(props: IListDialogProps) {
|
|||||||
<FormGroup>
|
<FormGroup>
|
||||||
{values.map((value) => (
|
{values.map((value) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={(
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedValues.some(
|
checked={selectedValues.some(
|
||||||
(selectedValue) => value === selectedValue,
|
(selectedValue) => value === selectedValue,
|
||||||
@@ -83,7 +82,7 @@ function ListDialog(props: IListDialogProps) {
|
|||||||
onChange={(e) => handleChange(e, value)}
|
onChange={(e) => handleChange(e, value)}
|
||||||
color="default"
|
color="default"
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
label={value}
|
label={value}
|
||||||
key={value}
|
key={value}
|
||||||
/>
|
/>
|
||||||
@@ -101,9 +100,7 @@ function ListDialog(props: IListDialogProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MultiSelectListPreference(props: MultiSelectListPreferenceProps) {
|
export default function MultiSelectListPreference(props: MultiSelectListPreferenceProps) {
|
||||||
const {
|
const { title, summary, currentValue, updateValue, entryValues, entries } = props;
|
||||||
title, summary, currentValue, updateValue, entryValues, entries,
|
|
||||||
} = props;
|
|
||||||
const [internalCurrentValue, setInternalCurrentValue] = useState<string[]>(currentValue);
|
const [internalCurrentValue, setInternalCurrentValue] = useState<string[]>(currentValue);
|
||||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
@@ -111,15 +108,17 @@ export default function MultiSelectListPreference(props: MultiSelectListPreferen
|
|||||||
setInternalCurrentValue(currentValue);
|
setInternalCurrentValue(currentValue);
|
||||||
}, [currentValue]);
|
}, [currentValue]);
|
||||||
|
|
||||||
const findEntriesOf = (values: string[]) => values.map((value) => {
|
const findEntriesOf = (values: string[]) =>
|
||||||
const idx = entryValues.indexOf(value);
|
values.map((value) => {
|
||||||
return entries[idx];
|
const idx = entryValues.indexOf(value);
|
||||||
});
|
return entries[idx];
|
||||||
|
});
|
||||||
|
|
||||||
const findEntryValuesOf = (values: string[]) => values.map((value) => {
|
const findEntryValuesOf = (values: string[]) =>
|
||||||
const idx = entries.indexOf(value);
|
values.map((value) => {
|
||||||
return entryValues[idx];
|
const idx = entries.indexOf(value);
|
||||||
});
|
return entryValues[idx];
|
||||||
|
});
|
||||||
|
|
||||||
const getSummary = () => summary;
|
const getSummary = () => summary;
|
||||||
|
|
||||||
@@ -137,10 +136,7 @@ export default function MultiSelectListPreference(props: MultiSelectListPreferen
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem button onClick={() => setDialogOpen(true)}>
|
||||||
button
|
|
||||||
onClick={() => setDialogOpen(true)}
|
|
||||||
>
|
|
||||||
<ListItemText primary={title} secondary={getSummary()} />
|
<ListItemText primary={title} secondary={getSummary()} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListDialog
|
<ListDialog
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import Switch from '@mui/material/Switch';
|
|||||||
import Checkbox from '@mui/material/Checkbox';
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
|
||||||
function getTwoStateType(type: 'Checkbox' | 'Switch') {
|
function getTwoStateType(type: 'Checkbox' | 'Switch') {
|
||||||
if (type === 'Switch') { return Switch; }
|
if (type === 'Switch') {
|
||||||
|
return Switch;
|
||||||
|
}
|
||||||
return Checkbox;
|
return Checkbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TwoSatePreference(props: TwoStatePreferenceProps) {
|
function TwoSatePreference(props: TwoStatePreferenceProps) {
|
||||||
const {
|
const { title, summary, currentValue, updateValue, type } = props;
|
||||||
title, summary, currentValue, updateValue, type,
|
|
||||||
} = props;
|
|
||||||
const [internalCurrentValue, setInternalCurrentValue] = useState<boolean>(currentValue);
|
const [internalCurrentValue, setInternalCurrentValue] = useState<boolean>(currentValue);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,17 +31,16 @@ function TwoSatePreference(props: TwoStatePreferenceProps) {
|
|||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary={title} secondary={summary} />
|
<ListItemText primary={title} secondary={summary} />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
{React.createElement(getTwoStateType(type),
|
{React.createElement(getTwoStateType(type), {
|
||||||
{
|
edge: 'end',
|
||||||
edge: 'end',
|
checked: internalCurrentValue,
|
||||||
checked: internalCurrentValue,
|
onChange: () => {
|
||||||
onChange: () => {
|
updateValue(!currentValue);
|
||||||
updateValue(!currentValue);
|
|
||||||
|
|
||||||
// appear smooth
|
// appear smooth
|
||||||
setInternalCurrentValue(!currentValue);
|
setInternalCurrentValue(!currentValue);
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
@@ -50,6 +49,7 @@ function TwoSatePreference(props: TwoStatePreferenceProps) {
|
|||||||
export function CheckBoxPreference(props: CheckBoxPreferenceProps) {
|
export function CheckBoxPreference(props: CheckBoxPreferenceProps) {
|
||||||
return <TwoSatePreference {...props} type="Checkbox" />;
|
return <TwoSatePreference {...props} type="Checkbox" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) {
|
export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) {
|
||||||
return <TwoSatePreference {...props} type="Switch" />;
|
return <TwoSatePreference {...props} type="Switch" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import CancelIcon from '@mui/icons-material/Cancel';
|
|||||||
import { useQueryParam, StringParam } from 'use-query-params';
|
import { useQueryParam, StringParam } from 'use-query-params';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
autoOpen?: boolean
|
autoOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
@@ -29,11 +29,14 @@ const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
|
|||||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
setQuery(e.target.value === '' ? undefined : e.target.value);
|
setQuery(e.target.value === '' ? undefined : e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelSearch = () => {
|
const cancelSearch = () => {
|
||||||
setQuery(null);
|
setQuery(null);
|
||||||
setSearchOpen(false);
|
setSearchOpen(false);
|
||||||
};
|
};
|
||||||
const handleBlur = () => { if (!query) setSearchOpen(false); };
|
const handleBlur = () => {
|
||||||
|
if (!query) setSearchOpen(false);
|
||||||
|
};
|
||||||
const openSearch = () => {
|
const openSearch = () => {
|
||||||
setSearchOpen(true);
|
setSearchOpen(true);
|
||||||
// Put Focus Action at the end of the Callstack so Input actually exists on the dom
|
// Put Focus Action at the end of the Callstack so Input actually exists on the dom
|
||||||
@@ -43,7 +46,7 @@ const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchShortcut = (e: KeyboardEvent) => {
|
const handleSearchShortcut = (e: KeyboardEvent) => {
|
||||||
if ((e.code === 'F3') || (e.ctrlKey && e.code === 'KeyF')) {
|
if (e.code === 'F3' || (e.ctrlKey && e.code === 'KeyF')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openSearch();
|
openSearch();
|
||||||
}
|
}
|
||||||
@@ -65,26 +68,23 @@ const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{searchOpen
|
{searchOpen ? (
|
||||||
? (
|
<Input
|
||||||
<Input
|
value={query || ''}
|
||||||
value={query || ''}
|
onChange={handleChange}
|
||||||
onChange={handleChange}
|
onBlur={handleBlur}
|
||||||
onBlur={handleBlur}
|
inputRef={inputRef}
|
||||||
inputRef={inputRef}
|
endAdornment={
|
||||||
endAdornment={(
|
<IconButton onClick={cancelSearch}>
|
||||||
<IconButton
|
<CancelIcon />
|
||||||
onClick={cancelSearch}
|
</IconButton>
|
||||||
>
|
}
|
||||||
<CancelIcon />
|
/>
|
||||||
</IconButton>
|
) : (
|
||||||
)}
|
<IconButton onClick={openSearch}>
|
||||||
/>
|
<SearchIcon />
|
||||||
) : (
|
</IconButton>
|
||||||
<IconButton onClick={openSearch}>
|
)}
|
||||||
<SearchIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import createSvgIcon from '@mui/material/utils/createSvgIcon';
|
import createSvgIcon from '@mui/material/utils/createSvgIcon';
|
||||||
|
|
||||||
const d = 'M 11 1 C 9.3550302 1 8 2.3550302 8 4 L 4 4 C 2.9069372 4 2 4.9069372 2 6 L 2 12 L 4 12 C 4.5650302 12 5 12.43497 5 13 C 5 13.56503 4.5650302 14 4 14 L 2 14 L 2 20 C 2 21.093063 2.9069372 22 4 22 L 10 22 L 10 20 C 10 19.43497 10.43497 19 11 19 C 11.56503 19 12 19.43497 12 20 L 12 22 L 18 22 C 19.093063 22 20 21.093063 20 20 L 20 16 C 21.64497 16 23 14.64497 23 13 C 23 11.35503 21.64497 10 20 10 L 20 6 C 20 4.9069372 19.093063 4 18 4 L 14 4 C 14 2.3550302 12.64497 1 11 1 z M 11 3 C 11.56503 3 12 3.4349698 12 4 L 12 6 L 18 6 L 18 12 L 20 12 C 20.56503 12 21 12.43497 21 13 C 21 13.56503 20.56503 14 20 14 L 18 14 L 18 20 L 14 20 C 14 18.35503 12.64497 17 11 17 C 9.3550302 17 8 18.35503 8 20 L 4 20 L 4 16 C 5.6449698 16 7 14.64497 7 13 C 7 11.35503 5.6449698 10 4 10 L 4 6 L 10 6 L 10 4 C 10 3.4349698 10.43497 3 11 3 z';
|
const d =
|
||||||
|
'M 11 1 C 9.3550302 1 8 2.3550302 8 4 L 4 4 C 2.9069372 4 2 4.9069372 2 6 L 2 12 L 4 12 C 4.5650302 12 5 12.43497 5 13 C 5 13.56503 4.5650302 14 4 14 L 2 14 L 2 20 C 2 21.093063 2.9069372 22 4 22 L 10 22 L 10 20 C 10 19.43497 10.43497 19 11 19 C 11.56503 19 12 19.43497 12 20 L 12 22 L 18 22 C 19.093063 22 20 21.093063 20 20 L 20 16 C 21.64497 16 23 14.64497 23 13 C 23 11.35503 21.64497 10 20 10 L 20 6 C 20 4.9069372 19.093063 4 18 4 L 14 4 C 14 2.3550302 12.64497 1 11 1 z M 11 3 C 11.56503 3 12 3.4349698 12 4 L 12 6 L 18 6 L 18 12 L 20 12 C 20.56503 12 21 12.43497 21 13 C 21 13.56503 20.56503 14 20 14 L 18 14 L 18 20 L 14 20 C 14 18.35503 12.64497 17 11 17 C 9.3550302 17 8 18.35503 8 20 L 4 20 L 4 16 C 5.6449698 16 7 14.64497 7 13 C 7 11.35503 5.6449698 10 4 10 L 4 6 L 10 6 L 10 4 C 10 3.4349698 10.43497 3 11 3 z';
|
||||||
|
|
||||||
const icon = createSvgIcon(<path d={d} />, 'CustomExtensionOutlined');
|
const icon = createSvgIcon(<path d={d} />, 'CustomExtensionOutlined');
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,7 @@ import { useTheme } from '@mui/material/styles';
|
|||||||
import { useMediaQuery } from '@mui/material';
|
import { useMediaQuery } from '@mui/material';
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
|
|
||||||
const ERROR_FACES = [
|
const ERROR_FACES = ['(・o・;)', 'Σ(ಠ_ಠ)', 'ಥ_ಥ', '(˘・_・˘)', '(; ̄Д ̄)', '(・Д・。'];
|
||||||
'(・o・;)',
|
|
||||||
'Σ(ಠ_ಠ)',
|
|
||||||
'ಥ_ಥ',
|
|
||||||
'(˘・_・˘)',
|
|
||||||
'(; ̄Д ̄)',
|
|
||||||
'(・Д・。',
|
|
||||||
];
|
|
||||||
|
|
||||||
function getRandomErrorFace() {
|
function getRandomErrorFace() {
|
||||||
const randIndex = Math.floor(Math.random() * ERROR_FACES.length);
|
const randIndex = Math.floor(Math.random() * ERROR_FACES.length);
|
||||||
@@ -27,8 +20,8 @@ function getRandomErrorFace() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
message: string
|
message: string;
|
||||||
messageExtra?: JSX.Element
|
messageExtra?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmptyView({ message, messageExtra }: IProps) {
|
export default function EmptyView({ message, messageExtra }: IProps) {
|
||||||
@@ -38,20 +31,19 @@ export default function EmptyView({ message, messageExtra }: IProps) {
|
|||||||
const errorFace = useMemo(() => getRandomErrorFace(), []);
|
const errorFace = useMemo(() => getRandomErrorFace(), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box
|
||||||
position: 'absolute',
|
sx={{
|
||||||
left: `calc(50% + ${isMobileWidth ? '0px' : theme.spacing(8 / 2)})`,
|
position: 'absolute',
|
||||||
top: '50%',
|
left: `calc(50% + ${isMobileWidth ? '0px' : theme.spacing(8 / 2)})`,
|
||||||
transform: 'translate(-50%, -50%)',
|
top: '50%',
|
||||||
textAlign: 'center',
|
transform: 'translate(-50%, -50%)',
|
||||||
}}
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h3" gutterBottom>
|
<Typography variant="h3" gutterBottom>
|
||||||
{errorFace}
|
{errorFace}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h5">
|
<Typography variant="h5">{message}</Typography>
|
||||||
{message}
|
|
||||||
</Typography>
|
|
||||||
{messageExtra}
|
{messageExtra}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,16 +10,14 @@ import CircularProgress from '@mui/material/CircularProgress';
|
|||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
shouldRender?: boolean | (() => boolean)
|
shouldRender?: boolean | (() => boolean);
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
component?: string | React.FunctionComponent<any> | React.ComponentClass<any, any>
|
component?: string | React.FunctionComponent<any> | React.ComponentClass<any, any>;
|
||||||
componentProps?: any
|
componentProps?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LoadingPlaceholder(props: IProps) {
|
export default function LoadingPlaceholder(props: IProps) {
|
||||||
const {
|
const { children, shouldRender, component, componentProps } = props;
|
||||||
children, shouldRender, component, componentProps,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
let condition = true;
|
let condition = true;
|
||||||
if (shouldRender !== undefined) {
|
if (shouldRender !== undefined) {
|
||||||
@@ -32,20 +30,17 @@ export default function LoadingPlaceholder(props: IProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
return (
|
return <>{children}</>;
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box
|
||||||
margin: '10px auto',
|
sx={{
|
||||||
display: 'flex',
|
margin: '10px auto',
|
||||||
justifyContent: 'center',
|
display: 'flex',
|
||||||
}}
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress thickness={5} />
|
<CircularProgress thickness={5} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -12,21 +12,19 @@ import { Theme } from '@mui/system/createTheme';
|
|||||||
import { SxProps } from '@mui/system/styleFunctionSx';
|
import { SxProps } from '@mui/system/styleFunctionSx';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
src: string
|
src: string;
|
||||||
alt: string
|
alt: string;
|
||||||
|
|
||||||
imgRef?: React.RefObject<HTMLImageElement>
|
imgRef?: React.RefObject<HTMLImageElement>;
|
||||||
|
|
||||||
spinnerStyle?: SxProps<Theme>
|
spinnerStyle?: SxProps<Theme>;
|
||||||
imgStyle?: CSSProperties
|
imgStyle?: CSSProperties;
|
||||||
|
|
||||||
onImageLoad?: () => void
|
onImageLoad?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SpinnerImage(props: IProps) {
|
export default function SpinnerImage(props: IProps) {
|
||||||
const {
|
const { src, alt, onImageLoad, imgRef, spinnerStyle, imgStyle } = props;
|
||||||
src, alt, onImageLoad, imgRef, spinnerStyle, imgStyle,
|
|
||||||
} = props;
|
|
||||||
const [imageSrc, setImagsrc] = useState<string>('');
|
const [imageSrc, setImagsrc] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,14 +59,7 @@ export default function SpinnerImage(props: IProps) {
|
|||||||
return <Box sx={spinnerStyle} />;
|
return <Box sx={spinnerStyle} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <img style={imgStyle} ref={imgRef} src={imageSrc} alt={alt} />;
|
||||||
<img
|
|
||||||
style={imgStyle}
|
|
||||||
ref={imgRef}
|
|
||||||
src={imageSrc}
|
|
||||||
alt={alt}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SpinnerImage.defaultProps = {
|
SpinnerImage.defaultProps = {
|
||||||
|
|||||||
@@ -14,16 +14,10 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TabPanel(props: IProps) {
|
export default function TabPanel(props: IProps) {
|
||||||
const {
|
const { children, index, currentIndex } = props;
|
||||||
children, index, currentIndex,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div role="tabpanel" hidden={index !== currentIndex} id={`simple-tabpanel-${index}`}>
|
||||||
role="tabpanel"
|
|
||||||
hidden={index !== currentIndex}
|
|
||||||
id={`simple-tabpanel-${index}`}
|
|
||||||
>
|
|
||||||
{currentIndex === index && children}
|
{currentIndex === index && children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ function Transition(props: SlideProps) {
|
|||||||
return <Slide {...props} direction="up" />;
|
return <Slide {...props} direction="up" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IToastProps{
|
interface IToastProps {
|
||||||
message: string
|
message: string;
|
||||||
severity: Severity
|
severity: Severity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Toast(props: IToastProps) {
|
export function Toast(props: IToastProps) {
|
||||||
@@ -61,15 +61,20 @@ export default function makeToast(message: string, severity: Severity) {
|
|||||||
setTimeout(() => removeToast(container.id), 3500);
|
setTimeout(() => removeToast(container.id), 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeToaster(
|
export function makeToaster([toasts, setToasts]: [
|
||||||
[toasts, setToasts] : [React.ReactElement[],
|
React.ReactElement[],
|
||||||
(arg0: React.ReactElement[]) => void],
|
(arg0: React.ReactElement[]) => void,
|
||||||
): [React.ReactElement[], ((message: string, severity: Severity) => void)] {
|
]): [React.ReactElement[], (message: string, severity: Severity) => void] {
|
||||||
return [toasts, (message: string, severity: Severity) => {
|
return [
|
||||||
setToasts([<Toast
|
toasts,
|
||||||
key={Math.floor(Math.random() * 1000) + 1}
|
(message: string, severity: Severity) => {
|
||||||
message={message}
|
setToasts([
|
||||||
severity={severity}
|
<Toast
|
||||||
/>]);
|
key={Math.floor(Math.random() * 1000) + 1}
|
||||||
}];
|
message={message}
|
||||||
|
severity={severity}
|
||||||
|
/>,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export const pluralize = (count: number, input: string | { one: string, many: string }) => {
|
export const pluralize = (count: number, input: string | { one: string; many: string }) => {
|
||||||
if (typeof input === 'string') {
|
if (typeof input === 'string') {
|
||||||
return `${input}${count === 1 ? '' : 's'}`;
|
return `${input}${count === 1 ? '' : 's'}`;
|
||||||
}
|
}
|
||||||
return input[count === 1 ? 'one' : 'many'];
|
return input[count === 1 ? 'one' : 'many'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const interpolate = (count: number, input: { one: string, many: string }) => {
|
export const interpolate = (count: number, input: { one: string; many: string }) => {
|
||||||
const text = count === 1 ? input.one : input.many;
|
const text = count === 1 ? input.one : input.many;
|
||||||
return text.replaceAll('%count%', count.toString());
|
return text.replaceAll('%count%', count.toString());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
|||||||
import DragHandle from '@mui/icons-material/DragHandle';
|
import DragHandle from '@mui/icons-material/DragHandle';
|
||||||
import PauseIcon from '@mui/icons-material/Pause';
|
import PauseIcon from '@mui/icons-material/Pause';
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
import {
|
import { Card, CardActionArea, Stack } from '@mui/material';
|
||||||
Card, CardActionArea, Stack,
|
|
||||||
} from '@mui/material';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import EmptyView from 'components/util/EmptyView';
|
import EmptyView from 'components/util/EmptyView';
|
||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import {
|
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||||
DragDropContext, Draggable, Droppable, DropResult,
|
|
||||||
} from 'react-beautiful-dnd';
|
|
||||||
import client from 'util/client';
|
import client from 'util/client';
|
||||||
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
@@ -56,8 +52,7 @@ const DownloadQueue: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = (result: DropResult) => {};
|
||||||
};
|
|
||||||
|
|
||||||
if (queue.length === 0) {
|
if (queue.length === 0) {
|
||||||
return <EmptyView message="No downloads" />;
|
return <EmptyView message="No downloads" />;
|
||||||
@@ -65,14 +60,15 @@ const DownloadQueue: React.FC = () => {
|
|||||||
|
|
||||||
const handleDelete = (chapter: IChapter) => {
|
const handleDelete = (chapter: IChapter) => {
|
||||||
// required to stop before deleting otherwise the download kept going. Server issue?
|
// required to stop before deleting otherwise the download kept going. Server issue?
|
||||||
client.get('/api/v1/downloads/stop')
|
client.get('/api/v1/downloads/stop').then(() =>
|
||||||
.then(() => Promise.all([
|
Promise.all([
|
||||||
// remove from download queue
|
// remove from download queue
|
||||||
client.delete(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`),
|
client.delete(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`),
|
||||||
// delete partial download, should be handle server side?
|
// delete partial download, should be handle server side?
|
||||||
// bug: The folder and the last image downloaded are not deleted
|
// bug: The folder and the last image downloaded are not deleted
|
||||||
client.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`),
|
client.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`),
|
||||||
]));
|
]),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -101,22 +97,38 @@ const DownloadQueue: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: snapshot.isDragging ? 'custom.light' : undefined,
|
backgroundColor: snapshot.isDragging
|
||||||
|
? 'custom.light'
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardActionArea
|
<CardActionArea
|
||||||
component={Link}
|
component={Link}
|
||||||
to={{ pathname: `/manga/${item.chapter.mangaId}`, state: { backLink: BACK } }}
|
to={{
|
||||||
sx={{ display: 'flex', alignItems: 'center', p: 1 }}
|
pathname: `/manga/${item.chapter.mangaId}`,
|
||||||
|
state: { backLink: BACK },
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<IconButton sx={{ pointerEvents: 'none' }}>
|
<IconButton sx={{ pointerEvents: 'none' }}>
|
||||||
<DragHandle />
|
<DragHandle />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Stack sx={{ flex: 1, ml: 1 }} direction="column">
|
<Stack
|
||||||
|
sx={{ flex: 1, ml: 1 }}
|
||||||
|
direction="column"
|
||||||
|
>
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
{item.manga.title}
|
{item.manga.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" display="block" gutterBottom>
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
display="block"
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
{item.chapter.name}
|
{item.chapter.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, {
|
import React, { useContext, useEffect, useState, useMemo, useRef } from 'react';
|
||||||
useContext, useEffect, useState, useMemo, useRef,
|
|
||||||
} from 'react';
|
|
||||||
import { fromEvent } from 'file-selector';
|
import { fromEvent } from 'file-selector';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
@@ -30,7 +28,7 @@ const EXTENSIONS = 1;
|
|||||||
const allLangs: string[] = [];
|
const allLangs: string[] = [];
|
||||||
|
|
||||||
interface GroupedExtension {
|
interface GroupedExtension {
|
||||||
[key: string]: IExtension[]
|
[key: string]: IExtension[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupExtensions(extensions: IExtension[]) {
|
function groupExtensions(extensions: IExtension[]) {
|
||||||
@@ -39,7 +37,9 @@ function groupExtensions(extensions: IExtension[]) {
|
|||||||
extensions.forEach((extension) => {
|
extensions.forEach((extension) => {
|
||||||
if (sortedExtenions[extension.lang] === undefined) {
|
if (sortedExtenions[extension.lang] === undefined) {
|
||||||
sortedExtenions[extension.lang] = [];
|
sortedExtenions[extension.lang] = [];
|
||||||
if (extension.lang !== 'all') { allLangs.push(extension.lang); }
|
if (extension.lang !== 'all') {
|
||||||
|
allLangs.push(extension.lang);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (extension.installed) {
|
if (extension.installed) {
|
||||||
if (extension.hasUpdate) {
|
if (extension.hasUpdate) {
|
||||||
@@ -67,7 +67,10 @@ function groupExtensions(extensions: IExtension[]) {
|
|||||||
export default function MangaExtensions() {
|
export default function MangaExtensions() {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>(
|
||||||
|
'shownExtensionLangs',
|
||||||
|
extensionDefaultLangs(),
|
||||||
|
);
|
||||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
@@ -78,10 +81,7 @@ export default function MangaExtensions() {
|
|||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<AppbarSearch />
|
<AppbarSearch />
|
||||||
<IconButton
|
<IconButton onClick={() => inputRef.current?.click()} size="large">
|
||||||
onClick={() => inputRef.current?.click()}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<LangSelect
|
<LangSelect
|
||||||
@@ -93,17 +93,33 @@ export default function MangaExtensions() {
|
|||||||
);
|
);
|
||||||
}, [shownLangs]);
|
}, [shownLangs]);
|
||||||
|
|
||||||
const { data: allExtensions, mutate, loading } = useQuery<IExtension[]>('/api/v1/extension/list');
|
const {
|
||||||
|
data: allExtensions,
|
||||||
|
mutate,
|
||||||
|
loading,
|
||||||
|
} = useQuery<IExtension[]>('/api/v1/extension/list');
|
||||||
|
|
||||||
const filteredExtensions = useMemo(() => (allExtensions ?? []).filter((ext) => {
|
const filteredExtensions = useMemo(
|
||||||
const nsfwFilter = showNsfw || !ext.isNsfw;
|
() =>
|
||||||
if (!query) return nsfwFilter;
|
(allExtensions ?? []).filter((ext) => {
|
||||||
return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase());
|
const nsfwFilter = showNsfw || !ext.isNsfw;
|
||||||
}), [allExtensions, showNsfw, query]);
|
if (!query) return nsfwFilter;
|
||||||
|
return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase());
|
||||||
|
}),
|
||||||
|
[allExtensions, showNsfw, query],
|
||||||
|
);
|
||||||
|
|
||||||
const groupedExtensions = useMemo(() => groupExtensions(filteredExtensions)
|
const groupedExtensions = useMemo(
|
||||||
.filter((group) => group[EXTENSIONS].length > 0)
|
() =>
|
||||||
.filter((group) => ['installed', 'updates pending', 'all', ...shownLangs].includes(group[LANGUAGE])), [shownLangs, filteredExtensions]);
|
groupExtensions(filteredExtensions)
|
||||||
|
.filter((group) => group[EXTENSIONS].length > 0)
|
||||||
|
.filter((group) =>
|
||||||
|
['installed', 'updates pending', 'all', ...shownLangs].includes(
|
||||||
|
group[LANGUAGE],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[shownLangs, filteredExtensions],
|
||||||
|
);
|
||||||
|
|
||||||
const flatRenderItems: (IExtension | string)[] = groupedExtensions.flat(2);
|
const flatRenderItems: (IExtension | string)[] = groupedExtensions.flat(2);
|
||||||
|
|
||||||
@@ -119,8 +135,10 @@ export default function MangaExtensions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeToast('Installing Extension File....', 'info');
|
makeToast('Installing Extension File....', 'info');
|
||||||
client.post('/api/v1/extension/install',
|
client
|
||||||
formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
.post('/api/v1/extension/install', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
makeToast('Installed extension successfully!', 'success');
|
makeToast('Installed extension successfully!', 'success');
|
||||||
mutate();
|
mutate();
|
||||||
@@ -175,7 +193,7 @@ export default function MangaExtensions() {
|
|||||||
}}
|
}}
|
||||||
totalCount={flatRenderItems.length}
|
totalCount={flatRenderItems.length}
|
||||||
itemContent={(index) => {
|
itemContent={(index) => {
|
||||||
if (typeof (flatRenderItems[index]) === 'string') {
|
if (typeof flatRenderItems[index] === 'string') {
|
||||||
const item = flatRenderItems[index] as string;
|
const item = flatRenderItems[index] as string;
|
||||||
return (
|
return (
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -6,9 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import { Tab, Tabs } from '@mui/material';
|
import { Tab, Tabs } from '@mui/material';
|
||||||
import React, {
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
useContext, useEffect, useState,
|
|
||||||
} from 'react';
|
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import EmptyView from 'components/util/EmptyView';
|
import EmptyView from 'components/util/EmptyView';
|
||||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||||
@@ -62,7 +60,12 @@ export default function Library() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (tabsError != null) {
|
if (tabsError != null) {
|
||||||
return <EmptyView message="Could not load categories" messageExtra={tabsError?.message ?? tabsError} />;
|
return (
|
||||||
|
<EmptyView
|
||||||
|
message="Could not load categories"
|
||||||
|
messageExtra={tabsError?.message ?? tabsError}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -109,11 +112,13 @@ export default function Library() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<TabPanel key={tab.order} index={tab.order} currentIndex={activeTab.order}>
|
<TabPanel key={tab.order} index={tab.order} currentIndex={activeTab.order}>
|
||||||
{tab === activeTab && (mangaError
|
{tab === activeTab &&
|
||||||
? (
|
(mangaError ? (
|
||||||
<EmptyView message="Could not load manga" messageExtra={mangaError?.message ?? mangaError} />
|
<EmptyView
|
||||||
)
|
message="Could not load manga"
|
||||||
: (
|
messageExtra={mangaError?.message ?? mangaError}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<LibraryMangaGrid
|
<LibraryMangaGrid
|
||||||
mangas={mangas}
|
mangas={mangas}
|
||||||
lastLibraryUpdate={lastLibraryUpdate}
|
lastLibraryUpdate={lastLibraryUpdate}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import { Warning } from '@mui/icons-material';
|
import { Warning } from '@mui/icons-material';
|
||||||
import {
|
import { CircularProgress, IconButton, Stack, Tooltip } from '@mui/material';
|
||||||
CircularProgress, IconButton, Stack, Tooltip,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import NavbarContext, { useSetDefaultBackTo } from 'components/context/NavbarContext';
|
import NavbarContext, { useSetDefaultBackTo } from 'components/context/NavbarContext';
|
||||||
import ChapterList from 'components/manga/ChapterList';
|
import ChapterList from 'components/manga/ChapterList';
|
||||||
@@ -18,9 +16,7 @@ import MangaToolbarMenu from 'components/manga/MangaToolbarMenu';
|
|||||||
import { NavbarToolbar } from 'components/navbar/DefaultNavBar';
|
import { NavbarToolbar } from 'components/navbar/DefaultNavBar';
|
||||||
import EmptyView from 'components/util/EmptyView';
|
import EmptyView from 'components/util/EmptyView';
|
||||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||||
import React, {
|
import React, { useContext, useEffect, useRef } from 'react';
|
||||||
useContext, useEffect, useRef,
|
|
||||||
} from 'react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useQuery } from 'util/client';
|
import { useQuery } from 'util/client';
|
||||||
|
|
||||||
@@ -32,12 +28,20 @@ const Manga: React.FC = () => {
|
|||||||
const autofetchedRef = useRef(false);
|
const autofetchedRef = useRef(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: manga, error, loading, isValidating, mutate,
|
data: manga,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
isValidating,
|
||||||
|
mutate,
|
||||||
} = useQuery<IManga>(`/api/v1/manga/${id}/?onlineFetch=false`);
|
} = useQuery<IManga>(`/api/v1/manga/${id}/?onlineFetch=false`);
|
||||||
|
|
||||||
const [refresh, { loading: refreshing }] = useRefreshManga(id);
|
const [refresh, { loading: refreshing }] = useRefreshManga(id);
|
||||||
|
|
||||||
useSetDefaultBackTo(manga?.inLibrary === false && manga.sourceId != null ? `/sources/${manga.sourceId}/popular` : '/library');
|
useSetDefaultBackTo(
|
||||||
|
manga?.inLibrary === false && manga.sourceId != null
|
||||||
|
? `/sources/${manga.sourceId}/popular`
|
||||||
|
: '/library',
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Automatically fetch manga from source if data is older then 24 hours
|
// Automatically fetch manga from source if data is older then 24 hours
|
||||||
@@ -45,9 +49,9 @@ const Manga: React.FC = () => {
|
|||||||
// not update age for some reason (ie. error on source side)
|
// not update age for some reason (ie. error on source side)
|
||||||
if (manga == null) return;
|
if (manga == null) return;
|
||||||
if (
|
if (
|
||||||
manga.inLibrary
|
manga.inLibrary &&
|
||||||
&& (manga.age > AUTOFETCH_AGE || manga.chaptersAge > AUTOFETCH_AGE)
|
(manga.age > AUTOFETCH_AGE || manga.chaptersAge > AUTOFETCH_AGE) &&
|
||||||
&& autofetchedRef.current === false
|
autofetchedRef.current === false
|
||||||
) {
|
) {
|
||||||
autofetchedRef.current = true;
|
autofetchedRef.current = true;
|
||||||
refresh();
|
refresh();
|
||||||
@@ -59,29 +63,28 @@ const Manga: React.FC = () => {
|
|||||||
}, [manga?.title]);
|
}, [manga?.title]);
|
||||||
|
|
||||||
if (error && !manga) {
|
if (error && !manga) {
|
||||||
return (
|
return <EmptyView message="Could not load manga" messageExtra={error.message ?? error} />;
|
||||||
<EmptyView message="Could not load manga" messageExtra={error.message ?? error} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>
|
<Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>
|
||||||
<NavbarToolbar>
|
<NavbarToolbar>
|
||||||
<Stack direction="row" alignItems="center">
|
<Stack direction="row" alignItems="center">
|
||||||
{error && !isValidating && !refreshing && (
|
{error && !isValidating && !refreshing && (
|
||||||
<Tooltip title={(
|
<Tooltip
|
||||||
<>
|
title={
|
||||||
Could not fetch manga data
|
<>
|
||||||
<br />
|
Could not fetch manga data
|
||||||
{error.message ?? error}
|
<br />
|
||||||
</>
|
{error.message ?? error}
|
||||||
)}
|
</>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<IconButton onClick={() => mutate()}>
|
<IconButton onClick={() => mutate()}>
|
||||||
<Warning color="error" />
|
<Warning color="error" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{(manga && (refreshing || isValidating)) && (
|
{manga && (refreshing || isValidating) && (
|
||||||
<IconButton disabled>
|
<IconButton disabled>
|
||||||
<CircularProgress size={16} />
|
<CircularProgress size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -6,9 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import React, {
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
useCallback, useContext, useEffect, useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import HorizontalPager from 'components/reader/pager/HorizontalPager';
|
import HorizontalPager from 'components/reader/pager/HorizontalPager';
|
||||||
import PageNumber from 'components/reader/PageNumber';
|
import PageNumber from 'components/reader/PageNumber';
|
||||||
@@ -53,7 +51,7 @@ const getReaderComponent = (readerType: ReaderType) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
const range = (n: number) => Array.from({ length: n }, (value, key) => key);
|
||||||
const initialChapter = () => ({
|
const initialChapter = () => ({
|
||||||
pageCount: -1,
|
pageCount: -1,
|
||||||
index: -1,
|
index: -1,
|
||||||
@@ -67,22 +65,26 @@ export default function Reader() {
|
|||||||
|
|
||||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
|
|
||||||
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string, mangaId: string }>();
|
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>();
|
||||||
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
|
const [manga, setManga] = useState<IMangaCard | IManga>({
|
||||||
|
id: +mangaId,
|
||||||
|
title: '',
|
||||||
|
thumbnailUrl: '',
|
||||||
|
});
|
||||||
const [chapter, setChapter] = useState<IChapter | IPartialChapter>(initialChapter());
|
const [chapter, setChapter] = useState<IChapter | IPartialChapter>(initialChapter());
|
||||||
const [curPage, setCurPage] = useState<number>(0);
|
const [curPage, setCurPage] = useState<number>(0);
|
||||||
const { setOverride, setTitle } = useContext(NavbarContext);
|
const { setOverride, setTitle } = useContext(NavbarContext);
|
||||||
|
|
||||||
const {
|
const { settings: defaultSettings, loading: areDefaultSettingsLoading } =
|
||||||
settings: defaultSettings,
|
useDefaultReaderSettings();
|
||||||
loading: areDefaultSettingsLoading,
|
|
||||||
} = useDefaultReaderSettings();
|
|
||||||
const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings));
|
const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings));
|
||||||
const [isMangaLoading, setIsMangaLoading] = useState(true);
|
const [isMangaLoading, setIsMangaLoading] = useState(true);
|
||||||
|
|
||||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
||||||
setSettings({ ...settings, [key]: value });
|
setSettings({ ...settings, [key]: value });
|
||||||
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() => makeToast('Failed to save the reader settings to the server', 'warning'));
|
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
|
||||||
|
makeToast('Failed to save the reader settings to the server', 'warning'),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -95,27 +97,27 @@ export default function Reader() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!areDefaultSettingsLoading && !isMangaLoading) {
|
if (!areDefaultSettingsLoading && !isMangaLoading) {
|
||||||
checkAndHandleMissingStoredReaderSettings(manga, 'manga', defaultSettings).catch(() => {});
|
checkAndHandleMissingStoredReaderSettings(manga, 'manga', defaultSettings).catch(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
setSettings(getReaderSettingsFor(manga, defaultSettings));
|
setSettings(getReaderSettingsFor(manga, defaultSettings));
|
||||||
}
|
}
|
||||||
}, [areDefaultSettingsLoading, isMangaLoading]);
|
}, [areDefaultSettingsLoading, isMangaLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// set the custom navbar
|
// set the custom navbar
|
||||||
setOverride(
|
setOverride({
|
||||||
{
|
status: true,
|
||||||
status: true,
|
value: (
|
||||||
value: (
|
<ReaderNavBar
|
||||||
<ReaderNavBar
|
settings={settings}
|
||||||
settings={settings}
|
setSettingValue={setSettingValue}
|
||||||
setSettingValue={setSettingValue}
|
manga={manga}
|
||||||
manga={manga}
|
chapter={chapter as IChapter}
|
||||||
chapter={chapter as IChapter}
|
curPage={curPage}
|
||||||
curPage={curPage}
|
/>
|
||||||
/>
|
),
|
||||||
),
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// clean up for when we leave the reader
|
// clean up for when we leave the reader
|
||||||
return () => setOverride({ status: false, value: <div /> });
|
return () => setOverride({ status: false, value: <div /> });
|
||||||
@@ -123,7 +125,8 @@ export default function Reader() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMangaLoading(true);
|
setIsMangaLoading(true);
|
||||||
client.get(`/api/v1/manga/${mangaId}/`)
|
client
|
||||||
|
.get(`/api/v1/manga/${mangaId}/`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data: IManga) => {
|
.then((data: IManga) => {
|
||||||
setManga(data);
|
setManga(data);
|
||||||
@@ -133,9 +136,10 @@ export default function Reader() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChapter(initialChapter);
|
setChapter(initialChapter);
|
||||||
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
client
|
||||||
|
.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data:IChapter) => {
|
.then((data: IChapter) => {
|
||||||
setChapter(data);
|
setChapter(data);
|
||||||
|
|
||||||
if (data.lastPageRead === data.pageCount - 1) {
|
if (data.lastPageRead === data.pageCount - 1) {
|
||||||
@@ -166,22 +170,32 @@ export default function Reader() {
|
|||||||
formData.append('read', 'true');
|
formData.append('read', 'true');
|
||||||
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
|
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
|
||||||
|
|
||||||
history.replace({ pathname: `/manga/${manga.id}/chapter/${chapter.index + 1}`, state: history.location.state });
|
history.replace({
|
||||||
|
pathname: `/manga/${manga.id}/chapter/${chapter.index + 1}`,
|
||||||
|
state: history.location.state,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [chapter.index, chapter.chapterCount, chapter.pageCount, manga.id]);
|
}, [chapter.index, chapter.chapterCount, chapter.pageCount, manga.id]);
|
||||||
|
|
||||||
const prevChapter = useCallback(() => {
|
const prevChapter = useCallback(() => {
|
||||||
if (chapter.index > 1) {
|
if (chapter.index > 1) {
|
||||||
history.replace({ pathname: `/manga/${manga.id}/chapter/${chapter.index - 1}`, state: history.location.state });
|
history.replace({
|
||||||
|
pathname: `/manga/${manga.id}/chapter/${chapter.index - 1}`,
|
||||||
|
state: history.location.state,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [chapter.index, manga.id]);
|
}, [chapter.index, manga.id]);
|
||||||
|
|
||||||
// return spinner while chpater data is loading
|
// return spinner while chpater data is loading
|
||||||
if (chapter.pageCount === -1) {
|
if (chapter.pageCount === -1) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box
|
||||||
height: '100vh', width: '100vw', display: 'grid', placeItems: 'center',
|
sx={{
|
||||||
}}
|
height: '100vh',
|
||||||
|
width: '100vw',
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress thickness={5} />
|
<CircularProgress thickness={5} />
|
||||||
</Box>
|
</Box>
|
||||||
@@ -196,15 +210,11 @@ export default function Reader() {
|
|||||||
const ReaderComponent = getReaderComponent(settings.readerType);
|
const ReaderComponent = getReaderComponent(settings.readerType);
|
||||||
|
|
||||||
// last page, also probably read = true, we will load the first page.
|
// last page, also probably read = true, we will load the first page.
|
||||||
const initialPage = (chapter.lastPageRead === chapter.pageCount - 1) ? 0 : chapter.lastPageRead;
|
const initialPage = chapter.lastPageRead === chapter.pageCount - 1 ? 0 : chapter.lastPageRead;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: settings.staticNav ? 'calc(100vw - 300px)' : '100vw' }}>
|
<Box sx={{ width: settings.staticNav ? 'calc(100vw - 300px)' : '100vw' }}>
|
||||||
<PageNumber
|
<PageNumber settings={settings} curPage={curPage} pageCount={chapter.pageCount} />
|
||||||
settings={settings}
|
|
||||||
curPage={curPage}
|
|
||||||
pageCount={chapter.pageCount}
|
|
||||||
/>
|
|
||||||
<ReaderComponent
|
<ReaderComponent
|
||||||
pages={pages}
|
pages={pages}
|
||||||
pageCount={chapter.pageCount}
|
pageCount={chapter.pageCount}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Card, CardActionArea, Typography } from '@mui/material';
|
import { Card, CardActionArea, Typography } from '@mui/material';
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
@@ -17,7 +17,10 @@ import { Link } from 'react-router-dom';
|
|||||||
import { StringParam, useQueryParam } from 'use-query-params';
|
import { StringParam, useQueryParam } from 'use-query-params';
|
||||||
import client from 'util/client';
|
import client from 'util/client';
|
||||||
import {
|
import {
|
||||||
langCodeToName, langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs,
|
langCodeToName,
|
||||||
|
langSortCmp,
|
||||||
|
sourceDefualtLangs,
|
||||||
|
sourceForcedDefaultLangs,
|
||||||
} from 'util/language';
|
} from 'util/language';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
|
|
||||||
@@ -25,7 +28,9 @@ function sourceToLangList(sources: ISource[]) {
|
|||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
|
|
||||||
sources.forEach((source) => {
|
sources.forEach((source) => {
|
||||||
if (result.indexOf(source.lang) === -1) { result.push(source.lang); }
|
if (result.indexOf(source.lang) === -1) {
|
||||||
|
result.push(source.lang);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
result.sort(langSortCmp);
|
result.sort(langSortCmp);
|
||||||
@@ -38,7 +43,10 @@ const SearchAll: React.FC = () => {
|
|||||||
const [triggerUpdate, setTriggerUpdate] = useState<number>(2);
|
const [triggerUpdate, setTriggerUpdate] = useState<number>(2);
|
||||||
const [mangas, setMangas] = useState<any>({});
|
const [mangas, setMangas] = useState<any>({});
|
||||||
|
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>(
|
||||||
|
'shownSourceLangs',
|
||||||
|
sourceDefualtLangs(),
|
||||||
|
);
|
||||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||||
|
|
||||||
const [sources, setSources] = useState<ISource[]>([]);
|
const [sources, setSources] = useState<ISource[]>([]);
|
||||||
@@ -56,35 +64,46 @@ const SearchAll: React.FC = () => {
|
|||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<AppbarSearch />
|
<AppbarSearch />
|
||||||
</>
|
</>,
|
||||||
,
|
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.get('/api/v1/source/list')
|
client
|
||||||
|
.get('/api/v1/source/list')
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setSources(data.sort((a: { displayName: string; }, b: { displayName: string; }) => {
|
setSources(
|
||||||
if (a.displayName < b.displayName) { return -1; }
|
data.sort((a: { displayName: string }, b: { displayName: string }) => {
|
||||||
if (a.displayName > b.displayName) { return 1; }
|
if (a.displayName < b.displayName) {
|
||||||
return 0;
|
return -1;
|
||||||
})); setFetchedSources(true);
|
}
|
||||||
|
if (a.displayName > b.displayName) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setFetchedSources(true);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function doIT(elem: any[]) {
|
async function doIT(elem: any[]) {
|
||||||
elem.map((ele) => limit.add(async () => {
|
elem.map((ele) =>
|
||||||
const response = await client.get(`/api/v1/source/${ele.id}/search?searchTerm=${query || ''}&pageNum=1`);
|
limit.add(async () => {
|
||||||
const data = await response.data;
|
const response = await client.get(
|
||||||
const tmp = mangas;
|
`/api/v1/source/${ele.id}/search?searchTerm=${query || ''}&pageNum=1`,
|
||||||
tmp[ele.id] = data.mangaList;
|
);
|
||||||
setMangas(tmp);
|
const data = await response.data;
|
||||||
const tmp2 = fetched;
|
const tmp = mangas;
|
||||||
tmp2[ele.id] = true;
|
tmp[ele.id] = data.mangaList;
|
||||||
setFetched(tmp2);
|
setMangas(tmp);
|
||||||
setResetUI(1);
|
const tmp2 = fetched;
|
||||||
}));
|
tmp2[ele.id] = true;
|
||||||
|
setFetched(tmp2);
|
||||||
|
setResetUI(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -98,7 +117,11 @@ const SearchAll: React.FC = () => {
|
|||||||
setFetched({});
|
setFetched({});
|
||||||
setMangas({});
|
setMangas({});
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
doIT(sources.filter(({ lang }) => shownLangs.indexOf(lang) !== -1).filter((source) => showNsfw || !source.isNsfw));
|
doIT(
|
||||||
|
sources
|
||||||
|
.filter(({ lang }) => shownLangs.indexOf(lang) !== -1)
|
||||||
|
.filter((source) => showNsfw || !source.isNsfw),
|
||||||
|
);
|
||||||
}, [triggerUpdate]);
|
}, [triggerUpdate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -137,9 +160,7 @@ const SearchAll: React.FC = () => {
|
|||||||
setTitle('Sources');
|
setTitle('Sources');
|
||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<AppbarSearch
|
<AppbarSearch autoOpen />
|
||||||
autoOpen
|
|
||||||
/>
|
|
||||||
<LangSelect
|
<LangSelect
|
||||||
shownLangs={shownLangs}
|
shownLangs={shownLangs}
|
||||||
setShownLangs={setShownLangs}
|
setShownLangs={setShownLangs}
|
||||||
@@ -154,20 +175,33 @@ const SearchAll: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* eslint-disable-next-line max-len */}
|
{/* eslint-disable-next-line max-len */}
|
||||||
{sources.filter(({ lang }) => shownLangs.indexOf(lang) !== -1).filter((source) => showNsfw || !source.isNsfw).sort((a, b) => {
|
{sources
|
||||||
const af = fetched[a.id];
|
.filter(({ lang }) => shownLangs.indexOf(lang) !== -1)
|
||||||
const bf = fetched[b.id];
|
.filter((source) => showNsfw || !source.isNsfw)
|
||||||
if (af && !bf) { return -1; }
|
.sort((a, b) => {
|
||||||
if (!af && bf) { return 1; }
|
const af = fetched[a.id];
|
||||||
if (!af && !bf) { return 0; }
|
const bf = fetched[b.id];
|
||||||
|
if (af && !bf) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (!af && bf) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!af && !bf) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const al = mangas[a.id].length === 0;
|
const al = mangas[a.id].length === 0;
|
||||||
const bl = mangas[b.id].length === 0;
|
const bl = mangas[b.id].length === 0;
|
||||||
if (al && !bl) { return 1; }
|
if (al && !bl) {
|
||||||
if (bl && !al) { return -1; }
|
return 1;
|
||||||
return 0;
|
}
|
||||||
}).map(({ lang, id, displayName }) => (
|
if (bl && !al) {
|
||||||
(
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
.map(({ lang, id, displayName }) => (
|
||||||
<>
|
<>
|
||||||
<Card sx={{ margin: '10px' }}>
|
<Card sx={{ margin: '10px' }}>
|
||||||
<CardActionArea
|
<CardActionArea
|
||||||
@@ -175,9 +209,7 @@ const SearchAll: React.FC = () => {
|
|||||||
to={`/sources/${id}/popular/?R&query=${query}`}
|
to={`/sources/${id}/popular/?R&query=${query}`}
|
||||||
sx={{ p: 3 }}
|
sx={{ p: 3 }}
|
||||||
>
|
>
|
||||||
<Typography variant="h5">
|
<Typography variant="h5">{displayName}</Typography>
|
||||||
{displayName}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption">
|
<Typography variant="caption">
|
||||||
{langCodeToName(lang)}
|
{langCodeToName(lang)}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -195,13 +227,11 @@ const SearchAll: React.FC = () => {
|
|||||||
inLibraryIndicator
|
inLibraryIndicator
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
))}
|
||||||
))}
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (<></>);
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SearchAll;
|
export default SearchAll;
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ import ListItemLink from 'components/util/ListItemLink';
|
|||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => { setTitle('Settings'); setAction(<></>); }, []);
|
useEffect(() => {
|
||||||
|
setTitle('Settings');
|
||||||
|
setAction(<></>);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
|
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
|
||||||
const [serverAddress, setServerAddress] = useLocalStorage<String>('serverBaseURL', '');
|
const [serverAddress, setServerAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
@@ -193,9 +196,7 @@ export default function Settings() {
|
|||||||
|
|
||||||
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>Enter Server Address</DialogContentText>
|
||||||
Enter Server Address
|
|
||||||
</DialogContentText>
|
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
@@ -219,9 +220,7 @@ export default function Settings() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={dialogOpenItemWidth} onClose={handleDialogCancelItemWidth}>
|
<Dialog open={dialogOpenItemWidth} onClose={handleDialogCancelItemWidth}>
|
||||||
<DialogTitle>
|
<DialogTitle>Manga Item width</DialogTitle>
|
||||||
Manga Item width
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent
|
<DialogContent
|
||||||
sx={{
|
sx={{
|
||||||
width: '98%',
|
width: '98%',
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import React, { useContext, useEffect } from 'react';
|
|||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import client, { useQuery } from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
import { SwitchPreferenceCompat, CheckBoxPreference } from 'components/sourceConfiguration/TwoStatePreference';
|
import {
|
||||||
|
SwitchPreferenceCompat,
|
||||||
|
CheckBoxPreference,
|
||||||
|
} from 'components/sourceConfiguration/TwoStatePreference';
|
||||||
import ListPreference from 'components/sourceConfiguration/ListPreference';
|
import ListPreference from 'components/sourceConfiguration/ListPreference';
|
||||||
import EditTextPreference from 'components/sourceConfiguration/EditTextPreference';
|
import EditTextPreference from 'components/sourceConfiguration/EditTextPreference';
|
||||||
import MultiSelectListPreference from 'components/sourceConfiguration/MultiSelectListPreference';
|
import MultiSelectListPreference from 'components/sourceConfiguration/MultiSelectListPreference';
|
||||||
@@ -36,10 +39,15 @@ function getPrefComponent(type: string) {
|
|||||||
export default function SourceConfigure() {
|
export default function SourceConfigure() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
useEffect(() => { setTitle('Source Configuration'); setAction(<></>); }, []);
|
useEffect(() => {
|
||||||
|
setTitle('Source Configuration');
|
||||||
|
setAction(<></>);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { sourceId } = useParams<{ sourceId: string }>();
|
const { sourceId } = useParams<{ sourceId: string }>();
|
||||||
const { data: sourcePreferences = [], mutate } = useQuery<SourcePreferences[]>(`/api/v1/source/${sourceId}/preferences`);
|
const { data: sourcePreferences = [], mutate } = useQuery<SourcePreferences[]>(
|
||||||
|
`/api/v1/source/${sourceId}/preferences`,
|
||||||
|
);
|
||||||
|
|
||||||
const convertToString = (position: number, value: any): string => {
|
const convertToString = (position: number, value: any): string => {
|
||||||
switch (sourcePreferences[position].props.defaultValueType) {
|
switch (sourcePreferences[position].props.defaultValueType) {
|
||||||
@@ -50,28 +58,27 @@ export default function SourceConfigure() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateValue = (position: number) => (
|
const updateValue = (position: number) => (value: any) => {
|
||||||
(value: any) => {
|
client
|
||||||
client.post(`/api/v1/source/${sourceId}/preferences`,
|
.post(
|
||||||
JSON.stringify({ position, value: convertToString(position, value) }))
|
`/api/v1/source/${sourceId}/preferences`,
|
||||||
.then(() => mutate());
|
JSON.stringify({ position, value: convertToString(position, value) }),
|
||||||
}
|
)
|
||||||
);
|
.then(() => mutate());
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<List sx={{ padding: 0 }}>
|
<List sx={{ padding: 0 }}>
|
||||||
{sourcePreferences.map(
|
{sourcePreferences.map((it, index) => {
|
||||||
(it, index) => {
|
const props = cloneObject(it.props);
|
||||||
const props = cloneObject(it.props);
|
props.updateValue = updateValue(index);
|
||||||
props.updateValue = updateValue(index);
|
props.key = index;
|
||||||
props.key = index;
|
|
||||||
|
|
||||||
// TypeScript is dumb in detecting extra props
|
// TypeScript is dumb in detecting extra props
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return React.createElement(getPrefComponent(it.type), props);
|
return React.createElement(getPrefComponent(it.type), props);
|
||||||
},
|
})}
|
||||||
)}
|
|
||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import SourceGridLayout from 'components/source/GridLayouts';
|
|||||||
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||||
|
|
||||||
interface IPos {
|
interface IPos {
|
||||||
position: number
|
position: number;
|
||||||
state: any
|
state: any;
|
||||||
group?: number
|
group?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SourceMangas(props: { popular: boolean }) {
|
export default function SourceMangas(props: { popular: boolean }) {
|
||||||
@@ -48,7 +48,8 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
const { options } = useLibraryOptionsContext();
|
const { options } = useLibraryOptionsContext();
|
||||||
|
|
||||||
function makeFilters() {
|
function makeFilters() {
|
||||||
client.get(`/api/v1/source/${sourceId}/filters`)
|
client
|
||||||
|
.get(`/api/v1/source/${sourceId}/filters`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data: ISourceFilters[]) => {
|
.then((data: ISourceFilters[]) => {
|
||||||
SetData(data);
|
SetData(data);
|
||||||
@@ -60,7 +61,8 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.get(`/api/v1/source/${sourceId}`)
|
client
|
||||||
|
.get(`/api/v1/source/${sourceId}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data: ISource) => {
|
.then((data: ISource) => {
|
||||||
setTitle(data.displayName);
|
setTitle(data.displayName);
|
||||||
@@ -79,20 +81,25 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
if (update.length > 0) {
|
if (update.length > 0) {
|
||||||
const rep = update;
|
const rep = update;
|
||||||
setUpdate([]);
|
setUpdate([]);
|
||||||
client.post(`/api/v1/source/${sourceId}/filters`,
|
client
|
||||||
rep.map((e: IPos) => {
|
.post(
|
||||||
const { position, state, group }: IPos = e;
|
`/api/v1/source/${sourceId}/filters`,
|
||||||
return group === undefined ? {
|
rep.map((e: IPos) => {
|
||||||
position,
|
const { position, state, group }: IPos = e;
|
||||||
state,
|
return group === undefined
|
||||||
} : {
|
? {
|
||||||
position: group,
|
position,
|
||||||
state: JSON.stringify({
|
state,
|
||||||
position,
|
}
|
||||||
state,
|
: {
|
||||||
}),
|
position: group,
|
||||||
};
|
state: JSON.stringify({
|
||||||
}))
|
position,
|
||||||
|
state,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setTriggerUpdate(0);
|
setTriggerUpdate(0);
|
||||||
makeFilters();
|
makeFilters();
|
||||||
@@ -101,7 +108,9 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
setFetched(false);
|
setFetched(false);
|
||||||
setMangas([]);
|
setMangas([]);
|
||||||
setLastPageNum(0);
|
setLastPageNum(0);
|
||||||
if (Noreset === undefined && Search) { setNoreset(null); }
|
if (Noreset === undefined && Search) {
|
||||||
|
setNoreset(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [triggerUpdate]);
|
}, [triggerUpdate]);
|
||||||
|
|
||||||
@@ -111,14 +120,13 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
setNoreset(undefined);
|
setNoreset(undefined);
|
||||||
setReset(1);
|
setReset(1);
|
||||||
} else if (Noreset === undefined) {
|
} else if (Noreset === undefined) {
|
||||||
client.get(`/api/v1/source/${sourceId}/filters?reset=true`)
|
client.get(`/api/v1/source/${sourceId}/filters?reset=true`).then(() => {
|
||||||
.then(() => {
|
makeFilters();
|
||||||
makeFilters();
|
setSearch(false);
|
||||||
setSearch(false);
|
if (reset === 1) {
|
||||||
if (reset === 1) {
|
setTriggerUpdate(0);
|
||||||
setTriggerUpdate(0);
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
makeFilters();
|
makeFilters();
|
||||||
@@ -140,8 +148,7 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</>
|
</>,
|
||||||
,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -152,8 +159,12 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query) {
|
if (query) {
|
||||||
setSearch(true);
|
setSearch(true);
|
||||||
} else { setSearch(false); }
|
} else {
|
||||||
if (Noreset === undefined) { setInit(null); }
|
setSearch(false);
|
||||||
|
}
|
||||||
|
if (Noreset === undefined) {
|
||||||
|
setInit(null);
|
||||||
|
}
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -163,16 +174,27 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
return () => clearTimeout(delayDebounceFn);
|
return () => clearTimeout(delayDebounceFn);
|
||||||
}
|
}
|
||||||
if (Search !== undefined) { setInit(null); }
|
if (Search !== undefined) {
|
||||||
|
setInit(null);
|
||||||
|
}
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [Search, query]);
|
}, [Search, query]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastPageNum !== 0) {
|
if (lastPageNum !== 0) {
|
||||||
const sourceType = props.popular ? 'popular' : 'latest';
|
const sourceType = props.popular ? 'popular' : 'latest';
|
||||||
client.get(`/api/v1/source/${sourceId}/${query !== undefined || Search || Noreset === null ? 'search' : sourceType}${query !== undefined || Search || Noreset === null ? `?searchTerm=${query || ''}&pageNum=${lastPageNum}` : `/${lastPageNum}`}`)
|
client
|
||||||
|
.get(
|
||||||
|
`/api/v1/source/${sourceId}/${
|
||||||
|
query !== undefined || Search || Noreset === null ? 'search' : sourceType
|
||||||
|
}${
|
||||||
|
query !== undefined || Search || Noreset === null
|
||||||
|
? `?searchTerm=${query || ''}&pageNum=${lastPageNum}`
|
||||||
|
: `/${lastPageNum}`
|
||||||
|
}`,
|
||||||
|
)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
|
.then((data: { mangaList: IManga[]; hasNextPage: boolean }) => {
|
||||||
setMangas([
|
setMangas([
|
||||||
...mangas,
|
...mangas,
|
||||||
...data.mangaList.map((it) => ({
|
...data.mangaList.map((it) => ({
|
||||||
@@ -180,11 +202,14 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
thumbnailUrl: it.thumbnailUrl,
|
thumbnailUrl: it.thumbnailUrl,
|
||||||
id: it.id,
|
id: it.id,
|
||||||
inLibrary: it.inLibrary,
|
inLibrary: it.inLibrary,
|
||||||
}))]);
|
})),
|
||||||
|
]);
|
||||||
setHasNextPage(data.hasNextPage);
|
setHasNextPage(data.hasNextPage);
|
||||||
setFetched(true);
|
setFetched(true);
|
||||||
});
|
});
|
||||||
} else { setLastPageNum(1); }
|
} else {
|
||||||
|
setLastPageNum(1);
|
||||||
|
}
|
||||||
}, [lastPageNum]);
|
}, [lastPageNum]);
|
||||||
|
|
||||||
let message;
|
let message;
|
||||||
@@ -196,7 +221,9 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
messageExtra = (
|
messageExtra = (
|
||||||
<>
|
<>
|
||||||
<span>Check out </span>
|
<span>Check out </span>
|
||||||
<a href="https://github.com/Suwayomi/Tachidesk-Server/wiki/Local-Source">Local source guide</a>
|
<a href="https://github.com/Suwayomi/Tachidesk-Server/wiki/Local-Source">
|
||||||
|
Local source guide
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import LangSelect from 'components/navbar/action/LangSelect';
|
|||||||
import SourceCard from 'components/SourceCard';
|
import SourceCard from 'components/SourceCard';
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import {
|
import {
|
||||||
sourceDefualtLangs, sourceForcedDefaultLangs, langCodeToName, langSortCmp,
|
sourceDefualtLangs,
|
||||||
|
sourceForcedDefaultLangs,
|
||||||
|
langCodeToName,
|
||||||
|
langSortCmp,
|
||||||
} from 'util/language';
|
} from 'util/language';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||||
@@ -23,7 +26,9 @@ function sourceToLangList(sources: ISource[]) {
|
|||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
|
|
||||||
sources.forEach((source) => {
|
sources.forEach((source) => {
|
||||||
if (result.indexOf(source.lang) === -1) { result.push(source.lang); }
|
if (result.indexOf(source.lang) === -1) {
|
||||||
|
result.push(source.lang);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
result.sort(langSortCmp);
|
result.sort(langSortCmp);
|
||||||
@@ -33,7 +38,9 @@ function sourceToLangList(sources: ISource[]) {
|
|||||||
function groupByLang(sources: ISource[]) {
|
function groupByLang(sources: ISource[]) {
|
||||||
const result = {} as any;
|
const result = {} as any;
|
||||||
sources.forEach((source) => {
|
sources.forEach((source) => {
|
||||||
if (result[source.lang] === undefined) { result[source.lang] = [] as ISource[]; }
|
if (result[source.lang] === undefined) {
|
||||||
|
result[source.lang] = [] as ISource[];
|
||||||
|
}
|
||||||
result[source.lang].push(source);
|
result[source.lang].push(source);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,7 +50,10 @@ function groupByLang(sources: ISource[]) {
|
|||||||
export default function Sources() {
|
export default function Sources() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>(
|
||||||
|
'shownSourceLangs',
|
||||||
|
sourceDefualtLangs(),
|
||||||
|
);
|
||||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||||
|
|
||||||
const { data: sources, loading } = useQuery<ISource[]>('/api/v1/source/list');
|
const { data: sources, loading } = useQuery<ISource[]>('/api/v1/source/list');
|
||||||
@@ -70,10 +80,7 @@ export default function Sources() {
|
|||||||
setTitle('Sources');
|
setTitle('Sources');
|
||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton onClick={() => history.push('/sources/all/search/')} size="large">
|
||||||
onClick={() => history.push('/sources/all/search/')}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<TravelExploreIcon />
|
<TravelExploreIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<LangSelect
|
<LangSelect
|
||||||
@@ -89,27 +96,29 @@ export default function Sources() {
|
|||||||
if (loading) return <LoadingPlaceholder />;
|
if (loading) return <LoadingPlaceholder />;
|
||||||
|
|
||||||
if (sources?.length === 0) {
|
if (sources?.length === 0) {
|
||||||
return (<h3>No sources found. Install Some Extensions first.</h3>);
|
return <h3>No sources found. Install Some Extensions first.</h3>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* eslint-disable-next-line max-len */}
|
{/* eslint-disable-next-line max-len */}
|
||||||
{Object.entries(groupByLang(sources ?? [])).sort((a, b) => langSortCmp(a[0], b[0])).map(([lang, list]) => (
|
{Object.entries(groupByLang(sources ?? []))
|
||||||
shownLangs.indexOf(lang) !== -1 && (
|
.sort((a, b) => langSortCmp(a[0], b[0]))
|
||||||
<React.Fragment key={lang}>
|
.map(
|
||||||
<h1 key={lang} style={{ marginLeft: 25 }}>{langCodeToName(lang)}</h1>
|
([lang, list]) =>
|
||||||
{(list as ISource[])
|
shownLangs.indexOf(lang) !== -1 && (
|
||||||
.filter((source) => showNsfw || !source.isNsfw)
|
<React.Fragment key={lang}>
|
||||||
.map((source) => (
|
<h1 key={lang} style={{ marginLeft: 25 }}>
|
||||||
<SourceCard
|
{langCodeToName(lang)}
|
||||||
key={source.id}
|
</h1>
|
||||||
source={source}
|
{(list as ISource[])
|
||||||
/>
|
.filter((source) => showNsfw || !source.isNsfw)
|
||||||
))}
|
.map((source) => (
|
||||||
</React.Fragment>
|
<SourceCard key={source.id} source={source} />
|
||||||
)
|
))}
|
||||||
))}
|
</React.Fragment>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ import NavbarContext from 'components/context/NavbarContext';
|
|||||||
import DownloadStateIndicator from 'components/molecules/DownloadStateIndicator';
|
import DownloadStateIndicator from 'components/molecules/DownloadStateIndicator';
|
||||||
import EmptyView from 'components/util/EmptyView';
|
import EmptyView from 'components/util/EmptyView';
|
||||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||||
import React, {
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
useContext, useEffect, useRef, useState,
|
|
||||||
} from 'react';
|
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import client from 'util/client';
|
import client from 'util/client';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
@@ -30,10 +28,12 @@ function epochToDate(epoch: number) {
|
|||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTheSameDay(first:Date, second:Date) {
|
function isTheSameDay(first: Date, second: Date) {
|
||||||
return first.getDate() === second.getDate()
|
return (
|
||||||
&& first.getMonth() === second.getMonth()
|
first.getDate() === second.getDate() &&
|
||||||
&& first.getFullYear() === second.getFullYear();
|
first.getMonth() === second.getMonth() &&
|
||||||
|
first.getFullYear() === second.getFullYear()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDateString(date: Date) {
|
function getDateString(date: Date) {
|
||||||
@@ -46,8 +46,9 @@ function getDateString(date: Date) {
|
|||||||
return date.toLocaleDateString();
|
return date.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupByDate(updates: IMangaChapter[]):
|
function groupByDate(
|
||||||
[string, { item: IMangaChapter, globalIdx: number }[] ][] {
|
updates: IMangaChapter[],
|
||||||
|
): [string, { item: IMangaChapter; globalIdx: number }[]][] {
|
||||||
if (updates.length === 0) return [];
|
if (updates.length === 0) return [];
|
||||||
|
|
||||||
const groups = {};
|
const groups = {};
|
||||||
@@ -63,7 +64,10 @@ function groupByDate(updates: IMangaChapter[]):
|
|||||||
return Object.keys(groups).map((key) => [key, groups[key]]);
|
return Object.keys(groups).map((key) => [key, groups[key]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
|
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace(
|
||||||
|
'http',
|
||||||
|
'ws',
|
||||||
|
);
|
||||||
const initialQueue = {
|
const initialQueue = {
|
||||||
status: 'Stopped',
|
status: 'Stopped',
|
||||||
queue: [],
|
queue: [],
|
||||||
@@ -104,13 +108,11 @@ const Updates: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasNextPage) {
|
if (hasNextPage) {
|
||||||
client.get(`/api/v1/update/recentChapters/${lastPageNum}`)
|
client
|
||||||
|
.get(`/api/v1/update/recentChapters/${lastPageNum}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then(({ hasNextPage: fetchedHasNextPage, page }: PaginatedList<IMangaChapter>) => {
|
.then(({ hasNextPage: fetchedHasNextPage, page }: PaginatedList<IMangaChapter>) => {
|
||||||
setUpdateEntries([
|
setUpdateEntries([...updateEntries, ...page]);
|
||||||
...updateEntries,
|
|
||||||
...page,
|
|
||||||
]);
|
|
||||||
setHasNextPage(fetchedHasNextPage);
|
setHasNextPage(fetchedHasNextPage);
|
||||||
setFetched(true);
|
setFetched(true);
|
||||||
});
|
});
|
||||||
@@ -122,7 +124,7 @@ const Updates: React.FC = () => {
|
|||||||
const scrollHandler = () => {
|
const scrollHandler = () => {
|
||||||
if (lastEntry.current) {
|
if (lastEntry.current) {
|
||||||
const rect = lastEntry.current.getBoundingClientRect();
|
const rect = lastEntry.current.getBoundingClientRect();
|
||||||
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) {
|
if ((rect.y + rect.height) / window.innerHeight < 2 && hasNextPage) {
|
||||||
setLastPageNum(lastPageNum + 1);
|
setLastPageNum(lastPageNum + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,8 +136,12 @@ const Updates: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, [hasNextPage, updateEntries]);
|
}, [hasNextPage, updateEntries]);
|
||||||
|
|
||||||
if (!fetched) { return <LoadingPlaceholder />; }
|
if (!fetched) {
|
||||||
if (fetched && updateEntries.length === 0) { return <EmptyView message="You don't have any updates yet." />; }
|
return <LoadingPlaceholder />;
|
||||||
|
}
|
||||||
|
if (fetched && updateEntries.length === 0) {
|
||||||
|
return <EmptyView message="You don't have any updates yet." />;
|
||||||
|
}
|
||||||
|
|
||||||
const downloadForChapter = (chapter: IChapter) => {
|
const downloadForChapter = (chapter: IChapter) => {
|
||||||
const { index, mangaId } = chapter;
|
const { index, mangaId } = chapter;
|
||||||
@@ -170,7 +176,10 @@ const Updates: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<CardActionArea
|
<CardActionArea
|
||||||
component={Link}
|
component={Link}
|
||||||
to={{ pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`, state: history.location.state }}
|
to={{
|
||||||
|
pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`,
|
||||||
|
state: history.location.state,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CardContent
|
<CardContent
|
||||||
sx={{
|
sx={{
|
||||||
@@ -196,7 +205,11 @@ const Updates: React.FC = () => {
|
|||||||
<Typography variant="h5" component="h2">
|
<Typography variant="h5" component="h2">
|
||||||
{manga.title}
|
{manga.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" display="block" gutterBottom>
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
display="block"
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
{chapter.name}
|
{chapter.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import { useQuery } from 'util/client';
|
|||||||
export default function About() {
|
export default function About() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
useEffect(() => { setTitle('About'); setAction(<></>); }, []);
|
useEffect(() => {
|
||||||
|
setTitle('About');
|
||||||
|
setAction(<></>);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { data: about } = useQuery<IAbout>('/api/v1/settings/about');
|
const { data: about } = useQuery<IAbout>('/api/v1/settings/about');
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ import NavbarContext from 'components/context/NavbarContext';
|
|||||||
|
|
||||||
export default function Backup() {
|
export default function Backup() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => { setTitle('Backup'); setAction(<></>); }, []);
|
useEffect(() => {
|
||||||
|
setTitle('Backup');
|
||||||
|
setAction(<></>);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { baseURL } = client.defaults;
|
const { baseURL } = client.defaults;
|
||||||
|
|
||||||
@@ -27,8 +30,10 @@ export default function Backup() {
|
|||||||
formData.append('backup.proto.gz', file);
|
formData.append('backup.proto.gz', file);
|
||||||
|
|
||||||
makeToast('Restoring backup....', 'info');
|
makeToast('Restoring backup....', 'info');
|
||||||
client.post('/api/v1/backup/import/file',
|
client
|
||||||
formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
.post('/api/v1/backup/import/file', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
.then(() => makeToast('Backup restore finished!', 'success'))
|
.then(() => makeToast('Backup restore finished!', 'success'))
|
||||||
.catch(() => makeToast('Backup restore failed!', 'error'));
|
.catch(() => makeToast('Backup restore failed!', 'error'));
|
||||||
} else if (file.name.toLowerCase().endsWith('json')) {
|
} else if (file.name.toLowerCase().endsWith('json')) {
|
||||||
@@ -81,12 +86,7 @@ export default function Backup() {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
<input
|
<input type="file" id="backup-file" style={{ display: 'none' }} />
|
||||||
type="file"
|
|
||||||
id="backup-file"
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,15 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, {
|
import React, { useMemo, useState, useContext, useEffect } from 'react';
|
||||||
useMemo, useState, useContext, useEffect,
|
import { List, ListItem, ListItemText, ListItemIcon, IconButton } from '@mui/material';
|
||||||
} from 'react';
|
|
||||||
import {
|
import {
|
||||||
List,
|
DragDropContext,
|
||||||
ListItem,
|
Droppable,
|
||||||
ListItemText,
|
Draggable,
|
||||||
ListItemIcon,
|
DropResult,
|
||||||
IconButton,
|
DraggingStyle,
|
||||||
} from '@mui/material';
|
NotDraggingStyle,
|
||||||
import {
|
|
||||||
DragDropContext, Droppable, Draggable, DropResult, DraggingStyle, NotDraggingStyle,
|
|
||||||
} from 'react-beautiful-dnd';
|
} from 'react-beautiful-dnd';
|
||||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
@@ -37,8 +34,11 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
|||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import client, { useQuery } from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
|
|
||||||
const getItemStyle = (isDragging: boolean,
|
const getItemStyle = (
|
||||||
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
|
isDragging: boolean,
|
||||||
|
draggableStyle: DraggingStyle | NotDraggingStyle | undefined,
|
||||||
|
palette: Palette,
|
||||||
|
) => ({
|
||||||
// styles we need to apply on draggables
|
// styles we need to apply on draggables
|
||||||
...draggableStyle,
|
...draggableStyle,
|
||||||
|
|
||||||
@@ -49,11 +49,14 @@ const getItemStyle = (isDragging: boolean,
|
|||||||
|
|
||||||
export default function Categories() {
|
export default function Categories() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);
|
useEffect(() => {
|
||||||
|
setTitle('Categories');
|
||||||
|
setAction(<></>);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { data, mutate } = useQuery<ICategory[]>('/api/v1/category/');
|
const { data, mutate } = useQuery<ICategory[]>('/api/v1/category/');
|
||||||
const categories = useMemo(() => {
|
const categories = useMemo(() => {
|
||||||
const res = [...data ?? []];
|
const res = [...(data ?? [])];
|
||||||
if (res.length > 0 && res[0].name === 'Default') {
|
if (res.length > 0 && res[0].name === 'Default') {
|
||||||
res.shift();
|
res.shift();
|
||||||
}
|
}
|
||||||
@@ -84,11 +87,7 @@ export default function Categories() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryReorder(
|
categoryReorder(categories, result.source.index, result.destination.index);
|
||||||
categories,
|
|
||||||
result.source.index,
|
|
||||||
result.destination.index,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetDialog = () => {
|
const resetDialog = () => {
|
||||||
@@ -102,7 +101,7 @@ export default function Categories() {
|
|||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditDialogOpen = (index:number) => {
|
const handleEditDialogOpen = (index: number) => {
|
||||||
setDialogName(categories[index].name);
|
setDialogName(categories[index].name);
|
||||||
setDialogDefault(categories[index].default);
|
setDialogDefault(categories[index].default);
|
||||||
setCategoryToEdit(index);
|
setCategoryToEdit(index);
|
||||||
@@ -121,19 +120,16 @@ export default function Categories() {
|
|||||||
formData.append('default', dialogDefault.toString());
|
formData.append('default', dialogDefault.toString());
|
||||||
|
|
||||||
if (categoryToEdit === -1) {
|
if (categoryToEdit === -1) {
|
||||||
client.post('/api/v1/category/', formData)
|
client.post('/api/v1/category/', formData).finally(() => mutate());
|
||||||
.finally(() => mutate());
|
|
||||||
} else {
|
} else {
|
||||||
const category = categories[categoryToEdit];
|
const category = categories[categoryToEdit];
|
||||||
client.patch(`/api/v1/category/${category.id}`, formData)
|
client.patch(`/api/v1/category/${category.id}`, formData).finally(() => mutate());
|
||||||
.finally(() => mutate());
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCategory = (index:number) => {
|
const deleteCategory = (index: number) => {
|
||||||
const category = categories[index];
|
const category = categories[index];
|
||||||
client.delete(`/api/v1/category/${category.id}`)
|
client.delete(`/api/v1/category/${category.id}`).finally(() => mutate());
|
||||||
.finally(() => mutate());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -163,9 +159,7 @@ export default function Categories() {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DragHandleIcon />
|
<DragHandleIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText primary={item.name} />
|
||||||
primary={item.name}
|
|
||||||
/>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleEditDialogOpen(index);
|
handleEditDialogOpen(index);
|
||||||
@@ -219,13 +213,13 @@ export default function Categories() {
|
|||||||
onChange={(e) => setDialogName(e.target.value)}
|
onChange={(e) => setDialogName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={(
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={dialogDefault}
|
checked={dialogDefault}
|
||||||
onChange={(e) => setDialogDefault(e.target.checked)}
|
onChange={(e) => setDialogDefault(e.target.checked)}
|
||||||
color="default"
|
color="default"
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
label="Default category when adding new manga to library"
|
label="Default category when adding new manga to library"
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -31,22 +31,31 @@ export default function DefaultReaderSettings() {
|
|||||||
const { metadata, settings, loading } = useDefaultReaderSettings();
|
const { metadata, settings, loading } = useDefaultReaderSettings();
|
||||||
|
|
||||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
||||||
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() => makeToast('Failed to save the default reader settings to the server', 'warning'));
|
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
|
||||||
|
makeToast('Failed to save the default reader settings to the server', 'warning'),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box
|
||||||
height: '100vh', width: '100vw', display: 'grid', placeItems: 'center',
|
sx={{
|
||||||
}}
|
height: '100vh',
|
||||||
|
width: '100vw',
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress thickness={5} />
|
<CircularProgress thickness={5} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAndHandleMissingStoredReaderSettings({ meta: metadata }, 'server', getDefaultSettings())
|
checkAndHandleMissingStoredReaderSettings(
|
||||||
.catch(() => {});
|
{ meta: metadata },
|
||||||
|
'server',
|
||||||
|
getDefaultSettings(),
|
||||||
|
).catch(() => {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReaderSettingsOptions
|
<ReaderSettingsOptions
|
||||||
|
|||||||
24
src/theme.ts
24
src/theme.ts
@@ -16,7 +16,6 @@ declare module '@mui/material/styles' {
|
|||||||
interface PaletteOptions {
|
interface PaletteOptions {
|
||||||
custom?: PaletteOptions['primary'];
|
custom?: PaletteOptions['primary'];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createTheme = (dark?: boolean) => {
|
const createTheme = (dark?: boolean) => {
|
||||||
@@ -26,16 +25,17 @@ const createTheme = (dark?: boolean) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const tachideskTheme = createMuiTheme({
|
const tachideskTheme = createMuiTheme(
|
||||||
palette: {
|
{
|
||||||
custom: {
|
palette: {
|
||||||
main: dark ? baseTheme.palette.common.black : baseTheme.palette.common.white,
|
custom: {
|
||||||
light: dark ? baseTheme.palette.grey[900] : baseTheme.palette.grey[100],
|
main: dark ? baseTheme.palette.common.black : baseTheme.palette.common.white,
|
||||||
|
light: dark ? baseTheme.palette.grey[900] : baseTheme.palette.grey[100],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
components: {
|
||||||
components: {
|
MuiCssBaseline: {
|
||||||
MuiCssBaseline: {
|
styleOverrides: `
|
||||||
styleOverrides: `
|
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
background: ${dark ? '#222' : '#e1e1e1'};
|
background: ${dark ? '#222' : '#e1e1e1'};
|
||||||
@@ -46,9 +46,11 @@ const createTheme = (dark?: boolean) => {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, baseTheme);
|
baseTheme,
|
||||||
|
);
|
||||||
|
|
||||||
return tachideskTheme;
|
return tachideskTheme;
|
||||||
};
|
};
|
||||||
|
|||||||
372
src/typings.d.ts
vendored
372
src/typings.d.ts
vendored
@@ -6,57 +6,57 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
interface IExtension {
|
interface IExtension {
|
||||||
name: string
|
name: string;
|
||||||
pkgName: string
|
pkgName: string;
|
||||||
versionName: string
|
versionName: string;
|
||||||
versionCode: number
|
versionCode: number;
|
||||||
lang: string
|
lang: string;
|
||||||
isNsfw: boolean
|
isNsfw: boolean;
|
||||||
apkName: string
|
apkName: string;
|
||||||
iconUrl: string
|
iconUrl: string;
|
||||||
installed: boolean
|
installed: boolean;
|
||||||
hasUpdate: boolean
|
hasUpdate: boolean;
|
||||||
obsolete: boolean
|
obsolete: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISource {
|
interface ISource {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
lang: string
|
lang: string;
|
||||||
iconUrl: string
|
iconUrl: string;
|
||||||
supportsLatest: boolean
|
supportsLatest: boolean;
|
||||||
isConfigurable: boolean
|
isConfigurable: boolean;
|
||||||
isNsfw: boolean
|
isNsfw: boolean;
|
||||||
displayName: string
|
displayName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISourceFilters {
|
interface ISourceFilters {
|
||||||
type: string
|
type: string;
|
||||||
filter: ISourceFilter
|
filter: ISourceFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISourceFilter {
|
interface ISourceFilter {
|
||||||
name: string
|
name: string;
|
||||||
state: number | string | boolean | ISourceFilters[] | IState
|
state: number | string | boolean | ISourceFilters[] | IState;
|
||||||
values?: string[]
|
values?: string[];
|
||||||
displayValues?: string[]
|
displayValues?: string[];
|
||||||
selected?: ISelected
|
selected?: ISelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISelected {
|
interface ISelected {
|
||||||
displayname: string
|
displayname: string;
|
||||||
value: string
|
value: string;
|
||||||
_value: string
|
_value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
ascending: boolean
|
ascending: boolean;
|
||||||
index: number
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMetadataMigration {
|
interface IMetadataMigration {
|
||||||
appKeyPrefix?: { oldPrefix: string, newPrefix: string }
|
appKeyPrefix?: { oldPrefix: string; newPrefix: string };
|
||||||
keys?: { oldKey: string, newKey: string }[]
|
keys?: { oldKey: string; newKey: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMetadata<VALUES extends AllowedMetadataValueTypes = string> {
|
interface IMetadata<VALUES extends AllowedMetadataValueTypes = string> {
|
||||||
@@ -64,7 +64,7 @@ interface IMetadata<VALUES extends AllowedMetadataValueTypes = string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IMetadataHolder<VALUES extends AllowedMetadataValueTypes = string> {
|
interface IMetadataHolder<VALUES extends AllowedMetadataValueTypes = string> {
|
||||||
meta?: IMetadata<VALUES>
|
meta?: IMetadata<VALUES>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AllowedMetadataValueTypes = string | boolean | number | undefined;
|
type AllowedMetadataValueTypes = string | boolean | number | undefined;
|
||||||
@@ -76,211 +76,211 @@ type AppMetadataKeys = MangaMetadataKeys;
|
|||||||
type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes];
|
type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes];
|
||||||
|
|
||||||
interface IMangaCard {
|
interface IMangaCard {
|
||||||
id: number
|
id: number;
|
||||||
title: string
|
title: string;
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string;
|
||||||
unreadCount?: number
|
unreadCount?: number;
|
||||||
downloadCount?: number
|
downloadCount?: number;
|
||||||
inLibrary?: boolean
|
inLibrary?: boolean;
|
||||||
meta?: IMetadata
|
meta?: IMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IManga {
|
interface IManga {
|
||||||
id: number
|
id: number;
|
||||||
sourceId: string
|
sourceId: string;
|
||||||
|
|
||||||
url: string
|
url: string;
|
||||||
title: string
|
title: string;
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string;
|
||||||
|
|
||||||
artist: string
|
artist: string;
|
||||||
author: string
|
author: string;
|
||||||
description: string
|
description: string;
|
||||||
genre: string[]
|
genre: string[];
|
||||||
status: string
|
status: string;
|
||||||
|
|
||||||
inLibrary: boolean
|
inLibrary: boolean;
|
||||||
source: ISource
|
source: ISource;
|
||||||
|
|
||||||
meta: IMetadata
|
meta: IMetadata;
|
||||||
|
|
||||||
realUrl: string
|
realUrl: string;
|
||||||
freshData: boolean
|
freshData: boolean;
|
||||||
unreadCount?: number
|
unreadCount?: number;
|
||||||
downloadCount?: number
|
downloadCount?: number;
|
||||||
|
|
||||||
age: number
|
age: number;
|
||||||
chaptersAge: number
|
chaptersAge: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IChapter {
|
interface IChapter {
|
||||||
id: number
|
id: number;
|
||||||
url: string
|
url: string;
|
||||||
name: string
|
name: string;
|
||||||
uploadDate: number
|
uploadDate: number;
|
||||||
chapterNumber: number
|
chapterNumber: number;
|
||||||
scanlator: string
|
scanlator: string;
|
||||||
mangaId: number
|
mangaId: number;
|
||||||
read: boolean
|
read: boolean;
|
||||||
bookmarked: boolean
|
bookmarked: boolean;
|
||||||
lastPageRead: number
|
lastPageRead: number;
|
||||||
lastReadAt: number
|
lastReadAt: number;
|
||||||
index: number
|
index: number;
|
||||||
fetchedAt: number
|
fetchedAt: number;
|
||||||
chapterCount: number
|
chapterCount: number;
|
||||||
pageCount: number
|
pageCount: number;
|
||||||
downloaded: boolean
|
downloaded: boolean;
|
||||||
meta: IAppMetadata
|
meta: IAppMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMangaChapter {
|
interface IMangaChapter {
|
||||||
manga: IManga
|
manga: IManga;
|
||||||
chapter: IChapter
|
chapter: IChapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPartialChapter {
|
interface IPartialChapter {
|
||||||
pageCount: number
|
pageCount: number;
|
||||||
index: number
|
index: number;
|
||||||
chapterCount: number
|
chapterCount: number;
|
||||||
lastPageRead: number
|
lastPageRead: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICategory {
|
interface ICategory {
|
||||||
id: number
|
id: number;
|
||||||
order: number
|
order: number;
|
||||||
name: string
|
name: string;
|
||||||
default: boolean
|
default: boolean;
|
||||||
meta: IAppMetadata
|
meta: IAppMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface INavbarOverride {
|
interface INavbarOverride {
|
||||||
status: boolean
|
status: boolean;
|
||||||
value: any
|
value: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReaderType =
|
type ReaderType =
|
||||||
'ContinuesVertical' |
|
| 'ContinuesVertical'
|
||||||
'Webtoon' |
|
| 'Webtoon'
|
||||||
'SingleVertical' |
|
| 'SingleVertical'
|
||||||
'SingleRTL' |
|
| 'SingleRTL'
|
||||||
'SingleLTR' |
|
| 'SingleLTR'
|
||||||
'DoubleVertical' |
|
| 'DoubleVertical'
|
||||||
'DoubleRTL' |
|
| 'DoubleRTL'
|
||||||
'DoubleLTR' |
|
| 'DoubleLTR'
|
||||||
'ContinuesHorizontalLTR' |
|
| 'ContinuesHorizontalLTR'
|
||||||
'ContinuesHorizontalRTL';
|
| 'ContinuesHorizontalRTL';
|
||||||
|
|
||||||
interface IReaderSettings{
|
interface IReaderSettings {
|
||||||
staticNav: boolean
|
staticNav: boolean;
|
||||||
showPageNumber: boolean
|
showPageNumber: boolean;
|
||||||
loadNextOnEnding: boolean
|
loadNextOnEnding: boolean;
|
||||||
readerType: ReaderType
|
readerType: ReaderType;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IReaderPage {
|
interface IReaderPage {
|
||||||
index: number
|
index: number;
|
||||||
src: string
|
src: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IReaderProps {
|
interface IReaderProps {
|
||||||
pages: Array<IReaderPage>
|
pages: Array<IReaderPage>;
|
||||||
pageCount: number
|
pageCount: number;
|
||||||
setCurPage: React.Dispatch<React.SetStateAction<number>>
|
setCurPage: React.Dispatch<React.SetStateAction<number>>;
|
||||||
curPage: number
|
curPage: number;
|
||||||
initialPage: number
|
initialPage: number;
|
||||||
settings: IReaderSettings
|
settings: IReaderSettings;
|
||||||
manga: IMangaCard | IManga
|
manga: IMangaCard | IManga;
|
||||||
chapter: IChapter | IPartialChapter
|
chapter: IChapter | IPartialChapter;
|
||||||
nextChapter: () => void
|
nextChapter: () => void;
|
||||||
prevChapter: () => void
|
prevChapter: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAbout {
|
interface IAbout {
|
||||||
name: string
|
name: string;
|
||||||
version: string
|
version: string;
|
||||||
revision: string
|
revision: string;
|
||||||
buildType: 'Stable' | 'Preview'
|
buildType: 'Stable' | 'Preview';
|
||||||
buildTime: number
|
buildTime: number;
|
||||||
github: string
|
github: string;
|
||||||
discord: string
|
discord: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDownloadChapter{
|
interface IDownloadChapter {
|
||||||
chapterIndex: number
|
chapterIndex: number;
|
||||||
mangaId: number
|
mangaId: number;
|
||||||
state: 'Queued' | 'Downloading' | 'Finished' | 'Error'
|
state: 'Queued' | 'Downloading' | 'Finished' | 'Error';
|
||||||
progress: number
|
progress: number;
|
||||||
chapter: IChapter
|
chapter: IChapter;
|
||||||
manga: IManga
|
manga: IManga;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IQueue {
|
interface IQueue {
|
||||||
status: 'Stopped' | 'Started'
|
status: 'Stopped' | 'Started';
|
||||||
queue: IDownloadChapter[]
|
queue: IDownloadChapter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUpdateStatus {
|
interface IUpdateStatus {
|
||||||
running: boolean
|
running: boolean;
|
||||||
statusMap: {
|
statusMap: {
|
||||||
COMPLETE?: IManga[],
|
COMPLETE?: IManga[];
|
||||||
RUNNING?: IManga[],
|
RUNNING?: IManga[];
|
||||||
PENDING?: IManga[]
|
PENDING?: IManga[];
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PreferenceProps {
|
interface PreferenceProps {
|
||||||
key: string
|
key: string;
|
||||||
title: string
|
title: string;
|
||||||
summary: string
|
summary: string;
|
||||||
defaultValue: any
|
defaultValue: any;
|
||||||
currentValue: any
|
currentValue: any;
|
||||||
defaultValueType: string
|
defaultValueType: string;
|
||||||
|
|
||||||
// intetnal props
|
// intetnal props
|
||||||
updateValue: any
|
updateValue: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TwoStatePreferenceProps extends PreferenceProps {
|
interface TwoStatePreferenceProps extends PreferenceProps {
|
||||||
|
|
||||||
// intetnal props
|
// intetnal props
|
||||||
type: 'Switch' | 'Checkbox'
|
type: 'Switch' | 'Checkbox';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CheckBoxPreferenceProps extends PreferenceProps {}
|
interface CheckBoxPreferenceProps extends PreferenceProps {}
|
||||||
|
|
||||||
interface SwitchPreferenceCompatProps extends PreferenceProps {}
|
interface SwitchPreferenceCompatProps extends PreferenceProps {}
|
||||||
|
|
||||||
interface ListPreferenceProps extends PreferenceProps {
|
interface ListPreferenceProps extends PreferenceProps {
|
||||||
entries: string[]
|
entries: string[];
|
||||||
entryValues: string[]
|
entryValues: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MultiSelectListPreferenceProps extends PreferenceProps {
|
interface MultiSelectListPreferenceProps extends PreferenceProps {
|
||||||
entries: string[]
|
entries: string[];
|
||||||
entryValues: string[]
|
entryValues: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditTextPreferenceProps extends PreferenceProps {
|
interface EditTextPreferenceProps extends PreferenceProps {
|
||||||
dialogTitle: string
|
dialogTitle: string;
|
||||||
dialogMessage: string
|
dialogMessage: string;
|
||||||
text: string
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourcePreferences {
|
interface SourcePreferences {
|
||||||
type: string
|
type: string;
|
||||||
props: any
|
props: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NavbarItem {
|
interface NavbarItem {
|
||||||
path: string,
|
path: string;
|
||||||
title:string,
|
title: string;
|
||||||
SelectedIconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>,
|
SelectedIconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
|
||||||
IconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>,
|
IconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
|
||||||
show: 'mobile' | 'desktop' | 'both'
|
show: 'mobile' | 'desktop' | 'both';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaginatedList<T> {
|
interface PaginatedList<T> {
|
||||||
page: T[],
|
page: T[];
|
||||||
hasNextPage: boolean
|
hasNextPage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NullAndUndefined<T> = T | null | undefined;
|
type NullAndUndefined<T> = T | null | undefined;
|
||||||
@@ -288,20 +288,20 @@ type NullAndUndefined<T> = T | null | undefined;
|
|||||||
type ChapterSortMode = 'fetchedAt' | 'source';
|
type ChapterSortMode = 'fetchedAt' | 'source';
|
||||||
|
|
||||||
interface ChapterListOptions {
|
interface ChapterListOptions {
|
||||||
active: boolean
|
active: boolean;
|
||||||
unread: NullAndUndefined<boolean>
|
unread: NullAndUndefined<boolean>;
|
||||||
downloaded: NullAndUndefined<boolean>
|
downloaded: NullAndUndefined<boolean>;
|
||||||
bookmarked: NullAndUndefined<boolean>
|
bookmarked: NullAndUndefined<boolean>;
|
||||||
reverse: boolean
|
reverse: boolean;
|
||||||
sortBy: ChapterSortMode
|
sortBy: ChapterSortMode;
|
||||||
showChapterNumber: boolean
|
showChapterNumber: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChapterOptionsReducerAction =
|
type ChapterOptionsReducerAction =
|
||||||
{ type: 'filter', filterType:string, filterValue: NullAndUndefined<boolean> }
|
| { type: 'filter'; filterType: string; filterValue: NullAndUndefined<boolean> }
|
||||||
| { type: 'sortBy', sortBy: ChapterSortMode }
|
| { type: 'sortBy'; sortBy: ChapterSortMode }
|
||||||
| { type: 'sortReverse' }
|
| { type: 'sortReverse' }
|
||||||
| { type: 'showChapterNumber' };
|
| { type: 'showChapterNumber' };
|
||||||
|
|
||||||
type LibrarySortMode = 'sortToRead' | 'sortAlph' | 'sortID';
|
type LibrarySortMode = 'sortToRead' | 'sortAlph' | 'sortID';
|
||||||
|
|
||||||
@@ -313,21 +313,21 @@ enum GridLayout {
|
|||||||
|
|
||||||
interface LibraryOptions {
|
interface LibraryOptions {
|
||||||
// display options
|
// display options
|
||||||
showDownloadBadge: boolean
|
showDownloadBadge: boolean;
|
||||||
showUnreadBadge: boolean
|
showUnreadBadge: boolean;
|
||||||
gridLayout: GridLayout
|
gridLayout: GridLayout;
|
||||||
SourcegridLayout: GridLayout
|
SourcegridLayout: GridLayout;
|
||||||
|
|
||||||
// filter options
|
// filter options
|
||||||
downloaded: NullAndUndefined<boolean>
|
downloaded: NullAndUndefined<boolean>;
|
||||||
unread: NullAndUndefined<boolean>
|
unread: NullAndUndefined<boolean>;
|
||||||
sorts: NullAndUndefined<LibrarySortMode>
|
sorts: NullAndUndefined<LibrarySortMode>;
|
||||||
sortDesc: NullAndUndefined<boolean>
|
sortDesc: NullAndUndefined<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BatchChaptersChange {
|
interface BatchChaptersChange {
|
||||||
delete?: boolean
|
delete?: boolean;
|
||||||
isRead?: boolean
|
isRead?: boolean;
|
||||||
isBookmarked?: boolean
|
isBookmarked?: boolean;
|
||||||
lastPageRead?: number
|
lastPageRead?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ const { hostname, port, protocol } = window.location;
|
|||||||
|
|
||||||
// if port is 3000 it's probably running from webpack devlopment server
|
// if port is 3000 it's probably running from webpack devlopment server
|
||||||
let inferredPort;
|
let inferredPort;
|
||||||
if (port === '3000') { inferredPort = '4567'; } else { inferredPort = port; }
|
if (port === '3000') {
|
||||||
|
inferredPort = '4567';
|
||||||
|
} else {
|
||||||
|
inferredPort = port;
|
||||||
|
}
|
||||||
|
|
||||||
const baseURL = storage.getItem('serverBaseURL', `${protocol}//${hostname}:${inferredPort}`);
|
const baseURL = storage.getItem('serverBaseURL', `${protocol}//${hostname}:${inferredPort}`);
|
||||||
|
|
||||||
@@ -42,10 +46,7 @@ export async function fetcher<T = any>(path: string) {
|
|||||||
return res.data as T;
|
return res.data as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useQuery = <
|
export const useQuery = <D extends any = any, E extends any = any>(
|
||||||
D extends any = any,
|
|
||||||
E extends any = any,
|
|
||||||
>(
|
|
||||||
key: string,
|
key: string,
|
||||||
config?: SWRConfiguration<D, E>,
|
config?: SWRConfiguration<D, E>,
|
||||||
): SWRResponse<D, E> & { loading: boolean } => {
|
): SWRResponse<D, E> & { loading: boolean } => {
|
||||||
|
|||||||
@@ -47,13 +47,10 @@ export const getUploadDateString = (date: Date | number) => {
|
|||||||
|
|
||||||
const addTimeString = wasUploadedToday || wasUploadedYesterday;
|
const addTimeString = wasUploadedToday || wasUploadedYesterday;
|
||||||
const timeString = addTimeString
|
const timeString = addTimeString
|
||||||
? uploadDate.toLocaleTimeString(
|
? uploadDate.toLocaleTimeString(undefined, {
|
||||||
undefined,
|
hour: '2-digit',
|
||||||
{
|
minute: '2-digit',
|
||||||
hour: '2-digit',
|
})
|
||||||
minute: '2-digit',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
if (wasUploadedToday) {
|
if (wasUploadedToday) {
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ export const ISOLanguages = [
|
|||||||
{ code: 'bs', name: 'Bosnian', nativeName: 'bosanski' },
|
{ code: 'bs', name: 'Bosnian', nativeName: 'bosanski' },
|
||||||
{ code: 'sv', name: 'Swedish', nativeName: 'svenska' },
|
{ code: 'sv', name: 'Swedish', nativeName: 'svenska' },
|
||||||
{ code: 'sv', name: 'Swedish', nativeName: 'svenska' },
|
{ code: 'sv', name: 'Swedish', nativeName: 'svenska' },
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function langCodeToName(code: string): string {
|
export function langCodeToName(code: string): string {
|
||||||
@@ -84,8 +83,10 @@ export function langCodeToName(code: string): string {
|
|||||||
let result = `language with code: ${code}`;
|
let result = `language with code: ${code}`;
|
||||||
|
|
||||||
for (let i = 0; i < ISOLanguages.length; i++) {
|
for (let i = 0; i < ISOLanguages.length; i++) {
|
||||||
if (ISOLanguages[i].code === proccessedCode
|
if (
|
||||||
|| ISOLanguages[i].code === code.toLocaleLowerCase()) {
|
ISOLanguages[i].code === proccessedCode ||
|
||||||
|
ISOLanguages[i].code === code.toLocaleLowerCase()
|
||||||
|
) {
|
||||||
result = ISOLanguages[i].nativeName;
|
result = ISOLanguages[i].nativeName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,23 +99,15 @@ function defaultNativeLang() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extensionDefaultLangs() {
|
export function extensionDefaultLangs() {
|
||||||
return [
|
return [defaultNativeLang(), 'all'];
|
||||||
defaultNativeLang(),
|
|
||||||
'all',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sourceDefualtLangs() {
|
export function sourceDefualtLangs() {
|
||||||
return [
|
return [defaultNativeLang(), 'localsourcelang'];
|
||||||
defaultNativeLang(),
|
|
||||||
'localsourcelang',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sourceForcedDefaultLangs(): string[] {
|
export function sourceForcedDefaultLangs(): string[] {
|
||||||
return [
|
return ['localsourcelang'];
|
||||||
'localsourcelang',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const langSortCmp = (a: string, b: string) => {
|
export const langSortCmp = (a: string, b: string) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
function getItem<T>(key: string, defaultValue: T) : T {
|
function getItem<T>(key: string, defaultValue: T): T {
|
||||||
try {
|
try {
|
||||||
const item = window.localStorage.getItem(key);
|
const item = window.localStorage.getItem(key);
|
||||||
|
|
||||||
@@ -16,7 +16,8 @@ function getItem<T>(key: string, defaultValue: T) : T {
|
|||||||
window.localStorage.setItem(key, JSON.stringify(defaultValue));
|
window.localStorage.setItem(key, JSON.stringify(defaultValue));
|
||||||
|
|
||||||
/* eslint-disable no-empty */
|
/* eslint-disable no-empty */
|
||||||
} finally { }
|
} finally {
|
||||||
|
}
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +26,8 @@ function setItem<T>(key: string, value: T): void {
|
|||||||
window.localStorage.setItem(key, JSON.stringify(value));
|
window.localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
|
||||||
// eslint-disable-next-line no-empty
|
// eslint-disable-next-line no-empty
|
||||||
} finally { }
|
} finally {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { getItem, setItem };
|
export default { getItem, setItem };
|
||||||
|
|||||||
@@ -13,13 +13,12 @@ const APP_METADATA_KEY_PREFIX = 'webUI_';
|
|||||||
|
|
||||||
const migrations: IMetadataMigration[] = [
|
const migrations: IMetadataMigration[] = [
|
||||||
{
|
{
|
||||||
keys: [
|
keys: [{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' }],
|
||||||
{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => `${appPrefix}${key}`;
|
const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) =>
|
||||||
|
`${appPrefix}${key}`;
|
||||||
|
|
||||||
const doesMetadataKeyExistIn = (
|
const doesMetadataKeyExistIn = (
|
||||||
meta: IMetadata | undefined,
|
meta: IMetadata | undefined,
|
||||||
@@ -27,9 +26,7 @@ const doesMetadataKeyExistIn = (
|
|||||||
appPrefix?: string,
|
appPrefix?: string,
|
||||||
): boolean => Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix));
|
): boolean => Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix));
|
||||||
|
|
||||||
const convertValueFromMetadata = <
|
const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes>(
|
||||||
T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes,
|
|
||||||
>(
|
|
||||||
value: string,
|
value: string,
|
||||||
): T => {
|
): T => {
|
||||||
if (!Number.isNaN(+value)) {
|
if (!Number.isNaN(+value)) {
|
||||||
@@ -74,8 +71,9 @@ const applyAppKeyPrefixMigration = (meta: IMetadata, migration: IMetadataMigrati
|
|||||||
const oldAppMetadata = getAppMetadataFrom(meta, oldPrefix);
|
const oldAppMetadata = getAppMetadataFrom(meta, oldPrefix);
|
||||||
const newAppMetadata = getAppMetadataFrom(meta, newPrefix);
|
const newAppMetadata = getAppMetadataFrom(meta, newPrefix);
|
||||||
|
|
||||||
const missingMetadataKeys = Object.keys(oldAppMetadata)
|
const missingMetadataKeys = Object.keys(oldAppMetadata).filter(
|
||||||
.filter((key) => !Object.keys(newAppMetadata).includes(key));
|
(key) => !Object.keys(newAppMetadata).includes(key),
|
||||||
|
);
|
||||||
|
|
||||||
const isMissingOldMetadata = missingMetadataKeys.length;
|
const isMissingOldMetadata = missingMetadataKeys.length;
|
||||||
if (isMissingOldMetadata) {
|
if (isMissingOldMetadata) {
|
||||||
@@ -219,16 +217,25 @@ export const requestUpdateMetadata = async (
|
|||||||
keysToValues: [AppMetadataKeys, AllowedMetadataValueTypes][],
|
keysToValues: [AppMetadataKeys, AllowedMetadataValueTypes][],
|
||||||
endpointToMutate?: string,
|
endpointToMutate?: string,
|
||||||
wrapWithMetaKey?: boolean,
|
wrapWithMetaKey?: boolean,
|
||||||
): Promise<void[]> => Promise.all(keysToValues.map(
|
): Promise<void[]> =>
|
||||||
([key, value]) => requestUpdateMetadataValue(
|
Promise.all(
|
||||||
endpoint, metadataHolder, key, value, endpointToMutate, wrapWithMetaKey,
|
keysToValues.map(([key, value]) =>
|
||||||
),
|
requestUpdateMetadataValue(
|
||||||
));
|
endpoint,
|
||||||
|
metadataHolder,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
endpointToMutate,
|
||||||
|
wrapWithMetaKey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const requestUpdateServerMetadata = async (
|
export const requestUpdateServerMetadata = async (
|
||||||
serverMetadata: IMetadata,
|
serverMetadata: IMetadata,
|
||||||
keysToValues: MetadataKeyValuePair[],
|
keysToValues: MetadataKeyValuePair[],
|
||||||
): Promise<void[]> => requestUpdateMetadata('', { meta: serverMetadata }, keysToValues, '/meta', false);
|
): Promise<void[]> =>
|
||||||
|
requestUpdateMetadata('', { meta: serverMetadata }, keysToValues, '/meta', false);
|
||||||
|
|
||||||
export const requestUpdateMangaMetadata = async (
|
export const requestUpdateMangaMetadata = async (
|
||||||
manga: IMangaCard | IManga,
|
manga: IMangaCard | IManga,
|
||||||
@@ -238,7 +245,12 @@ export const requestUpdateMangaMetadata = async (
|
|||||||
export const requestUpdateChapterMetadata = async (
|
export const requestUpdateChapterMetadata = async (
|
||||||
mangaChapter: IMangaChapter,
|
mangaChapter: IMangaChapter,
|
||||||
keysToValues: MetadataKeyValuePair[],
|
keysToValues: MetadataKeyValuePair[],
|
||||||
): Promise<void[]> => requestUpdateMetadata(`/manga/${mangaChapter.manga.id}/chapter/${mangaChapter.chapter.index}`, mangaChapter.chapter, keysToValues);
|
): Promise<void[]> =>
|
||||||
|
requestUpdateMetadata(
|
||||||
|
`/manga/${mangaChapter.manga.id}/chapter/${mangaChapter.chapter.index}`,
|
||||||
|
mangaChapter.chapter,
|
||||||
|
keysToValues,
|
||||||
|
);
|
||||||
|
|
||||||
export const requestUpdateCategoryMetadata = async (
|
export const requestUpdateCategoryMetadata = async (
|
||||||
category: ICategory,
|
category: ICategory,
|
||||||
|
|||||||
@@ -6,27 +6,32 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getMetadataFrom, requestUpdateMangaMetadata, requestUpdateServerMetadata } from 'util/metadata';
|
import {
|
||||||
|
getMetadataFrom,
|
||||||
|
requestUpdateMangaMetadata,
|
||||||
|
requestUpdateServerMetadata,
|
||||||
|
} from 'util/metadata';
|
||||||
import { useQuery } from 'util/client';
|
import { useQuery } from 'util/client';
|
||||||
|
|
||||||
export const getDefaultSettings = (forceUndefined: boolean = false) => ({
|
export const getDefaultSettings = (forceUndefined: boolean = false) =>
|
||||||
staticNav: forceUndefined ? undefined : false,
|
({
|
||||||
showPageNumber: forceUndefined ? undefined : true,
|
staticNav: forceUndefined ? undefined : false,
|
||||||
continuesPageGap: forceUndefined ? undefined : false,
|
showPageNumber: forceUndefined ? undefined : true,
|
||||||
loadNextOnEnding: forceUndefined ? undefined : false,
|
continuesPageGap: forceUndefined ? undefined : false,
|
||||||
readerType: forceUndefined ? undefined : 'ContinuesVertical',
|
loadNextOnEnding: forceUndefined ? undefined : false,
|
||||||
} as IReaderSettings);
|
readerType: forceUndefined ? undefined : 'ContinuesVertical',
|
||||||
|
} as IReaderSettings);
|
||||||
|
|
||||||
const getReaderSettingsWithDefaultValueFallback = (
|
const getReaderSettingsWithDefaultValueFallback = (
|
||||||
meta?: IMetadata,
|
meta?: IMetadata,
|
||||||
defaultSettings?: IReaderSettings,
|
defaultSettings?: IReaderSettings,
|
||||||
applyMetadataMigration: boolean = true,
|
applyMetadataMigration: boolean = true,
|
||||||
): IReaderSettings => ({
|
): IReaderSettings => ({
|
||||||
...getMetadataFrom(
|
...(getMetadataFrom(
|
||||||
{ meta },
|
{ meta },
|
||||||
Object.entries(defaultSettings ?? getDefaultSettings()) as MetadataKeyValuePair[],
|
Object.entries(defaultSettings ?? getDefaultSettings()) as MetadataKeyValuePair[],
|
||||||
applyMetadataMigration,
|
applyMetadataMigration,
|
||||||
) as unknown as IReaderSettings,
|
) as unknown as IReaderSettings),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getReaderSettingsFromMetadata = (
|
export const getReaderSettingsFromMetadata = (
|
||||||
@@ -44,9 +49,9 @@ export const getReaderSettingsFor = (
|
|||||||
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings, applyMetadataMigration);
|
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings, applyMetadataMigration);
|
||||||
|
|
||||||
export const useDefaultReaderSettings = (): {
|
export const useDefaultReaderSettings = (): {
|
||||||
metadata?: IMetadata,
|
metadata?: IMetadata;
|
||||||
settings: IReaderSettings,
|
settings: IReaderSettings;
|
||||||
loading: boolean
|
loading: boolean;
|
||||||
} => {
|
} => {
|
||||||
const { data: meta, loading } = useQuery<IMetadata>('/api/v1/meta');
|
const { data: meta, loading } = useQuery<IMetadata>('/api/v1/meta');
|
||||||
const settings = getReaderSettingsWithDefaultValueFallback(meta);
|
const settings = getReaderSettingsWithDefaultValueFallback(meta);
|
||||||
@@ -66,12 +71,13 @@ export const checkAndHandleMissingStoredReaderSettings = async (
|
|||||||
metadataHolderType: 'manga' | 'server',
|
metadataHolderType: 'manga' | 'server',
|
||||||
defaultSettings: IReaderSettings,
|
defaultSettings: IReaderSettings,
|
||||||
): Promise<void | void[]> => {
|
): Promise<void | void[]> => {
|
||||||
const meta = metadataHolder.meta ?? metadataHolder as IMetadata;
|
const meta = metadataHolder.meta ?? (metadataHolder as IMetadata);
|
||||||
const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true), false);
|
const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true), false);
|
||||||
const newSettings = getReaderSettingsFor({ meta }, defaultSettings);
|
const newSettings = getReaderSettingsFor({ meta }, defaultSettings);
|
||||||
|
|
||||||
const undefinedSettings = Object.entries(settingsToCheck)
|
const undefinedSettings = Object.entries(settingsToCheck).filter(
|
||||||
.filter((setting) => setting[1] === undefined);
|
(setting) => setting[1] === undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const settingsToUpdate: MetadataKeyValuePair[] = [];
|
const settingsToUpdate: MetadataKeyValuePair[] = [];
|
||||||
undefinedSettings.forEach((setting) => {
|
undefinedSettings.forEach((setting) => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useLocation } from 'react-router-dom';
|
|||||||
|
|
||||||
export const BACK = '__BACK__';
|
export const BACK = '__BACK__';
|
||||||
|
|
||||||
const useBackTo = (): { url?: string, back: boolean } => {
|
const useBackTo = (): { url?: string; back: boolean } => {
|
||||||
const location = useLocation<{ backLink?: string }>();
|
const location = useLocation<{ backLink?: string }>();
|
||||||
const { defaultBackTo } = useNavBarContext();
|
const { defaultBackTo } = useNavBarContext();
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, {
|
import React, { useState, Dispatch, SetStateAction, useReducer, Reducer, useCallback } from 'react';
|
||||||
useState,
|
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
useReducer,
|
|
||||||
Reducer,
|
|
||||||
useCallback,
|
|
||||||
} from 'react';
|
|
||||||
import storage from 'util/localStorage';
|
import storage from 'util/localStorage';
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
@@ -21,19 +14,18 @@ export default function useLocalStorage<T>(
|
|||||||
defaultValue: T | (() => T),
|
defaultValue: T | (() => T),
|
||||||
): [T, Dispatch<SetStateAction<T>>] {
|
): [T, Dispatch<SetStateAction<T>>] {
|
||||||
const initialState = defaultValue instanceof Function ? defaultValue() : defaultValue;
|
const initialState = defaultValue instanceof Function ? defaultValue() : defaultValue;
|
||||||
const [storedValue, setStoredValue] = useState<T>(
|
const [storedValue, setStoredValue] = useState<T>(storage.getItem(key, initialState));
|
||||||
storage.getItem(key, initialState),
|
|
||||||
);
|
|
||||||
|
|
||||||
const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(
|
const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(
|
||||||
((value) => {
|
(value) => {
|
||||||
setStoredValue((prevValue) => {
|
setStoredValue((prevValue) => {
|
||||||
// Allow value to be a function so we have same API as useState
|
// Allow value to be a function so we have same API as useState
|
||||||
const valueToStore = value instanceof Function ? value(prevValue) : value;
|
const valueToStore = value instanceof Function ? value(prevValue) : value;
|
||||||
storage.setItem(key, valueToStore);
|
storage.setItem(key, valueToStore);
|
||||||
return valueToStore;
|
return valueToStore;
|
||||||
});
|
});
|
||||||
}), [key],
|
},
|
||||||
|
[key],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [storedValue, setValue];
|
return [storedValue, setValue];
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user