This commit is contained in:
Aria Moradi
2021-10-30 16:10:34 +03:30
parent 92ae5c8cd0
commit 091d97c914
47 changed files with 76 additions and 132 deletions

View File

@@ -1,126 +0,0 @@
/*
* 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 from '@mui/styles/makeStyles';
import createStyles from '@mui/styles/createStyles';
import Button from '@mui/material/Button';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Dialog from '@mui/material/Dialog';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormGroup from '@mui/material/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[]) => {
if (data.length > 0 && data[0].name === 'Default') { data.shift(); }
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}
key={categoryInfo.category.id}
/>
))}
</FormGroup>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel} color="primary">
Cancel
</Button>
<Button onClick={handleOk} color="primary">
Ok
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -1,163 +0,0 @@
/*
* 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 { useTheme } from '@mui/material/styles';
import makeStyles from '@mui/styles/makeStyles';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import IconButton from '@mui/material/IconButton';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import Typography from '@mui/material/Typography';
import { Link } from 'react-router-dom';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import BookmarkIcon from '@mui/icons-material/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,
},
card: {
margin: '10px',
'&:hover': {
backgroundColor: theme.palette.action.hover,
transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
},
'&:active': {
backgroundColor: theme.palette.action.selected,
transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
},
},
}));
interface IProps{
chapter: IChapter
triggerChaptersUpdate: () => void
downloadStatusString: string
}
export default function ChapterCard(props: IProps) {
const classes = useStyles();
const theme = useTheme();
const { chapter, triggerChaptersUpdate, downloadStatusString } = 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 deleteChapter = () => {
client.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`)
.then(() => triggerChaptersUpdate());
handleClose();
};
const readChapterColor = theme.palette.mode === 'dark' ? '#acacac' : '#b0b0b0';
return (
<>
<li>
<Card className={classes.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}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{chapter.scanlator}
{chapter.scanlator && ' '}
{dateStr}
{downloadStatusString}
</Typography>
</div>
</div>
</Link>
<IconButton aria-label="more" onClick={handleClick} size="large">
<MoreVertIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
{downloadStatusString.endsWith('Downloaded')
&& <MenuItem onClick={deleteChapter}>Delete</MenuItem>}
{downloadStatusString.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>
</>
);
}

View File

@@ -1,153 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar';
import Typography from '@mui/material/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,
},
card: {
margin: '10px',
},
}));
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 [useCache] = useLocalStorage<boolean>('useCache', true);
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 className={classes.card}>
<CardContent className={classes.root}>
<div style={{ display: 'flex' }}>
<Avatar
variant="rounded"
className={classes.icon}
alt={name}
src={`${serverAddress}${iconUrl}?useCache=${useCache}`}
/>
<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>
);
}

View File

@@ -1,131 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
import createStyles from '@mui/styles/createStyles';
import Button from '@mui/material/Button';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Dialog from '@mui/material/Dialog';
import Switch from '@mui/material/Switch';
import IconButton from '@mui/material/IconButton';
import FilterListIcon from '@mui/icons-material/FilterList';
import { List, ListItemSecondaryAction, ListItemText } from '@mui/material';
import ListItem from '@mui/material/ListItem';
import { langCodeToName } from 'util/language';
import cloneObject from 'util/cloneObject';
const useStyles = makeStyles(() => createStyles({
paper: {
maxHeight: 435,
width: '80%',
},
}));
function removeAll(firstList: any[], secondList: any[]) {
secondList.forEach((item) => {
const index = firstList.indexOf(item);
if (index !== -1) {
firstList.splice(index, 1);
}
});
return firstList;
}
interface IProps {
shownLangs: string[]
setShownLangs: (arg0: string[]) => void
allLangs: string[]
forcedLangs?: string[]
}
export default function LangSelect(props: IProps) {
const {
shownLangs, setShownLangs, allLangs, forcedLangs,
} = props;
// hold a copy and only sate state on parent when OK pressed, improves performance
const [mShownLangs, setMShownLangs] = useState(
removeAll(cloneObject(shownLangs), forcedLangs!),
);
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"
size="large"
>
<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>
</>
);
}
LangSelect.defaultProps = {
forcedLangs: [] as string[],
};

View File

@@ -1,122 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea';
import Typography from '@mui/material/Typography';
import { Link } from 'react-router-dom';
import { Grid } from '@mui/material';
import useLocalStorage from 'util/useLocalStorage';
import SpinnerImage from 'components/SpinnerImage';
const useStyles = makeStyles((theme) => ({
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',
fontSize: '1.05rem',
textShadow: '0px 0px 3px #000000',
},
badge: {
position: 'absolute',
top: 2,
left: 2,
backgroundColor: theme.palette.primary.dark,
borderRadius: 5,
color: 'white',
padding: '0.1em',
paddingInline: '0.3em',
fontSize: '1.05rem',
},
image: {
height: '100%',
width: '100%',
},
spinner: {
minHeight: '400px',
display: 'grid',
placeItems: 'center',
},
}));
const truncateText = (str: string, maxLength: number) => {
const ending = '...';
// trim the string to the maximum length
const trimmedString = str.substr(0, maxLength - ending.length);
if (trimmedString.length < str.length) {
return trimmedString + ending;
}
return str;
};
interface IProps {
manga: IMangaCard
}
const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
const {
manga: {
id, title, thumbnailUrl, unreadCount: unread,
},
} = props;
const classes = useStyles();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [useCache] = useLocalStorage<boolean>('useCache', true);
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}>
{unread
? (
<Typography className={classes.badge} component="span">
{unread}
</Typography>
)
: null}
<SpinnerImage
alt={title}
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
spinnerClassName={classes.spinner}
imgClassName={classes.image}
/>
<div className={classes.gradient} />
<Typography className={classes.title}>
{truncateText(title, 61)}
</Typography>
</div>
</CardActionArea>
</Card>
</Link>
</Grid>
);
});
export default MangaCard;

View File

@@ -1,251 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
import IconButton from '@mui/material/IconButton';
import { Theme } from '@mui/material/styles';
import FavoriteIcon from '@mui/icons-material/Favorite';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import FilterListIcon from '@mui/icons-material/FilterList';
import PublicIcon from '@mui/icons-material/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.displayName !== null) {
return source.displayName;
}
return source.id;
}
function getValueOrUnknown(val: string) {
return val || 'UNKNOWN';
}
export default function MangaDetails(props: IProps) {
const { setAction } = useContext(NavbarContext);
const { manga } = props;
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"
size="large"
>
<FilterListIcon />
</IconButton>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</>,
);
} else { setAction(<></>); }
}, [inLibrary, categoryDialogOpen]);
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [useCache] = useLocalStorage<boolean>('useCache', true);
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}?useCache=${useCache}`} 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()} size="large">
{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.realUrl} target="_blank">
<IconButton size="large">
<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.map((g) => <h5 key={g}>{g}</h5>)}
</div>
</div>
</div>
);
}

View File

@@ -1,86 +0,0 @@
/*
* 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 '@mui/material/Grid';
import EmptyView from 'components/EmptyView';
import LoadingPlaceholder from 'components/LoadingPlaceholder';
import MangaCard from './MangaCard';
export interface IMangaGridProps{
mangas: IMangaCard[]
isLoading: boolean
message?: string
messageExtra?: JSX.Element
hasNextPage: boolean
lastPageNum: number
setLastPageNum: (lastPageNum: number) => void
}
export default function MangaGrid(props: IMangaGridProps) {
const {
mangas, isLoading, message, messageExtra, hasNextPage, lastPageNum, setLastPageNum,
} = props;
let mapped;
const lastManga = useRef<HTMLDivElement>(null);
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) {
if (isLoading) {
mapped = (
<LoadingPlaceholder />
);
} else {
mapped = (
<EmptyView message={message!} messageExtra={messageExtra} />
);
}
} else {
mapped = mangas.map((it, idx) => {
if (idx === mangas.length - 1) {
return (
<MangaCard
key={it.id}
manga={it}
ref={lastManga}
/>
);
}
return (
<MangaCard
key={it.id}
manga={it}
/>
);
});
}
return (
<Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
{mapped}
</Grid>
);
}
MangaGrid.defaultProps = {
message: '',
messageExtra: undefined,
};

View File

@@ -1,173 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import { useHistory } from 'react-router-dom';
import SearchIcon from '@mui/icons-material/Search';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined';
import Avatar from '@mui/material/Avatar';
import Typography from '@mui/material/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,
},
card: {
margin: '10px',
'&:hover': {
backgroundColor: theme.palette.action.hover,
transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
},
'&:active': {
backgroundColor: theme.palette.action.selected,
transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
},
},
showMobile: {
display: 'flex',
[theme.breakpoints.up('sm')]: {
display: 'none',
},
},
showBigger: {
display: 'flex',
[theme.breakpoints.down('sm')]: {
display: 'none',
},
},
}));
interface IProps {
source: ISource
}
export default function SourceCard(props: IProps) {
const {
source: {
id, name, lang, iconUrl, supportsLatest,
},
} = props;
const history = useHistory();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [useCache] = useLocalStorage<boolean>('useCache', true);
const classes = useStyles();
const redirectTo = (e: any, to: string) => {
history.push(to);
// prevent parent tags from getting the event
e.stopPropagation();
};
return (
<Card
className={classes.card}
onClick={(e) => redirectTo(e, `/sources/${id}/popular/`)}
>
<CardContent className={classes.root}>
<div style={{ display: 'flex' }}>
<Avatar
variant="rounded"
className={classes.icon}
alt={name}
src={`${serverAddress}${iconUrl}?useCache=${useCache}`}
/>
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<Typography variant="h5" component="h2">
{name}
</Typography>
{id !== '0' && (
<Typography variant="caption" display="block" gutterBottom>
{langCodeToName(lang)}
</Typography>
)}
</div>
</div>
<div>
<div className={classes.showMobile}>
<IconButton
style={{ width: 59, height: 59 }}
onClick={(e) => redirectTo(e, `/sources/${id}/search/`)}
size="large"
edge="end"
>
<SearchIcon
fontSize="medium"
/>
</IconButton>
{supportsLatest && (
<IconButton
onClick={(e) => redirectTo(e, `/sources/${id}/latest/`)}
size="large"
>
<FiberNewOutlinedIcon
fontSize="large"
/>
</IconButton>
)}
</div>
<div className={classes.showBigger}>
<Button
variant="outlined"
style={{ marginLeft: 20 }}
onClick={(e) => redirectTo(e, `/sources/${id}/search/`)}
>
Search
</Button>
{supportsLatest && (
<Button
variant="outlined"
style={{ marginLeft: 20 }}
onClick={(e) => redirectTo(e, `/sources/${id}/latest/`)}
>
Latest
</Button>
)}
<Button
variant="outlined"
style={{ marginLeft: 20 }}
onClick={(e: any) => redirectTo(e, `/sources/${id}/popular/`)}
>
Browse
</Button>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,62 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
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;

View File

@@ -1,137 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
import React, { useEffect, useRef } from 'react';
import SpinnerImage from 'components/SpinnerImage';
import useLocalStorage from 'util/useLocalStorage';
function imageStyle(settings: IReaderSettings): any {
const [dimensions, setDimensions] = React.useState({
height: window.innerHeight,
width: window.innerWidth,
});
React.useEffect(() => {
function handleResize() {
setDimensions({
height: window.innerHeight,
width: window.innerWidth,
});
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
if (settings.readerType === 'DoubleLTR'
|| settings.readerType === 'DoubleRTL'
|| settings.readerType === 'ContinuesHorizontalLTR'
|| settings.readerType === 'ContinuesHorizontalRTL') {
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: dimensions.width < dimensions.height ? '100vw' : '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 [useCache] = useLocalStorage<boolean>('useCache', true);
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: 'auto' }}>
<SpinnerImage
src={`${src}?useCache=${useCache}`}
onImageLoad={onImageLoad}
alt={`Page #${index}`}
imgRef={imgRef}
spinnerClassName={`${classes.image} ${classes.loadingImage}`}
imgClassName={classes.image}
/>
</div>
);
});
export default Page;

View File

@@ -1,39 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
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)',
padding: '2px',
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>
);
}

View File

@@ -1,212 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
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>
);
}

View File

@@ -1,147 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
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>
);
}

View File

@@ -1,111 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
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>
);
}

View File

@@ -1,130 +0,0 @@
/*
* 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 '@mui/styles/makeStyles';
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>
);
}

View File

@@ -1,80 +0,0 @@
/*
* 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 ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
export default function EditTextPreference(props: EditTextPreferenceProps) {
const {
title, summary, dialogTitle, dialogMessage, currentValue, updateValue,
} = props;
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const handleDialogCancel = () => {
setDialogOpen(false);
// reset the dialog
setInternalCurrentValue(currentValue);
};
const handleDialogSubmit = () => {
setDialogOpen(false);
updateValue(internalCurrentValue);
};
return (
<>
<ListItem
button
onClick={() => setDialogOpen(true)}
>
<ListItemText
primary={title}
secondary={summary}
/>
</ListItem>
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
<DialogTitle>
{dialogTitle}
</DialogTitle>
<DialogContent>
<DialogContentText>
{dialogMessage}
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="name"
type="text"
fullWidth
value={internalCurrentValue}
onChange={(e) => setInternalCurrentValue(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogCancel} color="primary">
Cancel
</Button>
<Button onClick={handleDialogSubmit} color="primary">
OK
</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,149 +0,0 @@
/*
* 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, useEffect } from 'react';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import RadioGroup from '@mui/material/RadioGroup';
import Radio from '@mui/material/Radio';
import FormControlLabel from '@mui/material/FormControlLabel';
import Button from '@mui/material/Button';
interface IListDialogProps{
value: string
open: boolean
onClose: (arg0: string | null) => void
options: string[]
}
function ListDialog(props: IListDialogProps) {
const {
value: valueProp, open, onClose, options,
} = props;
const [value, setValue] = React.useState(valueProp);
const radioGroupRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!open) {
setValue(valueProp);
}
}, [valueProp, open]);
const handleEntering = () => {
if (radioGroupRef.current != null) {
radioGroupRef?.current.focus();
}
};
const handleCancel = () => {
onClose(null);
};
const handleOk = () => {
onClose(value);
};
const handleChange = (event: any) => {
setValue(event.target.value);
};
return (
<Dialog
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
maxWidth="xs"
TransitionProps={{ onEntering: handleEntering }}
open={open}
>
<DialogTitle>Phone Ringtone</DialogTitle>
<DialogContent dividers>
<RadioGroup
ref={radioGroupRef}
aria-label="ringtone"
name="ringtone"
value={value}
onChange={handleChange}
>
{options.map((option) => (
<FormControlLabel
value={option}
key={option}
control={<Radio />}
label={option}
/>
))}
</RadioGroup>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleOk}>Ok</Button>
</DialogActions>
</Dialog>
);
}
export default function ListPreference(props: ListPreferenceProps) {
const {
title, summary, currentValue, updateValue, entryValues, entries,
} = props;
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
useEffect(() => {
setInternalCurrentValue(currentValue);
}, [currentValue]);
const findEntryOf = (value: string) => {
const idx = entryValues.indexOf(value);
return entries[idx];
};
const findEntryValueOf = (value: string) => {
const idx = entries.indexOf(value);
return entryValues[idx];
};
const getSummary = () => {
if (summary === '%s') {
return findEntryOf(currentValue);
}
return summary;
};
const handleDialogClose = (newValue: string | null) => {
if (newValue !== null) {
updateValue(findEntryValueOf(newValue));
// appear smooth
setInternalCurrentValue(newValue);
}
setDialogOpen(false);
};
return (
<>
<ListItem
button
onClick={() => setDialogOpen(true)}
>
<ListItemText primary={title} secondary={getSummary()} />
</ListItem>
<ListDialog
open={dialogOpen}
onClose={handleDialogClose}
value={findEntryOf(internalCurrentValue)}
options={entries}
/>
</>
);
}

View File

@@ -1,59 +0,0 @@
/*
* 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, useEffect } from 'react';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import Switch from '@mui/material/Switch';
import Checkbox from '@mui/material/Checkbox';
function getTwoStateType(type: 'Checkbox' | 'Switch') {
if (type === 'Switch') { return Switch; }
return Checkbox;
}
function TwoSatePreference(props: TwoStatePreferenceProps) {
const {
title, summary, currentValue, updateValue, type,
} = props;
const [internalCurrentValue, setInternalCurrentValue] = useState<boolean>(currentValue);
useEffect(() => {
setInternalCurrentValue(currentValue);
}, [currentValue]);
return (
<ListItem>
<ListItemText primary={title} secondary={summary} />
<ListItemSecondaryAction>
{React.createElement(getTwoStateType(type),
{
edge: 'end',
checked: internalCurrentValue,
onChange: () => {
updateValue(!currentValue);
// appear smooth
setInternalCurrentValue(!currentValue);
},
})}
</ListItemSecondaryAction>
</ListItem>
);
}
export function CheckBoxPreference(props: CheckBoxPreferenceProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <TwoSatePreference {...props} type="Checkbox" />;
}
export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <TwoSatePreference {...props} type="Switch" />;
}
export default { CheckBoxPreference, SwitchPreferenceCompat };