Remove Anime support
This commit is contained in:
36
src/App.tsx
36
src/App.tsx
@@ -20,25 +20,19 @@ import NavBar from 'components/navbar/NavBar';
|
|||||||
import NavbarContext from 'context/NavbarContext';
|
import NavbarContext from 'context/NavbarContext';
|
||||||
import DarkTheme from 'context/DarkTheme';
|
import DarkTheme from 'context/DarkTheme';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
import MangaSources from 'screens/manga/MangaSources';
|
import MangaSources from 'screens/manga/Sources';
|
||||||
import AnimeSources from 'screens/anime/AnimeSources';
|
|
||||||
import Settings from 'screens/Settings';
|
import Settings from 'screens/Settings';
|
||||||
import About from 'screens/settings/About';
|
import About from 'screens/settings/About';
|
||||||
import Categories from 'screens/settings/Categories';
|
import Categories from 'screens/settings/Categories';
|
||||||
import Backup from 'screens/settings/Backup';
|
import Backup from 'screens/settings/Backup';
|
||||||
import Library from 'screens/manga/Library';
|
import Library from 'screens/manga/Library';
|
||||||
import SearchSingle from 'screens/manga/SearchSingle';
|
import SearchSingle from 'screens/manga/SearchSingle';
|
||||||
import AnimeSearchSingle from 'screens/anime/AnimeSearchSingle';
|
|
||||||
import SourceConfigure from 'screens/manga/SourceConfigure';
|
import SourceConfigure from 'screens/manga/SourceConfigure';
|
||||||
import Manga from 'screens/manga/Manga';
|
import Manga from 'screens/manga/Manga';
|
||||||
import Anime from 'screens/anime/Anime';
|
import MangaExtensions from 'screens/manga/Extensions';
|
||||||
import MangaExtensions from 'screens/manga/MangaExtensions';
|
|
||||||
import SourceMangas from 'screens/manga/SourceMangas';
|
import SourceMangas from 'screens/manga/SourceMangas';
|
||||||
import SourceAnimes from 'screens/anime/SourceAnimes';
|
|
||||||
import Reader from 'screens/manga/Reader';
|
import Reader from 'screens/manga/Reader';
|
||||||
import Player from 'screens/anime/Player';
|
|
||||||
import Updates from 'screens/manga/Updates';
|
import Updates from 'screens/manga/Updates';
|
||||||
import AnimeExtensions from 'screens/anime/AnimeExtensions';
|
|
||||||
import DownloadQueue from 'screens/manga/DownloadQueue';
|
import DownloadQueue from 'screens/manga/DownloadQueue';
|
||||||
|
|
||||||
declare module '@mui/styles/defaultTheme' {
|
declare module '@mui/styles/defaultTheme' {
|
||||||
@@ -97,7 +91,7 @@ export default function App() {
|
|||||||
style={{ paddingTop: '64px' }}
|
style={{ paddingTop: '64px' }}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
{/* general routes */}
|
{/* General Routes */}
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/"
|
path="/"
|
||||||
@@ -156,30 +150,6 @@ export default function App() {
|
|||||||
<Route path="/updates">
|
<Route path="/updates">
|
||||||
<Updates />
|
<Updates />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Anime Routes */}
|
|
||||||
|
|
||||||
<Route path="/anime/sources/:sourceId/search/">
|
|
||||||
<AnimeSearchSingle />
|
|
||||||
</Route>
|
|
||||||
<Route path="/anime/extensions">
|
|
||||||
<AnimeExtensions />
|
|
||||||
</Route>
|
|
||||||
<Route path="/anime/sources/:sourceId/popular/">
|
|
||||||
<SourceAnimes popular />
|
|
||||||
</Route>
|
|
||||||
<Route path="/anime/sources/:sourceId/latest/">
|
|
||||||
<SourceMangas popular={false} />
|
|
||||||
</Route>
|
|
||||||
<Route path="/anime/sources">
|
|
||||||
<AnimeSources />
|
|
||||||
</Route>
|
|
||||||
<Route path="/anime/:animeId/episode/:episodeIndex">
|
|
||||||
<Player />
|
|
||||||
</Route>
|
|
||||||
<Route path="/anime/:id">
|
|
||||||
<Anime />
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
</Switch>
|
||||||
</Container>
|
</Container>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|||||||
@@ -1,83 +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 CardMedia from '@mui/material/CardMedia';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Grid } from '@mui/material';
|
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
|
||||||
|
|
||||||
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%',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
manga: IMangaCard
|
|
||||||
}
|
|
||||||
const AnimeCard = React.forwardRef<HTMLDivElement, IProps>((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={`/anime/${id}/`}>
|
|
||||||
<Card className={classes.root} ref={ref}>
|
|
||||||
<CardActionArea>
|
|
||||||
<div className={classes.wrapper}>
|
|
||||||
<CardMedia
|
|
||||||
className={classes.image}
|
|
||||||
component="img"
|
|
||||||
alt={title}
|
|
||||||
image={serverAddress + thumbnailUrl}
|
|
||||||
title={title}
|
|
||||||
/>
|
|
||||||
<div className={classes.gradient} />
|
|
||||||
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
|
|
||||||
</div>
|
|
||||||
</CardActionArea>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default AnimeCard;
|
|
||||||
@@ -1,258 +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 'components/manga/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: IAnime
|
|
||||||
}
|
|
||||||
|
|
||||||
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 AnimeDetails(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"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<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/anime/anime/${manga.id}/library/`).then(() => {
|
|
||||||
setInLibrary('In Library');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeFromLibrary() {
|
|
||||||
// setInLibrary('removing');
|
|
||||||
client.delete(`/api/v1/anime/anime/${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}?x=${Math.random()}`} 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.url} 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.split(', ').map((g) => <h5 key={g}>{g}</h5>)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 React, { useEffect, useRef } from 'react';
|
|
||||||
import Grid from '@mui/material/Grid';
|
|
||||||
import AnimeCard from './AnimeCard';
|
|
||||||
|
|
||||||
interface IProps{
|
|
||||||
mangas: IMangaCard[]
|
|
||||||
message?: string
|
|
||||||
hasNextPage: boolean
|
|
||||||
lastPageNum: number
|
|
||||||
setLastPageNum: (lastPageNum: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AnimeGrid(props: IProps) {
|
|
||||||
const {
|
|
||||||
mangas, message, hasNextPage, lastPageNum, setLastPageNum,
|
|
||||||
} = props;
|
|
||||||
let mapped;
|
|
||||||
const lastManga = useRef<HTMLInputElement>(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) {
|
|
||||||
mapped = <h3>{message}</h3>;
|
|
||||||
} else {
|
|
||||||
mapped = mangas.map((it, idx) => {
|
|
||||||
if (idx === mangas.length - 1) {
|
|
||||||
return <AnimeCard manga={it} ref={lastManga} />;
|
|
||||||
}
|
|
||||||
return <AnimeCard manga={it} />;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
|
|
||||||
{mapped}
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimeGrid.defaultProps = {
|
|
||||||
message: 'loading...',
|
|
||||||
};
|
|
||||||
@@ -1,150 +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{
|
|
||||||
episode: IEpisode
|
|
||||||
triggerEpisodesUpdate: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EpisodeCard(props: IProps) {
|
|
||||||
const classes = useStyles();
|
|
||||||
const theme = useTheme();
|
|
||||||
const { episode, triggerEpisodesUpdate } = props;
|
|
||||||
|
|
||||||
const dateStr = episode.uploadDate && new Date(episode.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/anime/anime/${episode.animeId}/episode/${episode.index}`, formData)
|
|
||||||
.then(() => triggerEpisodesUpdate());
|
|
||||||
};
|
|
||||||
|
|
||||||
const readChapterColor = theme.palette.mode === 'dark' ? '#acacac' : '#b0b0b0';
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<li>
|
|
||||||
<Card className={classes.card}>
|
|
||||||
<CardContent className={classes.root}>
|
|
||||||
<Link
|
|
||||||
to={`/anime/${episode.animeId}/episode/${episode.index}`}
|
|
||||||
style={{
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: episode.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 }}>
|
|
||||||
{episode.bookmarked && <BookmarkIcon />}
|
|
||||||
</span>
|
|
||||||
{episode.name}
|
|
||||||
{episode.episodeNumber > 0 && ` : ${episode.episodeNumber}`}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" display="block" gutterBottom>
|
|
||||||
{episode.scanlator}
|
|
||||||
{episode.scanlator && ' '}
|
|
||||||
{dateStr}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<IconButton aria-label="more" onClick={handleClick} size="large">
|
|
||||||
<MoreVertIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Menu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
keepMounted
|
|
||||||
open={Boolean(anchorEl)}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
{/* <MenuItem onClick={handleClose}>Download</MenuItem> */}
|
|
||||||
<MenuItem onClick={() => sendChange('bookmarked', !episode.bookmarked)}>
|
|
||||||
{episode.bookmarked && 'Remove bookmark'}
|
|
||||||
{!episode.bookmarked && 'Bookmark'}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => sendChange('read', !episode.read)}>
|
|
||||||
Mark as
|
|
||||||
{' '}
|
|
||||||
{episode.read && 'unread'}
|
|
||||||
{!episode.read && 'read'}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => sendChange('markPrevRead', true)}>
|
|
||||||
Mark previous as Read
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,152 +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 classes = useStyles();
|
|
||||||
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
|
||||||
|
|
||||||
function install() {
|
|
||||||
setInstalledState('installing');
|
|
||||||
client.get(`/api/v1/anime/extension/install/${pkgName}`)
|
|
||||||
.then(() => {
|
|
||||||
setInstalledState('uninstall');
|
|
||||||
notifyInstall();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
setInstalledState('updating');
|
|
||||||
client.get(`/api/v1/anime/extension/update/${pkgName}`)
|
|
||||||
.then(() => {
|
|
||||||
setInstalledState('uninstall');
|
|
||||||
notifyInstall();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function uninstall() {
|
|
||||||
setInstalledState('uninstalling');
|
|
||||||
client.get(`/api/v1/anime/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}
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,97 +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 Button from '@mui/material/Button';
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
source: ISource
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SourceCard(props: IProps) {
|
|
||||||
const {
|
|
||||||
source: {
|
|
||||||
id, name, lang, iconUrl, supportsLatest,
|
|
||||||
},
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
|
||||||
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={classes.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' }}>
|
|
||||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/anime/sources/${id}/search/`; }}>Search</Button>
|
|
||||||
{supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/anime/sources/${id}/latest/`; }}>Latest</Button>}
|
|
||||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/anime/sources/${id}/popular/`; }}>Browse</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,6 @@ const useStyles = makeStyles(() => createStyles({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
function removeAll(firstList: any[], secondList: any[]) {
|
function removeAll(firstList: any[], secondList: any[]) {
|
||||||
console.log(secondList);
|
|
||||||
secondList.forEach((item) => {
|
secondList.forEach((item) => {
|
||||||
const index = firstList.indexOf(item);
|
const index = firstList.indexOf(item);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
|
|||||||
@@ -1,119 +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, useContext } from 'react';
|
|
||||||
import { Theme } from '@mui/material/styles';
|
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
|
||||||
import EpisodeCard from 'components/anime/EpisodeCard';
|
|
||||||
import AnimeDetails from 'components/anime/AnimeDetails';
|
|
||||||
import NavbarContext from 'context/NavbarContext';
|
|
||||||
import client from 'util/client';
|
|
||||||
import LoadingPlaceholder from 'components/LoadingPlaceholder';
|
|
||||||
import makeToast from 'components/Toast';
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
|
||||||
root: {
|
|
||||||
[theme.breakpoints.up('md')]: {
|
|
||||||
display: 'flex',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
chapters: {
|
|
||||||
listStyle: 'none',
|
|
||||||
padding: 0,
|
|
||||||
minHeight: '200px',
|
|
||||||
[theme.breakpoints.up('md')]: {
|
|
||||||
width: '50vw',
|
|
||||||
height: 'calc(100vh - 64px)',
|
|
||||||
margin: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
loading: {
|
|
||||||
margin: '10px 0',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function Anime() {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const { setTitle } = useContext(NavbarContext);
|
|
||||||
useEffect(() => { setTitle('Anime'); }, []); // delegate setting topbar action to MangaDetails
|
|
||||||
|
|
||||||
const { id } = useParams<{ id: string }>();
|
|
||||||
|
|
||||||
const [manga, setManga] = useState<IAnime>();
|
|
||||||
const [episodes, setEpisodes] = useState<IEpisode[]>([]);
|
|
||||||
const [fetchedEpisodes, setFetchedEpisodes] = useState(false);
|
|
||||||
const [noEpisodesFound, setNoEpisodesFound] = useState(false);
|
|
||||||
const [episodeUpdateTriggerer, setEpisodeUpdateTriggerer] = useState(0);
|
|
||||||
|
|
||||||
function triggerEpisodesUpdate() {
|
|
||||||
setEpisodeUpdateTriggerer(episodeUpdateTriggerer + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (manga === undefined || !manga.freshData) {
|
|
||||||
client.get(`/api/v1/anime/anime/${id}/?onlineFetch=${manga !== undefined}`)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data: IAnime) => {
|
|
||||||
setManga(data);
|
|
||||||
setTitle(data.title);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [manga]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const shouldFetchOnline = fetchedEpisodes && episodeUpdateTriggerer === 0;
|
|
||||||
client.get(`/api/v1/anime/anime/${id}/episodes?onlineFetch=${shouldFetchOnline}`)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data) => {
|
|
||||||
if (data.length === 0 && fetchedEpisodes) {
|
|
||||||
makeToast('No episodes found', 'warning');
|
|
||||||
setNoEpisodesFound(true);
|
|
||||||
}
|
|
||||||
setEpisodes(data);
|
|
||||||
})
|
|
||||||
.then(() => setFetchedEpisodes(true));
|
|
||||||
}, [episodes.length, fetchedEpisodes, episodeUpdateTriggerer]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.root}>
|
|
||||||
<LoadingPlaceholder
|
|
||||||
shouldRender={manga !== undefined}
|
|
||||||
component={AnimeDetails}
|
|
||||||
componentProps={{ manga }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LoadingPlaceholder
|
|
||||||
shouldRender={episodes.length > 0 || noEpisodesFound}
|
|
||||||
>
|
|
||||||
<Virtuoso
|
|
||||||
style={{ // override Virtuoso default values and set them with class
|
|
||||||
height: 'undefined',
|
|
||||||
overflowY: window.innerWidth < 960 ? 'visible' : 'auto',
|
|
||||||
}}
|
|
||||||
className={classes.chapters}
|
|
||||||
totalCount={episodes.length}
|
|
||||||
itemContent={(index:number) => (
|
|
||||||
<EpisodeCard
|
|
||||||
episode={episodes[index]}
|
|
||||||
triggerEpisodesUpdate={triggerEpisodesUpdate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
useWindowScroll={window.innerWidth < 960}
|
|
||||||
overscan={window.innerHeight * 0.5}
|
|
||||||
/>
|
|
||||||
</LoadingPlaceholder>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,108 +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, { useContext, useEffect, useState } from 'react';
|
|
||||||
import ExtensionCard from 'components/anime/ExtensionCard';
|
|
||||||
import NavbarContext from 'context/NavbarContext';
|
|
||||||
import client from 'util/client';
|
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
|
||||||
import LangSelect from 'components/manga/LangSelect';
|
|
||||||
import { extensionDefaultLangs, langCodeToName, langSortCmp } from 'util/language';
|
|
||||||
|
|
||||||
const allLangs: string[] = [];
|
|
||||||
|
|
||||||
function groupExtensions(extensions: IExtension[]) {
|
|
||||||
allLangs.length = 0; // empty the array
|
|
||||||
const result = { installed: [], 'updates pending': [] } as any;
|
|
||||||
extensions.sort((a, b) => ((a.apkName > b.apkName) ? 1 : -1));
|
|
||||||
|
|
||||||
extensions.forEach((extension) => {
|
|
||||||
if (result[extension.lang] === undefined) {
|
|
||||||
result[extension.lang] = [];
|
|
||||||
if (extension.lang !== 'all') { allLangs.push(extension.lang); }
|
|
||||||
}
|
|
||||||
if (extension.installed) {
|
|
||||||
if (extension.hasUpdate) {
|
|
||||||
result['updates pending'].push(extension);
|
|
||||||
} else {
|
|
||||||
result.installed.push(extension);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result[extension.lang].push(extension);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// put english first for convience
|
|
||||||
allLangs.sort(langSortCmp);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AnimeExtensions() {
|
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTitle('Extensions');
|
|
||||||
setAction(
|
|
||||||
<LangSelect
|
|
||||||
shownLangs={shownLangs}
|
|
||||||
setShownLangs={setShownLangs}
|
|
||||||
allLangs={allLangs}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}, [shownLangs]);
|
|
||||||
|
|
||||||
const [extensionsRaw, setExtensionsRaw] = useState<IExtension[]>([]);
|
|
||||||
const [extensions, setExtensions] = useState<any>({});
|
|
||||||
|
|
||||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
|
||||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
client.get('/api/v1/anime/extension/list')
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data) => setExtensionsRaw(data));
|
|
||||||
}, [updateTriggerHolder]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (extensionsRaw.length > 0) {
|
|
||||||
const groupedExtension = groupExtensions(extensionsRaw);
|
|
||||||
setExtensions(groupedExtension);
|
|
||||||
}
|
|
||||||
}, [extensionsRaw]);
|
|
||||||
|
|
||||||
if (Object.entries(extensions).length === 0) {
|
|
||||||
return <h3>loading...</h3>;
|
|
||||||
}
|
|
||||||
const groupsToShow = ['updates pending', 'installed', ...shownLangs];
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
Object.entries(extensions).map(([lang, list]) => (
|
|
||||||
((groupsToShow.indexOf(lang) !== -1 && (list as []).length > 0)
|
|
||||||
&& (
|
|
||||||
<React.Fragment key={lang}>
|
|
||||||
<h1 key={lang} style={{ marginLeft: 25 }}>
|
|
||||||
{langCodeToName(lang)}
|
|
||||||
</h1>
|
|
||||||
{(list as IExtension[]).map((it) => (
|
|
||||||
<ExtensionCard
|
|
||||||
key={it.apkName}
|
|
||||||
extension={it}
|
|
||||||
notifyInstall={() => {
|
|
||||||
triggerUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</React.Fragment>
|
|
||||||
))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,115 +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, { useContext, useEffect, useState } from 'react';
|
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import AnimeGrid from 'components/anime/AnimeGrid';
|
|
||||||
import NavbarContext from 'context/NavbarContext';
|
|
||||||
import client from 'util/client';
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
root: {
|
|
||||||
margin: '20px 10px',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-around',
|
|
||||||
width: '300px',
|
|
||||||
TextField: {
|
|
||||||
margin: theme.spacing(1),
|
|
||||||
width: '25ch',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function AnimeSearchSingle() {
|
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
|
||||||
useEffect(() => { setTitle('Search'); setAction(<></>); }, []);
|
|
||||||
|
|
||||||
const { sourceId } = useParams<{ sourceId: string }>();
|
|
||||||
const classes = useStyles();
|
|
||||||
const [error, setError] = useState<boolean>(false);
|
|
||||||
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
|
||||||
const [message, setMessage] = useState<string>('');
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
|
||||||
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
|
||||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
|
||||||
|
|
||||||
const textInput = React.createRef<HTMLInputElement>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
client.get(`/api/v1/anime/source/${sourceId}`)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data: { name: string }) => setTitle(`Search: ${data.name}`));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function processInput() {
|
|
||||||
if (textInput.current) {
|
|
||||||
const { value } = textInput.current;
|
|
||||||
if (value === '') {
|
|
||||||
setError(true);
|
|
||||||
setMessage('Type something to search');
|
|
||||||
} else {
|
|
||||||
setError(false);
|
|
||||||
setSearchTerm(value);
|
|
||||||
setMangas([]);
|
|
||||||
setMessage('loading...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchTerm.length > 0) {
|
|
||||||
client.get(`/api/v1/anime/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data: { mangaList: IAnime[], hasNextPage: boolean }) => {
|
|
||||||
setMessage('');
|
|
||||||
if (data.mangaList.length > 0) {
|
|
||||||
setMangas([
|
|
||||||
...mangas,
|
|
||||||
...data.mangaList.map((it) => ({
|
|
||||||
title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
|
|
||||||
}))]);
|
|
||||||
setHasNextPage(data.hasNextPage);
|
|
||||||
} else {
|
|
||||||
setMessage('search query returned nothing.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [searchTerm]);
|
|
||||||
|
|
||||||
const mangaGrid = (
|
|
||||||
<AnimeGrid
|
|
||||||
mangas={mangas}
|
|
||||||
message={message}
|
|
||||||
hasNextPage={hasNextPage}
|
|
||||||
lastPageNum={lastPageNum}
|
|
||||||
setLastPageNum={setLastPageNum}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={classes.root}>
|
|
||||||
<TextField
|
|
||||||
inputRef={textInput}
|
|
||||||
error={error}
|
|
||||||
id="filled-basic"
|
|
||||||
variant="filled"
|
|
||||||
size="small"
|
|
||||||
label="Search text.."
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && processInput()}
|
|
||||||
/>
|
|
||||||
<Button variant="contained" color="primary" onClick={() => processInput()}>
|
|
||||||
Search
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{mangaGrid}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,84 +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, { useContext, useEffect, useState } from 'react';
|
|
||||||
import LangSelect from 'components/manga/LangSelect';
|
|
||||||
import SourceCard from 'components/anime/SourceCard';
|
|
||||||
import NavbarContext from 'context/NavbarContext';
|
|
||||||
import client from 'util/client';
|
|
||||||
import { sourceDefualtLangs, langCodeToName, langSortCmp } from 'util/language';
|
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
|
||||||
|
|
||||||
function sourceToLangList(sources: ISource[]) {
|
|
||||||
const result: string[] = [];
|
|
||||||
|
|
||||||
sources.forEach((source) => {
|
|
||||||
if (result.indexOf(source.lang) === -1) { result.push(source.lang); }
|
|
||||||
});
|
|
||||||
|
|
||||||
result.sort(langSortCmp);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupByLang(sources: ISource[]) {
|
|
||||||
const result = {} as any;
|
|
||||||
sources.forEach((source) => {
|
|
||||||
if (result[source.lang] === undefined) { result[source.lang] = [] as ISource[]; }
|
|
||||||
result[source.lang].push(source);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AnimeSources() {
|
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
|
||||||
|
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
|
||||||
|
|
||||||
const [sources, setSources] = useState<ISource[]>([]);
|
|
||||||
const [fetched, setFetched] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTitle('Sources');
|
|
||||||
setAction(
|
|
||||||
<LangSelect
|
|
||||||
shownLangs={shownLangs}
|
|
||||||
setShownLangs={setShownLangs}
|
|
||||||
allLangs={sourceToLangList(sources)}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}, [shownLangs, sources]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
client.get('/api/v1/anime/source/list')
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data) => { setSources(data); setFetched(true); });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (sources.length === 0) {
|
|
||||||
if (fetched) return (<h3>No sources found. Install Some Extensions first.</h3>);
|
|
||||||
return (<h3>loading...</h3>);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* eslint-disable-next-line max-len */}
|
|
||||||
{Object.entries(groupByLang(sources)).sort((a, b) => langSortCmp(a[0], b[0])).map(([lang, list]) => (
|
|
||||||
shownLangs.indexOf(lang) !== -1 && (
|
|
||||||
<React.Fragment key={lang}>
|
|
||||||
<h1 key={lang} style={{ marginLeft: 25 }}>{langCodeToName(lang)}</h1>
|
|
||||||
{(list as ISource[]).map((source) => (
|
|
||||||
<SourceCard
|
|
||||||
key={source.id}
|
|
||||||
source={source}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,83 +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 CircularProgress from '@mui/material/CircularProgress';
|
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import NavbarContext from 'context/NavbarContext';
|
|
||||||
import client from 'util/client';
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
root: {
|
|
||||||
width: 'calc(100vw - 10px)',
|
|
||||||
height: 'calc(100vh - 64px)',
|
|
||||||
},
|
|
||||||
|
|
||||||
loading: {
|
|
||||||
margin: '50px auto',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
|
|
||||||
video: {
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '100%',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialEpisode = () => ({
|
|
||||||
index: -1, episodeCount: 0, videos: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function Player() {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const { episodeIndex, animeId } = useParams<{ episodeIndex: string, animeId: string }>();
|
|
||||||
const [episode, setEpisode] = useState<IEpisode | IPartialEpisode>(initialEpisode());
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [episodeLink, setEpisodeLink] = useState<string>('');
|
|
||||||
const { setTitle } = useContext(NavbarContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTitle('Reader');
|
|
||||||
client.get(`/api/v1/anime/anime/${animeId}/`)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data: IAnime) => {
|
|
||||||
setTitle(data.title);
|
|
||||||
});
|
|
||||||
}, [episodeIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setEpisode(initialEpisode);
|
|
||||||
client.get(`/api/v1/anime/anime/${animeId}/episode/${episodeIndex}`)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data:IEpisode) => {
|
|
||||||
setEpisode(data);
|
|
||||||
});
|
|
||||||
}, [episodeIndex]);
|
|
||||||
|
|
||||||
// return spinner while chpater data is loading
|
|
||||||
if (episode.videos.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className={classes.loading}>
|
|
||||||
<CircularProgress thickness={5} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.root}>
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
|
||||||
<video className={classes.video} controls>
|
|
||||||
{episode.videos.map((it) => (
|
|
||||||
<source src={it.videoUrl} />
|
|
||||||
))}
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +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, { useContext, useEffect, useState } from 'react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import AnimeGrid from 'components/anime/AnimeGrid';
|
|
||||||
import NavbarContext from 'context/NavbarContext';
|
|
||||||
import client from 'util/client';
|
|
||||||
|
|
||||||
export default function SourceAnimes(props: { popular: boolean }) {
|
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
|
||||||
useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
|
|
||||||
|
|
||||||
const { sourceId } = useParams<{ sourceId: string }>();
|
|
||||||
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
|
||||||
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
|
||||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
client.get(`/api/v1/anime/source/${sourceId}`)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data: { name: string }) => setTitle(data.name));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const sourceType = props.popular ? 'popular' : 'latest';
|
|
||||||
client.get(`/api/v1/anime/source/${sourceId}/${sourceType}/${lastPageNum}`)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data: { mangaList: IAnime[], hasNextPage: boolean }) => {
|
|
||||||
setMangas([
|
|
||||||
...mangas,
|
|
||||||
...data.mangaList.map((it) => ({
|
|
||||||
title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
|
|
||||||
}))]);
|
|
||||||
setHasNextPage(data.hasNextPage);
|
|
||||||
});
|
|
||||||
}, [lastPageNum]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimeGrid
|
|
||||||
mangas={mangas}
|
|
||||||
hasNextPage={hasNextPage}
|
|
||||||
lastPageNum={lastPageNum}
|
|
||||||
setLastPageNum={setLastPageNum}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
49
src/typings.d.ts
vendored
49
src/typings.d.ts
vendored
@@ -58,28 +58,6 @@ interface IManga {
|
|||||||
freshData: boolean
|
freshData: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAnime {
|
|
||||||
id: number
|
|
||||||
sourceId: string
|
|
||||||
|
|
||||||
url: string
|
|
||||||
title: string
|
|
||||||
thumbnailUrl: string
|
|
||||||
|
|
||||||
artist: string
|
|
||||||
author: string
|
|
||||||
description: string
|
|
||||||
genre: string | null
|
|
||||||
status: string
|
|
||||||
|
|
||||||
inLibrary: boolean
|
|
||||||
source: ISource
|
|
||||||
|
|
||||||
realUrl: string
|
|
||||||
|
|
||||||
freshData: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IChapter {
|
interface IChapter {
|
||||||
url: string
|
url: string
|
||||||
name: string
|
name: string
|
||||||
@@ -102,39 +80,12 @@ interface IMangaChapter {
|
|||||||
chapter: IChapter
|
chapter: IChapter
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Video{
|
|
||||||
url: string
|
|
||||||
quality: string
|
|
||||||
videoUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IEpisode {
|
|
||||||
url: string
|
|
||||||
name: string
|
|
||||||
uploadDate: number
|
|
||||||
episodeNumber: number
|
|
||||||
scanlator: string
|
|
||||||
animeId: number
|
|
||||||
read: boolean
|
|
||||||
bookmarked: boolean
|
|
||||||
lastPageRead: number
|
|
||||||
index: number
|
|
||||||
episodeCount: number
|
|
||||||
videos: Video[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPartialChpter {
|
interface IPartialChpter {
|
||||||
pageCount: number
|
pageCount: number
|
||||||
index: number
|
index: number
|
||||||
chapterCount: number
|
chapterCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPartialEpisode {
|
|
||||||
index: number
|
|
||||||
episodeCount: number
|
|
||||||
videos: Video[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ICategory {
|
interface ICategory {
|
||||||
id: number
|
id: number
|
||||||
order: number
|
order: number
|
||||||
|
|||||||
Reference in New Issue
Block a user