move webUI out of Tachidesk
This commit is contained in:
123
src/components/manga/CategorySelect.tsx
Normal file
123
src/components/manga/CategorySelect.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { makeStyles, createStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import client from 'util/client';
|
||||
|
||||
const useStyles = makeStyles(() => createStyles({
|
||||
paper: {
|
||||
maxHeight: 435,
|
||||
width: '80%',
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps {
|
||||
open: boolean
|
||||
setOpen: (value: boolean) => void
|
||||
mangaId: number
|
||||
}
|
||||
|
||||
interface ICategoryInfo {
|
||||
category: ICategory
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
export default function CategorySelect(props: IProps) {
|
||||
const classes = useStyles();
|
||||
const { open, setOpen, mangaId } = props;
|
||||
const [categoryInfos, setCategoryInfos] = useState<ICategoryInfo[]>([]);
|
||||
|
||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
||||
|
||||
useEffect(() => {
|
||||
let tmpCategoryInfos: ICategoryInfo[] = [];
|
||||
client.get('/api/v1/category/')
|
||||
.then((response) => response.data)
|
||||
.then((data: ICategory[]) => {
|
||||
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
|
||||
})
|
||||
.then(() => {
|
||||
client.get(`/api/v1/manga/${mangaId}/category/`)
|
||||
.then((response) => response.data)
|
||||
.then((data: ICategory[]) => {
|
||||
data.forEach((category) => {
|
||||
tmpCategoryInfos[category.order - 1].selected = true;
|
||||
});
|
||||
setCategoryInfos(tmpCategoryInfos);
|
||||
});
|
||||
});
|
||||
}, [updateTriggerHolder, open]);
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, categoryId: number) => {
|
||||
const { checked } = event.target as HTMLInputElement;
|
||||
|
||||
const method = checked ? client.get : client.delete;
|
||||
method(`/api/v1/manga/${mangaId}/category/${categoryId}`)
|
||||
.then(() => triggerUpdate());
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
classes={classes}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle>Set categories</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<FormGroup>
|
||||
{categoryInfos.length === 0
|
||||
&& (
|
||||
<span>
|
||||
No categories found!
|
||||
<br />
|
||||
You should make some from settings.
|
||||
</span>
|
||||
)}
|
||||
{categoryInfos.map((categoryInfo) => (
|
||||
<FormControlLabel
|
||||
control={(
|
||||
<Checkbox
|
||||
checked={categoryInfo.selected}
|
||||
onChange={(e) => handleChange(e, categoryInfo.category.id)}
|
||||
color="default"
|
||||
/>
|
||||
)}
|
||||
label={categoryInfo.category.name}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary">
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
146
src/components/manga/ChapterCard.tsx
Normal file
146
src/components/manga/ChapterCard.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { makeStyles, useTheme } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import BookmarkIcon from '@material-ui/icons/Bookmark';
|
||||
import client from 'util/client';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
bullet: {
|
||||
display: 'inline-block',
|
||||
margin: '0 2px',
|
||||
transform: 'scale(0.8)',
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
},
|
||||
pos: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
icon: {
|
||||
width: theme.spacing(7),
|
||||
height: theme.spacing(7),
|
||||
flex: '0 0 auto',
|
||||
marginRight: 16,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps{
|
||||
chapter: IChapter
|
||||
triggerChaptersUpdate: () => void
|
||||
downloadingString: string
|
||||
}
|
||||
|
||||
export default function ChapterCard(props: IProps) {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const { chapter, triggerChaptersUpdate, downloadingString } = props;
|
||||
|
||||
const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10);
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const sendChange = (key: string, value: any) => {
|
||||
handleClose();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append(key, value);
|
||||
client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData)
|
||||
.then(() => triggerChaptersUpdate());
|
||||
};
|
||||
|
||||
const downloadChapter = () => {
|
||||
client.get(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0';
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
<Card>
|
||||
<CardContent className={classes.root}>
|
||||
<Link
|
||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
color: chapter.read ? readChapterColor : theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h5" component="h2">
|
||||
<span style={{ color: theme.palette.primary.dark }}>
|
||||
{chapter.bookmarked && <BookmarkIcon />}
|
||||
</span>
|
||||
{chapter.name}
|
||||
{chapter.chapterNumber > 0 && ` : ${chapter.chapterNumber}`}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" gutterBottom>
|
||||
{chapter.scanlator}
|
||||
{chapter.scanlator && ' '}
|
||||
{dateStr}
|
||||
{downloadingString}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<IconButton aria-label="more" onClick={handleClick}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{downloadingString.length === 0
|
||||
&& <MenuItem onClick={downloadChapter}>Download</MenuItem> }
|
||||
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
||||
{chapter.bookmarked && 'Remove bookmark'}
|
||||
{!chapter.bookmarked && 'Bookmark'}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => sendChange('read', !chapter.read)}>
|
||||
Mark as
|
||||
{' '}
|
||||
{chapter.read && 'unread'}
|
||||
{!chapter.read && 'read'}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => sendChange('markPrevRead', true)}>
|
||||
Mark previous as Read
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
149
src/components/manga/ExtensionCard.tsx
Normal file
149
src/components/manga/ExtensionCard.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import client from 'util/client';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
bullet: {
|
||||
display: 'inline-block',
|
||||
margin: '0 2px',
|
||||
transform: 'scale(0.8)',
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
},
|
||||
pos: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
icon: {
|
||||
width: theme.spacing(7),
|
||||
height: theme.spacing(7),
|
||||
flex: '0 0 auto',
|
||||
marginRight: 16,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps {
|
||||
extension: IExtension
|
||||
notifyInstall: () => void
|
||||
}
|
||||
|
||||
export default function ExtensionCard(props: IProps) {
|
||||
const {
|
||||
extension: {
|
||||
name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl,
|
||||
},
|
||||
notifyInstall,
|
||||
} = props;
|
||||
const [installedState, setInstalledState] = useState<string>(
|
||||
() => {
|
||||
if (obsolete) { return 'obsolete'; }
|
||||
if (hasUpdate) { return 'update'; }
|
||||
return (installed ? 'uninstall' : 'install');
|
||||
},
|
||||
);
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
const classes = useStyles();
|
||||
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
||||
|
||||
function install() {
|
||||
setInstalledState('installing');
|
||||
client.get(`/api/v1/extension/install/${pkgName}`)
|
||||
.then(() => {
|
||||
setInstalledState('uninstall');
|
||||
notifyInstall();
|
||||
});
|
||||
}
|
||||
|
||||
function update() {
|
||||
setInstalledState('updating');
|
||||
client.get(`/api/v1/extension/update/${pkgName}`)
|
||||
.then(() => {
|
||||
setInstalledState('uninstall');
|
||||
notifyInstall();
|
||||
});
|
||||
}
|
||||
|
||||
function uninstall() {
|
||||
setInstalledState('uninstalling');
|
||||
client.get(`/api/v1/extension/uninstall/${pkgName}`)
|
||||
.then(() => {
|
||||
// setInstalledState('install');
|
||||
notifyInstall();
|
||||
});
|
||||
}
|
||||
|
||||
function handleButtonClick() {
|
||||
switch (installedState) {
|
||||
case 'install':
|
||||
install();
|
||||
break;
|
||||
case 'update':
|
||||
update();
|
||||
break;
|
||||
case 'obsolete':
|
||||
uninstall();
|
||||
setTimeout(() => window.location.reload(), 3000);
|
||||
break;
|
||||
case 'uninstall':
|
||||
uninstall();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className={classes.root}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
className={classes.icon}
|
||||
alt={name}
|
||||
src={serverAddress + iconUrl}
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h5" component="h2">
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" gutterBottom>
|
||||
{langPress}
|
||||
{' '}
|
||||
{versionName}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
style={{ color: installedState === 'obsolete' ? 'red' : 'inherit' }}
|
||||
onClick={() => handleButtonClick()}
|
||||
>
|
||||
{installedState}
|
||||
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
109
src/components/manga/ExtensionLangSelect.tsx
Normal file
109
src/components/manga/ExtensionLangSelect.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { makeStyles, createStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import { langCodeToName } from 'util/language';
|
||||
import cloneObject from 'util/cloneObject';
|
||||
|
||||
const useStyles = makeStyles(() => createStyles({
|
||||
paper: {
|
||||
maxHeight: 435,
|
||||
width: '80%',
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps {
|
||||
shownLangs: string[]
|
||||
setShownLangs: (arg0: string[]) => void
|
||||
allLangs: string[]
|
||||
}
|
||||
|
||||
export default function ExtensionLangSelect(props: IProps) {
|
||||
const { shownLangs, setShownLangs, allLangs } = props;
|
||||
// hold a copy and only sate state on parent when OK pressed, improves performance
|
||||
const [mShownLangs, setMShownLangs] = useState(shownLangs);
|
||||
const classes = useStyles();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
setOpen(false);
|
||||
setShownLangs(mShownLangs);
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, lang: string) => {
|
||||
const { checked } = event.target as HTMLInputElement;
|
||||
|
||||
if (checked) {
|
||||
setMShownLangs([...mShownLangs, lang]);
|
||||
} else {
|
||||
const clone = cloneObject(mShownLangs);
|
||||
clone.splice(clone.indexOf(lang), 1);
|
||||
setMShownLangs(clone);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
<Dialog
|
||||
classes={classes}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle>Enabled Languages</DialogTitle>
|
||||
<DialogContent dividers style={{ padding: 0 }}>
|
||||
<List>
|
||||
{allLangs.map((lang) => (
|
||||
<ListItem key={lang}>
|
||||
<ListItemText primary={langCodeToName(lang)} />
|
||||
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
checked={mShownLangs.indexOf(lang) !== -1}
|
||||
onChange={(e) => handleChange(e, lang)}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary">
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
87
src/components/manga/MangaCard.tsx
Normal file
87
src/components/manga/MangaCard.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardActionArea from '@material-ui/core/CardActionArea';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
import SpinnerImage from 'components/SpinnerImage';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
},
|
||||
wrapper: {
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
},
|
||||
gradient: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(to bottom, transparent, #000000)',
|
||||
opacity: 0.5,
|
||||
},
|
||||
title: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
padding: '0.5em',
|
||||
color: 'white',
|
||||
},
|
||||
image: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
},
|
||||
|
||||
spinner: {
|
||||
minHeight: '400px',
|
||||
padding: '180px calc(50% - 20px)',
|
||||
},
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
manga: IMangaCard
|
||||
}
|
||||
const MangaCard = React.forwardRef((props: IProps, ref) => {
|
||||
const {
|
||||
manga: {
|
||||
id, title, thumbnailUrl,
|
||||
},
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
return (
|
||||
<Grid item xs={6} sm={4} md={3} lg={2}>
|
||||
<Link to={`/manga/${id}/`}>
|
||||
<Card className={classes.root} ref={ref}>
|
||||
<CardActionArea>
|
||||
<div className={classes.wrapper}>
|
||||
<SpinnerImage
|
||||
alt={title}
|
||||
src={serverAddress + thumbnailUrl}
|
||||
spinnerClassName={classes.spinner}
|
||||
imgClassName={classes.image}
|
||||
/>
|
||||
<div className={classes.gradient} />
|
||||
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
|
||||
</div>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Link>
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
||||
export default MangaCard;
|
||||
257
src/components/manga/MangaDetails.tsx
Normal file
257
src/components/manga/MangaDetails.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { Theme } from '@material-ui/core/styles';
|
||||
import FavoriteIcon from '@material-ui/icons/Favorite';
|
||||
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
|
||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||
import PublicIcon from '@material-ui/icons/Public';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import NavbarContext from 'context/NavbarContext';
|
||||
import client from 'util/client';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
import CategorySelect from './CategorySelect';
|
||||
|
||||
const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
position: 'sticky',
|
||||
top: '64px',
|
||||
left: '0px',
|
||||
width: '50vw',
|
||||
height: 'calc(100vh - 64px)',
|
||||
alignSelf: 'flex-start',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
top: {
|
||||
padding: '10px',
|
||||
// [theme.breakpoints.up('md')]: {
|
||||
// minWidth: '50%',
|
||||
// },
|
||||
},
|
||||
leftRight: {
|
||||
display: 'flex',
|
||||
},
|
||||
leftSide: {
|
||||
'& img': {
|
||||
borderRadius: 4,
|
||||
maxWidth: '100%',
|
||||
minWidth: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
maxWidth: '50%',
|
||||
// [theme.breakpoints.up('md')]: {
|
||||
// minWidth: '100px',
|
||||
// },
|
||||
},
|
||||
rightSide: {
|
||||
marginLeft: 15,
|
||||
maxWidth: '100%',
|
||||
'& span': {
|
||||
fontWeight: '400',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
fontSize: '1.3em',
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
'& button': {
|
||||
color: inLibrary === 'In Library' ? '#2196f3' : 'inherit',
|
||||
},
|
||||
'& span': {
|
||||
display: 'block',
|
||||
fontSize: '0.85em',
|
||||
},
|
||||
'& a': {
|
||||
textDecoration: 'none',
|
||||
color: '#858585',
|
||||
'& button': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
},
|
||||
bottom: {
|
||||
paddingLeft: '10px',
|
||||
paddingRight: '10px',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
fontSize: '1.2em',
|
||||
// maxWidth: '50%',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
fontSize: '1.3em',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
'& h4': {
|
||||
marginTop: '1em',
|
||||
marginBottom: 0,
|
||||
},
|
||||
'& p': {
|
||||
textAlign: 'justify',
|
||||
textJustify: 'inter-word',
|
||||
},
|
||||
},
|
||||
genre: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
'& h5': {
|
||||
border: '2px solid #2196f3',
|
||||
borderRadius: '1.13em',
|
||||
marginRight: '1em',
|
||||
marginTop: 0,
|
||||
marginBottom: '10px',
|
||||
padding: '0.3em',
|
||||
color: '#2196f3',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps{
|
||||
manga: IManga
|
||||
}
|
||||
|
||||
function getSourceName(source: ISource) {
|
||||
if (source.name !== null) {
|
||||
return `${source.name} (${source.lang.toLocaleUpperCase()})`;
|
||||
}
|
||||
return source.id;
|
||||
}
|
||||
|
||||
function getValueOrUnknown(val: string) {
|
||||
return val || 'UNKNOWN';
|
||||
}
|
||||
|
||||
export default function MangaDetails(props: IProps) {
|
||||
const { setAction } = useContext(NavbarContext);
|
||||
|
||||
const { manga } = props;
|
||||
if (manga.genre == null) {
|
||||
manga.genre = '';
|
||||
}
|
||||
const [inLibrary, setInLibrary] = useState<string>(
|
||||
manga.inLibrary ? 'In Library' : 'Add To Library',
|
||||
);
|
||||
|
||||
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (inLibrary === 'In Library') {
|
||||
setAction(
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => setCategoryDialogOpen(true)}
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
<CategorySelect
|
||||
open={categoryDialogOpen}
|
||||
setOpen={setCategoryDialogOpen}
|
||||
mangaId={manga.id}
|
||||
/>
|
||||
</>,
|
||||
|
||||
);
|
||||
} else { setAction(<></>); }
|
||||
}, [inLibrary, categoryDialogOpen]);
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
const classes = useStyles(inLibrary)();
|
||||
|
||||
function addToLibrary() {
|
||||
// setInLibrary('adding');
|
||||
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||
setInLibrary('In Library');
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromLibrary() {
|
||||
// setInLibrary('removing');
|
||||
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||
setInLibrary('Add To Library');
|
||||
});
|
||||
}
|
||||
|
||||
function handleButtonClick() {
|
||||
if (inLibrary === 'Add To Library') {
|
||||
addToLibrary();
|
||||
} else {
|
||||
removeFromLibrary();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.top}>
|
||||
<div className={classes.leftRight}>
|
||||
<div className={classes.leftSide}>
|
||||
<img src={`${serverAddress}${manga.thumbnailUrl}`} alt="Manga Thumbnail" />
|
||||
</div>
|
||||
<div className={classes.rightSide}>
|
||||
<h1>
|
||||
{manga.title}
|
||||
</h1>
|
||||
<h3>
|
||||
Author:
|
||||
{' '}
|
||||
<span>{getValueOrUnknown(manga.author)}</span>
|
||||
</h3>
|
||||
<h3>
|
||||
Artist:
|
||||
{' '}
|
||||
<span>{getValueOrUnknown(manga.artist)}</span>
|
||||
</h3>
|
||||
<h3>
|
||||
Status:
|
||||
{' '}
|
||||
{manga.status}
|
||||
</h3>
|
||||
<h3>
|
||||
Source:
|
||||
{' '}
|
||||
{getSourceName(manga.source)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.buttons}>
|
||||
<div>
|
||||
<IconButton onClick={() => handleButtonClick()}>
|
||||
{inLibrary === 'In Library' && <FavoriteIcon />}
|
||||
{inLibrary !== 'In Library' && <FavoriteBorderIcon />}
|
||||
<span>{inLibrary}</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
{ /* eslint-disable-next-line react/jsx-no-target-blank */ }
|
||||
<a href={manga.url} target="_blank">
|
||||
<IconButton>
|
||||
<PublicIcon />
|
||||
<span>Open Site</span>
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.bottom}>
|
||||
<div className={classes.description}>
|
||||
<h4>About</h4>
|
||||
<p>{manga.description}</p>
|
||||
</div>
|
||||
<div className={classes.genre}>
|
||||
{manga.genre.split(', ').map((g) => <h5 key={g}>{g}</h5>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
src/components/manga/MangaGrid.tsx
Normal file
62
src/components/manga/MangaGrid.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import MangaCard from './MangaCard';
|
||||
|
||||
interface IProps{
|
||||
mangas: IMangaCard[]
|
||||
message?: string
|
||||
hasNextPage: boolean
|
||||
lastPageNum: number
|
||||
setLastPageNum: (lastPageNum: number) => void
|
||||
}
|
||||
|
||||
export default function MangaGrid(props: IProps) {
|
||||
const {
|
||||
mangas, message, hasNextPage, lastPageNum, setLastPageNum,
|
||||
} = props;
|
||||
let mapped;
|
||||
const lastManga = useRef<HTMLInputElement>();
|
||||
|
||||
const scrollHandler = () => {
|
||||
if (lastManga.current) {
|
||||
const rect = lastManga.current.getBoundingClientRect();
|
||||
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) {
|
||||
setLastPageNum(lastPageNum + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', scrollHandler, true);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollHandler, true);
|
||||
};
|
||||
}, [hasNextPage, mangas]);
|
||||
|
||||
if (mangas.length === 0) {
|
||||
mapped = <h3>{message}</h3>;
|
||||
} else {
|
||||
mapped = mangas.map((it, idx) => {
|
||||
if (idx === mangas.length - 1) {
|
||||
return <MangaCard manga={it} ref={lastManga} />;
|
||||
}
|
||||
return <MangaCard manga={it} />;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
|
||||
{mapped}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
MangaGrid.defaultProps = {
|
||||
message: 'loading...',
|
||||
};
|
||||
87
src/components/manga/SourceCard.tsx
Normal file
87
src/components/manga/SourceCard.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
import { langCodeToName } from 'util/language';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
bullet: {
|
||||
display: 'inline-block',
|
||||
margin: '0 2px',
|
||||
transform: 'scale(0.8)',
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
},
|
||||
pos: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
icon: {
|
||||
width: theme.spacing(7),
|
||||
height: theme.spacing(7),
|
||||
flex: '0 0 auto',
|
||||
marginRight: 16,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps {
|
||||
source: ISource
|
||||
}
|
||||
|
||||
export default function SourceCard(props: IProps) {
|
||||
const {
|
||||
source: {
|
||||
id, name, lang, iconUrl, supportsLatest, isConfigurable,
|
||||
},
|
||||
} = props;
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className={classes.root}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
className={classes.icon}
|
||||
alt={name}
|
||||
src={serverAddress + iconUrl}
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h5" component="h2">
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" gutterBottom>
|
||||
{langCodeToName(lang)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{isConfigurable && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/configure/`; }}>Configure</Button>}
|
||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/search/`; }}>Search</Button>
|
||||
{supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/latest/`; }}>Latest</Button>}
|
||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/popular/`; }}>Browse</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
62
src/components/manga/reader/DoublePage.tsx
Normal file
62
src/components/manga/reader/DoublePage.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
|
||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||
image: {
|
||||
display: 'block',
|
||||
marginBottom: 0,
|
||||
width: 'auto',
|
||||
minHeight: '99vh',
|
||||
height: 'auto',
|
||||
maxHeight: '99vh',
|
||||
objectFit: 'contain',
|
||||
},
|
||||
page: {
|
||||
display: 'flex',
|
||||
flexDirection: settings.readerType === 'DoubleLTR' ? 'row' : 'row-reverse',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
overflowX: 'scroll',
|
||||
},
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
index: number
|
||||
image1src: string
|
||||
image2src: string
|
||||
settings: IReaderSettings
|
||||
}
|
||||
|
||||
const DoublePage = React.forwardRef((props: IProps, ref: any) => {
|
||||
const {
|
||||
image1src, image2src, index, settings,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(settings)();
|
||||
|
||||
return (
|
||||
<div ref={ref} className={classes.page}>
|
||||
<img
|
||||
className={classes.image}
|
||||
src={image1src}
|
||||
alt={`Page #${index}`}
|
||||
/>
|
||||
<img
|
||||
className={classes.image}
|
||||
src={image2src}
|
||||
alt={`Page #${index + 1}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default DoublePage;
|
||||
119
src/components/manga/reader/Page.tsx
Normal file
119
src/components/manga/reader/Page.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { CSSProperties } from '@material-ui/core/styles/withStyles';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import SpinnerImage from 'components/SpinnerImage';
|
||||
|
||||
function imageStyle(settings: IReaderSettings): CSSProperties {
|
||||
if (settings.readerType === 'DoubleLTR'
|
||||
|| settings.readerType === 'DoubleRTL'
|
||||
|| settings.readerType === 'ContinuesHorizontalLTR'
|
||||
|| settings.readerType === 'ContinuesHorizontalRTL') {
|
||||
return {
|
||||
display: 'block',
|
||||
marginLeft: '7px',
|
||||
marginRight: '7px',
|
||||
width: 'auto',
|
||||
minHeight: '99vh',
|
||||
height: 'auto',
|
||||
maxHeight: '99vh',
|
||||
objectFit: 'contain',
|
||||
pointerEvents: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
display: 'block',
|
||||
marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0,
|
||||
minWidth: '50vw',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
};
|
||||
}
|
||||
|
||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||
loading: {
|
||||
margin: '100px auto',
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
},
|
||||
loadingImage: {
|
||||
height: '100vh',
|
||||
width: '70vw',
|
||||
padding: '50px calc(50% - 20px)',
|
||||
backgroundColor: '#525252',
|
||||
marginBottom: 10,
|
||||
},
|
||||
image: imageStyle(settings),
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
src: string
|
||||
index: number
|
||||
onImageLoad: () => void
|
||||
setCurPage: React.Dispatch<React.SetStateAction<number>>
|
||||
settings: IReaderSettings
|
||||
}
|
||||
|
||||
const Page = React.forwardRef((props: IProps, ref: any) => {
|
||||
const {
|
||||
src, index, onImageLoad, setCurPage, settings,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(settings)();
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
const handleVerticalScroll = () => {
|
||||
if (imgRef.current) {
|
||||
const rect = imgRef.current.getBoundingClientRect();
|
||||
if (rect.y < 0 && rect.y + rect.height > 0) {
|
||||
setCurPage(index);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleHorizontalScroll = () => {
|
||||
if (imgRef.current) {
|
||||
const rect = imgRef.current.getBoundingClientRect();
|
||||
if (rect.left <= window.innerWidth / 2 && rect.right > window.innerWidth / 2) {
|
||||
setCurPage(index);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
switch (settings.readerType) {
|
||||
case 'Webtoon':
|
||||
case 'ContinuesVertical':
|
||||
window.addEventListener('scroll', handleVerticalScroll);
|
||||
return () => window.removeEventListener('scroll', handleVerticalScroll);
|
||||
case 'ContinuesHorizontalLTR':
|
||||
case 'ContinuesHorizontalRTL':
|
||||
window.addEventListener('scroll', handleHorizontalScroll);
|
||||
return () => window.removeEventListener('scroll', handleHorizontalScroll);
|
||||
default:
|
||||
return () => {};
|
||||
}
|
||||
}, [handleVerticalScroll]);
|
||||
|
||||
return (
|
||||
<div ref={ref} style={{ margin: '0 auto' }}>
|
||||
<SpinnerImage
|
||||
src={src}
|
||||
onImageLoad={onImageLoad}
|
||||
alt={`Page #${index}`}
|
||||
imgRef={imgRef}
|
||||
spinnerClassName={`${classes.image} ${classes.loadingImage}`}
|
||||
imgClassName={classes.image}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Page;
|
||||
39
src/components/manga/reader/PageNumber.tsx
Normal file
39
src/components/manga/reader/PageNumber.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
|
||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||
pageNumber: {
|
||||
display: settings.showPageNumber ? 'block' : 'none',
|
||||
position: 'fixed',
|
||||
bottom: '50px',
|
||||
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
|
||||
width: '50px',
|
||||
textAlign: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
settings: IReaderSettings
|
||||
curPage: number
|
||||
pageCount: number
|
||||
}
|
||||
|
||||
export default function PageNumber(props: IProps) {
|
||||
const { settings, curPage, pageCount } = props;
|
||||
const classes = useStyles(settings)();
|
||||
|
||||
return (
|
||||
<div className={classes.pageNumber}>
|
||||
{`${curPage + 1} / ${pageCount}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
212
src/components/manga/reader/pager/DoublePagedPager.tsx
Normal file
212
src/components/manga/reader/pager/DoublePagedPager.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Page from '../Page';
|
||||
import DoublePage from '../DoublePage';
|
||||
|
||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||
preload: {
|
||||
display: 'none',
|
||||
},
|
||||
reader: {
|
||||
display: 'flex',
|
||||
flexDirection: (settings.readerType === 'DoubleLTR') ? 'row' : 'row-reverse',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
overflowX: 'scroll',
|
||||
},
|
||||
});
|
||||
|
||||
export default function DoublePagedPager(props: IReaderProps) {
|
||||
const {
|
||||
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(settings)();
|
||||
|
||||
const selfRef = useRef<HTMLDivElement>(null);
|
||||
const pagesRef = useRef<HTMLImageElement[]>([]);
|
||||
|
||||
const pagesDisplayed = useRef<number>(0);
|
||||
const pageLoaded = useRef<boolean[]>(Array(pages.length).fill(false));
|
||||
|
||||
function setPagesToDisplay() {
|
||||
pagesDisplayed.current = 0;
|
||||
if (curPage < pages.length && pagesRef.current[curPage]) {
|
||||
if (pageLoaded.current[curPage]) {
|
||||
pagesDisplayed.current = 1;
|
||||
const imgElem = pagesRef.current[curPage];
|
||||
const aspectRatio = imgElem.height / imgElem.width;
|
||||
if (aspectRatio < 1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (curPage + 1 < pages.length && pagesRef.current[curPage + 1]) {
|
||||
if (pageLoaded.current[curPage + 1]) {
|
||||
const imgElem = pagesRef.current[curPage + 1];
|
||||
const aspectRatio = imgElem.height / imgElem.width;
|
||||
if (aspectRatio < 1) {
|
||||
return;
|
||||
}
|
||||
pagesDisplayed.current = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function displayPages() {
|
||||
if (pagesDisplayed.current === 2) {
|
||||
ReactDOM.render(
|
||||
<DoublePage
|
||||
key={curPage}
|
||||
index={curPage}
|
||||
image1src={pages[curPage].src}
|
||||
image2src={pages[curPage + 1].src}
|
||||
settings={settings}
|
||||
/>,
|
||||
document.getElementById('display'),
|
||||
);
|
||||
} else {
|
||||
ReactDOM.render(
|
||||
<Page
|
||||
key={curPage}
|
||||
index={curPage}
|
||||
src={(pagesDisplayed.current === 1) ? pages[curPage].src : ''}
|
||||
onImageLoad={() => {}}
|
||||
setCurPage={setCurPage}
|
||||
settings={settings}
|
||||
/>,
|
||||
document.getElementById('display'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function pagesToGoBack() {
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
if (curPage - i > 0 && pagesRef.current[curPage - i]) {
|
||||
if (pageLoaded.current[curPage - i]) {
|
||||
const imgElem = pagesRef.current[curPage - i];
|
||||
const aspectRatio = imgElem.height / imgElem.width;
|
||||
if (aspectRatio < 1) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (curPage < pages.length - 1) {
|
||||
const nextCurPage = curPage + pagesDisplayed.current;
|
||||
setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage);
|
||||
} else if (settings.loadNextonEnding) {
|
||||
nextChapter();
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (curPage > 0) {
|
||||
const nextCurPage = curPage - pagesToGoBack();
|
||||
setCurPage((nextCurPage < 0) ? 0 : nextCurPage);
|
||||
} else {
|
||||
prevChapter();
|
||||
}
|
||||
}
|
||||
|
||||
function goLeft() {
|
||||
if (settings.readerType === 'DoubleLTR') {
|
||||
prevPage();
|
||||
} else {
|
||||
nextPage();
|
||||
}
|
||||
}
|
||||
|
||||
function goRight() {
|
||||
if (settings.readerType === 'DoubleLTR') {
|
||||
nextPage();
|
||||
} else {
|
||||
prevPage();
|
||||
}
|
||||
}
|
||||
|
||||
function keyboardControl(e:KeyboardEvent) {
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
e.preventDefault();
|
||||
nextPage();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
goRight();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
goLeft();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function clickControl(e:MouseEvent) {
|
||||
if (e.clientX > window.innerWidth / 2) {
|
||||
goRight();
|
||||
} else {
|
||||
goLeft();
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageLoad(index: number) {
|
||||
return () => {
|
||||
pageLoaded.current[index] = true;
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const retryDisplay = setInterval(() => {
|
||||
const isLastPage = (curPage === pages.length - 1);
|
||||
if ((!isLastPage && pageLoaded.current[curPage] && pageLoaded.current[curPage + 1])
|
||||
|| pageLoaded.current[curPage]) {
|
||||
setPagesToDisplay();
|
||||
displayPages();
|
||||
clearInterval(retryDisplay);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
document.addEventListener('keydown', keyboardControl);
|
||||
selfRef.current?.addEventListener('click', clickControl);
|
||||
|
||||
return () => {
|
||||
clearInterval(retryDisplay);
|
||||
document.removeEventListener('keydown', keyboardControl);
|
||||
selfRef.current?.removeEventListener('click', clickControl);
|
||||
};
|
||||
}, [selfRef, curPage, settings.readerType]);
|
||||
|
||||
return (
|
||||
<div ref={selfRef}>
|
||||
<div id="preload" className={classes.preload}>
|
||||
{
|
||||
pages.map((page) => (
|
||||
<img
|
||||
ref={(e:HTMLImageElement) => { pagesRef.current[page.index] = e; }}
|
||||
key={`${page.index}`}
|
||||
src={page.src}
|
||||
onLoad={handleImageLoad(page.index)}
|
||||
alt={`${page.index}`}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div id="display" className={classes.reader} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
147
src/components/manga/reader/pager/HorizontalPager.tsx
Normal file
147
src/components/manga/reader/pager/HorizontalPager.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Page from '../Page';
|
||||
|
||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||
reader: {
|
||||
display: 'flex',
|
||||
flexDirection: (settings.readerType === 'ContinuesHorizontalLTR') ? 'row' : 'row-reverse',
|
||||
justifyContent: (settings.readerType === 'ContinuesHorizontalLTR') ? 'flex-start' : 'flex-end',
|
||||
margin: '0 auto',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
overflowX: 'visible',
|
||||
userSelect: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
export default function HorizontalPager(props: IReaderProps) {
|
||||
const {
|
||||
pages, curPage, settings, setCurPage, prevChapter, nextChapter,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(settings)();
|
||||
|
||||
const selfRef = useRef<HTMLDivElement>(null);
|
||||
const pagesRef = useRef<HTMLDivElement[]>([]);
|
||||
|
||||
function nextPage() {
|
||||
if (curPage < pages.length - 1) {
|
||||
pagesRef.current[curPage + 1]?.scrollIntoView({ inline: 'center' });
|
||||
setCurPage((page) => page + 1);
|
||||
} else if (settings.loadNextonEnding) {
|
||||
nextChapter();
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (curPage > 0) {
|
||||
pagesRef.current[curPage - 1]?.scrollIntoView({ inline: 'center' });
|
||||
setCurPage(curPage - 1);
|
||||
} else if (curPage === 0) {
|
||||
prevChapter();
|
||||
}
|
||||
}
|
||||
|
||||
function goLeft() {
|
||||
if (settings.readerType === 'ContinuesHorizontalLTR') {
|
||||
prevPage();
|
||||
} else {
|
||||
nextPage();
|
||||
}
|
||||
}
|
||||
|
||||
function goRight() {
|
||||
if (settings.readerType === 'ContinuesHorizontalLTR') {
|
||||
nextPage();
|
||||
} else {
|
||||
prevPage();
|
||||
}
|
||||
}
|
||||
|
||||
const mouseXPos = useRef<number>(0);
|
||||
|
||||
function dragScreen(e: MouseEvent) {
|
||||
window.scrollBy(mouseXPos.current - e.pageX, 0);
|
||||
}
|
||||
|
||||
function dragControl(e:MouseEvent) {
|
||||
mouseXPos.current = e.pageX;
|
||||
selfRef.current?.addEventListener('mousemove', dragScreen);
|
||||
}
|
||||
|
||||
function removeDragControl() {
|
||||
selfRef.current?.removeEventListener('mousemove', dragScreen);
|
||||
}
|
||||
|
||||
function clickControl(e:MouseEvent) {
|
||||
if (e.clientX >= window.innerWidth * 0.85) {
|
||||
goRight();
|
||||
} else if (e.clientX <= window.innerWidth * 0.15) {
|
||||
goLeft();
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoadNextonEnding = () => {
|
||||
if (settings.readerType === 'ContinuesHorizontalLTR') {
|
||||
if (window.scrollX + window.innerWidth >= document.body.scrollWidth) {
|
||||
nextChapter();
|
||||
}
|
||||
} else if (settings.readerType === 'ContinuesHorizontalRTL') {
|
||||
if (window.scrollX <= window.innerWidth) {
|
||||
nextChapter();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
pagesRef.current[curPage]?.scrollIntoView({ inline: 'center' });
|
||||
}, [settings.readerType]);
|
||||
|
||||
useEffect(() => {
|
||||
selfRef.current?.addEventListener('mousedown', dragControl);
|
||||
selfRef.current?.addEventListener('mouseup', removeDragControl);
|
||||
|
||||
return () => {
|
||||
selfRef.current?.removeEventListener('mousedown', dragControl);
|
||||
selfRef.current?.removeEventListener('mouseup', removeDragControl);
|
||||
};
|
||||
}, [selfRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings.loadNextonEnding) {
|
||||
document.addEventListener('scroll', handleLoadNextonEnding);
|
||||
}
|
||||
selfRef.current?.addEventListener('mousedown', clickControl);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('scroll', handleLoadNextonEnding);
|
||||
selfRef.current?.removeEventListener('mousedown', clickControl);
|
||||
};
|
||||
}, [selfRef, curPage]);
|
||||
|
||||
return (
|
||||
<div ref={selfRef} className={classes.reader}>
|
||||
{
|
||||
pages.map((page) => (
|
||||
<Page
|
||||
key={page.index}
|
||||
index={page.index}
|
||||
src={page.src}
|
||||
onImageLoad={() => {}}
|
||||
setCurPage={setCurPage}
|
||||
settings={settings}
|
||||
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
111
src/components/manga/reader/pager/PagedPager.tsx
Normal file
111
src/components/manga/reader/pager/PagedPager.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Page from '../Page';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
reader: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto',
|
||||
width: '100%',
|
||||
height: '100vh',
|
||||
},
|
||||
});
|
||||
|
||||
export default function PagedReader(props: IReaderProps) {
|
||||
const {
|
||||
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const selfRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function nextPage() {
|
||||
if (curPage < pages.length - 1) {
|
||||
setCurPage(curPage + 1);
|
||||
} else if (settings.loadNextonEnding) {
|
||||
nextChapter();
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (curPage > 0) {
|
||||
setCurPage(curPage - 1);
|
||||
} else {
|
||||
prevChapter();
|
||||
}
|
||||
}
|
||||
|
||||
function goLeft() {
|
||||
if (settings.readerType === 'SingleLTR') {
|
||||
prevPage();
|
||||
} else if (settings.readerType === 'SingleRTL') {
|
||||
nextPage();
|
||||
}
|
||||
}
|
||||
|
||||
function goRight() {
|
||||
if (settings.readerType === 'SingleLTR') {
|
||||
nextPage();
|
||||
} else if (settings.readerType === 'SingleRTL') {
|
||||
prevPage();
|
||||
}
|
||||
}
|
||||
|
||||
function keyboardControl(e:KeyboardEvent) {
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
e.preventDefault();
|
||||
nextPage();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
goRight();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
goLeft();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function clickControl(e:MouseEvent) {
|
||||
if (e.clientX > window.innerWidth / 2) {
|
||||
goRight();
|
||||
} else {
|
||||
goLeft();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', keyboardControl);
|
||||
selfRef.current?.addEventListener('click', clickControl);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyboardControl);
|
||||
selfRef.current?.removeEventListener('click', clickControl);
|
||||
};
|
||||
}, [selfRef, curPage, settings.readerType]);
|
||||
|
||||
return (
|
||||
<div ref={selfRef} className={classes.reader}>
|
||||
<Page
|
||||
key={curPage}
|
||||
index={curPage}
|
||||
onImageLoad={() => {}}
|
||||
src={pages[curPage].src}
|
||||
setCurPage={setCurPage}
|
||||
settings={settings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
130
src/components/manga/reader/pager/VerticalPager.tsx
Normal file
130
src/components/manga/reader/pager/VerticalPager.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Page from '../Page';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
reader: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto',
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
export default function VerticalReader(props: IReaderProps) {
|
||||
const {
|
||||
pages, settings, setCurPage, curPage, chapter, nextChapter, prevChapter,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const selfRef = useRef<HTMLDivElement>(null);
|
||||
const pagesRef = useRef<HTMLDivElement[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
pagesRef.current = pagesRef.current.slice(0, pages.length);
|
||||
}, [pages.length]);
|
||||
|
||||
function nextPage() {
|
||||
if (curPage < pages.length - 1) {
|
||||
pagesRef.current[curPage + 1]?.scrollIntoView();
|
||||
setCurPage((page) => page + 1);
|
||||
} else if (settings.loadNextonEnding) {
|
||||
nextChapter();
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (curPage > 0) {
|
||||
const rect = pagesRef.current[curPage].getBoundingClientRect();
|
||||
if (rect.y < 0 && rect.y + rect.height > 0) {
|
||||
pagesRef.current[curPage]?.scrollIntoView();
|
||||
} else {
|
||||
pagesRef.current[curPage - 1]?.scrollIntoView();
|
||||
setCurPage(curPage - 1);
|
||||
}
|
||||
} else if (curPage === 0) {
|
||||
prevChapter();
|
||||
}
|
||||
}
|
||||
|
||||
function keyboardControl(e:KeyboardEvent) {
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
e.preventDefault();
|
||||
nextPage();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
nextPage();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
prevPage();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function clickControl(e:MouseEvent) {
|
||||
if (e.clientX > window.innerWidth / 2) {
|
||||
nextPage();
|
||||
} else {
|
||||
prevPage();
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoadNextonEnding = () => {
|
||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||
nextChapter();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (settings.loadNextonEnding) { document.addEventListener('scroll', handleLoadNextonEnding); }
|
||||
document.addEventListener('keydown', keyboardControl, false);
|
||||
selfRef.current?.addEventListener('click', clickControl);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('scroll', handleLoadNextonEnding);
|
||||
document.removeEventListener('keydown', keyboardControl);
|
||||
selfRef.current?.removeEventListener('click', clickControl);
|
||||
};
|
||||
}, [selfRef, curPage]);
|
||||
|
||||
useEffect(() => {
|
||||
// scroll last read page into view
|
||||
let initialPage = (chapter as IChapter).lastPageRead;
|
||||
if (initialPage > pages.length - 1) {
|
||||
initialPage = pages.length - 1;
|
||||
}
|
||||
if (initialPage > -1) {
|
||||
pagesRef.current[initialPage].scrollIntoView();
|
||||
}
|
||||
}, [pagesRef.current.length]);
|
||||
|
||||
return (
|
||||
<div ref={selfRef} className={classes.reader}>
|
||||
{
|
||||
pages.map((page) => (
|
||||
<Page
|
||||
key={page.index}
|
||||
index={page.index}
|
||||
src={page.src}
|
||||
onImageLoad={() => {}}
|
||||
setCurPage={setCurPage}
|
||||
settings={settings}
|
||||
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import React from 'react';
|
||||
|
||||
export default function CheckBoxPreference({ title, summary }: CheckBoxPreferenceProps) {
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={title}
|
||||
secondary={summary}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user