kick webUI out of Tachidesk

This commit is contained in:
Aria Moradi
2021-08-06 04:55:03 +04:30
parent 3af7de3460
commit bdd5caae1a
73 changed files with 0 additions and 18346 deletions

View File

@@ -2,7 +2,5 @@ rootProject.name = System.getenv("ProductName") ?: "Tachidesk"
include("server")
include("webUI")
include("AndroidCompat")
include("AndroidCompat:Config")

View File

@@ -1,21 +0,0 @@
plugins {
id("com.github.node-gradle.node") version "3.0.1"
}
val nodeRoot = "${project.projectDir}/src"
node {
nodeProjectDir.set(file(nodeRoot))
}
tasks {
register<Copy>("copyBuild") {
from(file("$nodeRoot/build"))
into(file("$rootDir/server/src/main/resources/webUI"))
dependsOn("yarn_build")
}
named("yarn_build") {
dependsOn("yarn") // install node_modules
}
}

View File

@@ -1 +0,0 @@
.eslintrc.js

View File

@@ -1,19 +0,0 @@
module.exports = {
extends: ['airbnb-typescript'],
plugins: ['@typescript-eslint'],
parserOptions: {
project: './tsconfig.json',
},
rules: {
// Indent with 4 spaces
'@typescript-eslint/indent': ['error', 4],
// Indent JSX with 4 spaces
'react/jsx-indent': ['error', 4],
// Indent props with 4 spaces
'react/jsx-indent-props': ['error', 4],
'no-plusplus': ['error', { 'allowForLoopAfterthoughts': true }]
},
};

View File

@@ -1,4 +0,0 @@
node_modules/
.eslintcache
.vscode
.env

View File

@@ -1,70 +0,0 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `yarn build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

View File

@@ -1,55 +0,0 @@
{
"name": "project",
"version": "0.1.0",
"private": true,
"dependencies": {
"@fontsource/roboto": "^4.3.0",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.58",
"axios": "^0.21.1",
"file-selector": "^0.2.4",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^17.0.2",
"react-lazyload": "^3.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-virtuoso": "^1.8.6",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react": "^17.0.2",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "^17.0.2",
"@types/react-lazyload": "^3.1.0",
"@types/react-router-dom": "^5.1.7",
"@typescript-eslint/eslint-plugin": "4.23.0",
"@typescript-eslint/parser": "4.23.0",
"eslint": "^7.26.0",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"typescript": "^4.2.4"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 579 KiB

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/>
<meta name="theme-color" content="#000000"/>
<meta
name="description"
content="A manga reader that runs tachiyomi's extensions"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png"/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Tachidesk</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@@ -1,31 +0,0 @@
{
"short_name": "Tachidesk",
"name": "Tachidesk",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#ff2323",
"background_color": "#ff2323"
}

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,178 +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 {
BrowserRouter as Router, Switch,
Route,
Redirect,
} from 'react-router-dom';
import { Container } from '@material-ui/core';
import CssBaseline from '@material-ui/core/CssBaseline';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import NavBar from 'components/navbar/NavBar';
import NavbarContext from 'context/NavbarContext';
import DarkTheme from 'context/DarkTheme';
import useLocalStorage from 'util/useLocalStorage';
import MangaSources from 'screens/manga/MangaSources';
import AnimeSources from 'screens/anime/AnimeSources';
import Settings from 'screens/Settings';
import About from 'screens/settings/About';
import Categories from 'screens/settings/Categories';
import Backup from 'screens/settings/Backup';
import Library from 'screens/manga/Library';
import SearchSingle from 'screens/manga/SearchSingle';
import SourceConfigure from 'screens/manga/SourceConfigure';
import Manga from 'screens/manga/Manga';
import Anime from 'screens/anime/Anime';
import MangaExtensions from 'screens/manga/MangaExtensions';
import SourceMangas from 'screens/manga/SourceMangas';
import SourceAnimes from 'screens/anime/SourceAnimes';
import Reader from 'screens/manga/Reader';
import Player from 'screens/anime/Player';
import AnimeExtensions from 'screens/anime/AnimeExtensions';
import DownloadQueue from 'screens/manga/DownloadQueue';
export default function App() {
const [title, setTitle] = useState<string>('Tachidesk');
const [action, setAction] = useState<any>(<div />);
const [override, setOverride] = useState<INavbarOverride>({ status: false, value: <div /> });
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
const navBarContext = {
title, setTitle, action, setAction, override, setOverride,
};
const darkThemeContext = { darkTheme, setDarkTheme };
const theme = React.useMemo(
() => createMuiTheme({
palette: {
type: darkTheme ? 'dark' : 'light',
},
overrides: {
MuiCssBaseline: {
'@global': {
'*::-webkit-scrollbar': {
width: '10px',
background: darkTheme ? '#222' : '#e1e1e1',
},
'*::-webkit-scrollbar-thumb': {
background: darkTheme ? '#111' : '#aaa',
borderRadius: '5px',
},
},
},
},
}),
[darkTheme],
);
return (
<Router>
<ThemeProvider theme={theme}>
<NavbarContext.Provider value={navBarContext}>
<CssBaseline />
<NavBar />
<Container
id="appMainContainer"
maxWidth={false}
disableGutters
style={{ paddingTop: '64px' }}
>
<Switch>
{/* general routes */}
<Route
exact
path="/"
render={() => (
<Redirect to="/library" />
)}
/>
<Route path="/settings/about">
<About />
</Route>
<Route path="/settings/categories">
<Categories />
</Route>
<Route path="/settings/backup">
<Backup />
</Route>
<Route path="/settings">
<DarkTheme.Provider value={darkThemeContext}>
<Settings />
</DarkTheme.Provider>
</Route>
{/* Manga Routes */}
<Route path="/sources/:sourceId/search/">
<SearchSingle />
</Route>
<Route path="/manga/extensions">
<MangaExtensions />
</Route>
<Route path="/sources/:sourceId/popular/">
<SourceMangas popular />
</Route>
<Route path="/sources/:sourceId/latest/">
<SourceMangas popular={false} />
</Route>
<Route path="/sources/:sourceId/configure/">
<SourceConfigure />
</Route>
<Route path="/manga/sources">
<MangaSources />
</Route>
<Route path="/manga/downloads">
<DownloadQueue />
</Route>
<Route path="/manga/:mangaId/chapter/:chapterNum">
<></>
</Route>
<Route path="/manga/:id">
<Manga />
</Route>
<Route path="/library">
<Library />
</Route>
{/* Anime Routes */}
<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>
</Container>
<Switch>
<Route
path="/manga/:mangaId/chapter/:chapterIndex"
// passing a key re-mounts the reader when changing chapters
render={(props:any) => <Reader key={props.match.params.chapterIndex} />}
/>
</Switch>
</NavbarContext.Provider>
</ThemeProvider>
</Router>
);
}

View File

@@ -1,56 +0,0 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable react/require-default-props */
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import CircularProgress from '@material-ui/core/CircularProgress';
const useStyles = makeStyles({
loading: {
margin: '10px auto',
display: 'flex',
justifyContent: 'center',
},
});
interface IProps {
shouldRender: boolean | (() => boolean)
children?: React.ReactNode
component?: string | React.FunctionComponent<any> | React.ComponentClass<any, any>
componentProps?: any
}
export default function LoadingPlaceholder(props: IProps) {
const {
children, shouldRender, component, componentProps,
} = props;
const classes = useStyles();
const condition = shouldRender instanceof Function ? shouldRender() : shouldRender;
if (condition) {
if (component) {
return React.createElement(component, componentProps);
}
if (children) {
return (
<>
{children}
</>
);
}
}
return (
<div className={classes.loading}>
<CircularProgress thickness={5} />
</div>
);
}

View File

@@ -1,67 +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 CircularProgress from '@material-ui/core/CircularProgress';
interface IProps {
src: string
alt: string
imgRef?: React.RefObject<HTMLImageElement>
spinnerClassName?: string
imgClassName?: string
onImageLoad?: () => void
}
export default function SpinnerImage(props: IProps) {
const {
src, alt, onImageLoad, imgRef, spinnerClassName, imgClassName,
} = props;
const [imageSrc, setImagsrc] = useState<string>('');
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => {
setImagsrc(src);
onImageLoad?.();
};
return () => {
img.onload = null;
};
}, [src]);
if (imageSrc.length === 0) {
return (
// <div className={`${classes.image} ${classes.loadingImage}`}>
<div className={spinnerClassName}>
<CircularProgress thickness={5} />
</div>
);
}
return (
<img
className={imgClassName}
ref={imgRef}
src={imageSrc}
alt={alt}
/>
);
}
SpinnerImage.defaultProps = {
spinnerClassName: '',
imgClassName: '',
onImageLoad: () => {},
imgRef: undefined,
};

View File

@@ -1,114 +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 '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import CollectionsBookmarkIcon from '@material-ui/icons/CollectionsBookmark';
import ExploreIcon from '@material-ui/icons/Explore';
import ExtensionIcon from '@material-ui/icons/Extension';
import GetAppIcon from '@material-ui/icons/GetApp';
import ListItemText from '@material-ui/core/ListItemText';
import SettingsIcon from '@material-ui/icons/Settings';
import { Link } from 'react-router-dom';
const useStyles = makeStyles({
list: {
width: 250,
},
});
interface IProps {
drawerOpen: boolean
setDrawerOpen: React.Dispatch<React.SetStateAction<boolean>>
}
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
const classes = useStyles();
return (
<div>
<Drawer
open={drawerOpen}
anchor="left"
onClose={() => setDrawerOpen(false)}
>
<div
className={classes.list}
role="presentation"
onClick={() => setDrawerOpen(false)}
onKeyDown={() => setDrawerOpen(false)}
>
<List>
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Library">
<ListItemIcon>
<CollectionsBookmarkIcon />
</ListItemIcon>
<ListItemText primary="Library" />
</ListItem>
</Link>
<Link to="/manga/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions">
<ListItemIcon>
<ExtensionIcon />
</ListItemIcon>
<ListItemText primary="Extensions" />
</ListItem>
</Link>
{/* <Link to="/anime/extensions"
style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions">
<ListItemIcon>
<ExtensionIcon />
</ListItemIcon>
<ListItemText primary="Anime Extensions" />
</ListItem>
</Link> */}
<Link to="/manga/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Sources">
<ListItemIcon>
<ExploreIcon />
</ListItemIcon>
<ListItemText primary="Sources" />
</ListItem>
</Link>
{/* <Link to="/anime/sources"
style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Sources">
<ListItemIcon>
<ExploreIcon />
</ListItemIcon>
<ListItemText primary="Anime Sources" />
</ListItem>
</Link> */}
<Link to="/manga/downloads" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Manga Download Queue">
<ListItemIcon>
<GetAppIcon />
</ListItemIcon>
<ListItemText primary="Downloads" />
</ListItem>
</Link>
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="settings">
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItem>
</Link>
</List>
</div>
</Drawer>
</div>
);
}

View File

@@ -1,63 +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 ReactDOM from 'react-dom';
import React from 'react';
import Slide, { SlideProps } from '@material-ui/core/Slide';
import Snackbar from '@material-ui/core/Snackbar';
import MuiAlert, { Color as Severity } from '@material-ui/lab/Alert';
function removeToast(id: string) {
const container = document.querySelector(`#${id}`)!!;
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
}
function Transition(props: SlideProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <Slide {...props} direction="up" />;
}
interface IToastProps{
message: string
severity: Severity
}
function Toast(props: IToastProps) {
const { message, severity } = props;
const [open, setOpen] = React.useState(true);
const handleClose = () => {
setOpen(false);
};
return (
<Snackbar
open={open}
onClose={handleClose}
autoHideDuration={3000}
TransitionComponent={Transition}
message="I love snacks"
>
<MuiAlert elevation={6} variant="filled" onClose={handleClose} severity={severity}>
{message}
</MuiAlert>
</Snackbar>
);
}
export default function makeToast(message: string, severity: Severity) {
const id = Math.floor(Math.random() * 1000);
const container = document.createElement('div');
container.id = `alert-${id}`;
document.body.appendChild(container);
ReactDOM.render(<Toast message={message} severity={severity} />, container);
setTimeout(() => removeToast(container.id), 3500);
}

View File

@@ -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 '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea';
import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom';
import { Grid } from '@material-ui/core';
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((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;

View File

@@ -1,257 +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 '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';
import { Theme } from '@material-ui/core/styles';
import FavoriteIcon from '@material-ui/icons/Favorite';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import FilterListIcon from '@material-ui/icons/FilterList';
import PublicIcon from '@material-ui/icons/Public';
import React, { useContext, useEffect, useState } from 'react';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
import CategorySelect from '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: IManga
}
function getSourceName(source: ISource) {
if (source.name !== null) {
return `${source.name} (${source.lang.toLocaleUpperCase()})`;
}
return source.id;
}
function getValueOrUnknown(val: string) {
return val || 'UNKNOWN';
}
export default function 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"
>
<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()}>
{inLibrary === 'In Library' && <FavoriteIcon />}
{inLibrary !== 'In Library' && <FavoriteBorderIcon />}
<span>{inLibrary}</span>
</IconButton>
</div>
{ /* eslint-disable-next-line react/jsx-no-target-blank */ }
<a href={manga.url} target="_blank">
<IconButton>
<PublicIcon />
<span>Open Site</span>
</IconButton>
</a>
</div>
</div>
<div className={classes.bottom}>
<div className={classes.description}>
<h4>About</h4>
<p>{manga.description}</p>
</div>
<div className={classes.genre}>
{manga.genre.split(', ').map((g) => <h5 key={g}>{g}</h5>)}
</div>
</div>
</div>
);
}

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 React, { useEffect, useRef } from 'react';
import Grid from '@material-ui/core/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>();
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...',
};

View File

@@ -1,138 +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, useTheme } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import IconButton from '@material-ui/core/IconButton';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import BookmarkIcon from '@material-ui/icons/Bookmark';
import client from 'util/client';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
},
bullet: {
display: 'inline-block',
margin: '0 2px',
transform: 'scale(0.8)',
},
title: {
fontSize: 14,
},
pos: {
marginBottom: 12,
},
icon: {
width: theme.spacing(7),
height: theme.spacing(7),
flex: '0 0 auto',
marginRight: 16,
},
}));
interface IProps{
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.type === 'dark' ? '#acacac' : '#b0b0b0';
return (
<>
<li>
<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}>
<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>
</>
);
}

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 } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
},
bullet: {
display: 'inline-block',
margin: '0 2px',
transform: 'scale(0.8)',
},
title: {
fontSize: 14,
},
pos: {
marginBottom: 12,
},
icon: {
width: theme.spacing(7),
height: theme.spacing(7),
flex: '0 0 auto',
marginRight: 16,
},
}));
interface IProps {
extension: IExtension
notifyInstall: () => void
}
export default function ExtensionCard(props: IProps) {
const {
extension: {
name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl,
},
notifyInstall,
} = props;
const [installedState, setInstalledState] = useState<string>(
() => {
if (obsolete) { return 'obsolete'; }
if (hasUpdate) { return 'update'; }
return (installed ? 'uninstall' : 'install');
},
);
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles();
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
function install() {
setInstalledState('installing');
client.get(`/api/v1/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>
<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>
);
}

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 from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import useLocalStorage from 'util/useLocalStorage';
import { langCodeToName } from 'util/language';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
},
bullet: {
display: 'inline-block',
margin: '0 2px',
transform: 'scale(0.8)',
},
title: {
fontSize: 14,
},
pos: {
marginBottom: 12,
},
icon: {
width: theme.spacing(7),
height: theme.spacing(7),
flex: '0 0 auto',
marginRight: 16,
},
}));
interface IProps {
source: ISource
}
export default function SourceCard(props: IProps) {
const {
source: {
id, name, lang, iconUrl, supportsLatest,
},
} = props;
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles();
return (
<Card>
<CardContent className={classes.root}>
<div style={{ display: 'flex' }}>
<Avatar
variant="rounded"
className={classes.icon}
alt={name}
src={serverAddress + iconUrl}
/>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2">
{name}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{langCodeToName(lang)}
</Typography>
</div>
</div>
<div style={{ display: 'flex' }}>
<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>
);
}

View File

@@ -1,123 +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, createStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormGroup from '@material-ui/core/FormGroup';
import client from 'util/client';
const useStyles = makeStyles(() => createStyles({
paper: {
maxHeight: 435,
width: '80%',
},
}));
interface IProps {
open: boolean
setOpen: (value: boolean) => void
mangaId: number
}
interface ICategoryInfo {
category: ICategory
selected: boolean
}
export default function CategorySelect(props: IProps) {
const classes = useStyles();
const { open, setOpen, mangaId } = props;
const [categoryInfos, setCategoryInfos] = useState<ICategoryInfo[]>([]);
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
useEffect(() => {
let tmpCategoryInfos: ICategoryInfo[] = [];
client.get('/api/v1/category/')
.then((response) => response.data)
.then((data: ICategory[]) => {
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
})
.then(() => {
client.get(`/api/v1/manga/${mangaId}/category/`)
.then((response) => response.data)
.then((data: ICategory[]) => {
data.forEach((category) => {
tmpCategoryInfos[category.order - 1].selected = true;
});
setCategoryInfos(tmpCategoryInfos);
});
});
}, [updateTriggerHolder, open]);
const handleCancel = () => {
setOpen(false);
};
const handleOk = () => {
setOpen(false);
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, categoryId: number) => {
const { checked } = event.target as HTMLInputElement;
const method = checked ? client.get : client.delete;
method(`/api/v1/manga/${mangaId}/category/${categoryId}`)
.then(() => triggerUpdate());
};
return (
<Dialog
classes={classes}
maxWidth="xs"
open={open}
>
<DialogTitle>Set categories</DialogTitle>
<DialogContent dividers>
<FormGroup>
{categoryInfos.length === 0
&& (
<span>
No categories found!
<br />
You should make some from settings.
</span>
)}
{categoryInfos.map((categoryInfo) => (
<FormControlLabel
control={(
<Checkbox
checked={categoryInfo.selected}
onChange={(e) => handleChange(e, categoryInfo.category.id)}
color="default"
/>
)}
label={categoryInfo.category.name}
/>
))}
</FormGroup>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel} color="primary">
Cancel
</Button>
<Button onClick={handleOk} color="primary">
Ok
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -1,146 +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, useTheme } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import IconButton from '@material-ui/core/IconButton';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import BookmarkIcon from '@material-ui/icons/Bookmark';
import client from 'util/client';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
},
bullet: {
display: 'inline-block',
margin: '0 2px',
transform: 'scale(0.8)',
},
title: {
fontSize: 14,
},
pos: {
marginBottom: 12,
},
icon: {
width: theme.spacing(7),
height: theme.spacing(7),
flex: '0 0 auto',
marginRight: 16,
},
}));
interface IProps{
chapter: IChapter
triggerChaptersUpdate: () => void
downloadingString: string
}
export default function ChapterCard(props: IProps) {
const classes = useStyles();
const theme = useTheme();
const { chapter, triggerChaptersUpdate, downloadingString } = props;
const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const sendChange = (key: string, value: any) => {
handleClose();
const formData = new FormData();
formData.append(key, value);
client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData)
.then(() => triggerChaptersUpdate());
};
const downloadChapter = () => {
client.get(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`);
handleClose();
};
const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0';
return (
<>
<li>
<Card>
<CardContent className={classes.root}>
<Link
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
style={{
textDecoration: 'none',
color: chapter.read ? readChapterColor : theme.palette.text.primary,
}}
>
<div style={{ display: 'flex' }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2">
<span style={{ color: theme.palette.primary.dark }}>
{chapter.bookmarked && <BookmarkIcon />}
</span>
{chapter.name}
{chapter.chapterNumber > 0 && ` : ${chapter.chapterNumber}`}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{chapter.scanlator}
{chapter.scanlator && ' '}
{dateStr}
{downloadingString}
</Typography>
</div>
</div>
</Link>
<IconButton aria-label="more" onClick={handleClick}>
<MoreVertIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
{downloadingString.length === 0
&& <MenuItem onClick={downloadChapter}>Download</MenuItem> }
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
{chapter.bookmarked && 'Remove bookmark'}
{!chapter.bookmarked && 'Bookmark'}
</MenuItem>
<MenuItem onClick={() => sendChange('read', !chapter.read)}>
Mark as
{' '}
{chapter.read && 'unread'}
{!chapter.read && 'read'}
</MenuItem>
<MenuItem onClick={() => sendChange('markPrevRead', true)}>
Mark previous as Read
</MenuItem>
</Menu>
</CardContent>
</Card>
</li>
</>
);
}

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 } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
},
bullet: {
display: 'inline-block',
margin: '0 2px',
transform: 'scale(0.8)',
},
title: {
fontSize: 14,
},
pos: {
marginBottom: 12,
},
icon: {
width: theme.spacing(7),
height: theme.spacing(7),
flex: '0 0 auto',
marginRight: 16,
},
}));
interface IProps {
extension: IExtension
notifyInstall: () => void
}
export default function ExtensionCard(props: IProps) {
const {
extension: {
name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl,
},
notifyInstall,
} = props;
const [installedState, setInstalledState] = useState<string>(
() => {
if (obsolete) { return 'obsolete'; }
if (hasUpdate) { return 'update'; }
return (installed ? 'uninstall' : 'install');
},
);
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles();
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
function install() {
setInstalledState('installing');
client.get(`/api/v1/extension/install/${pkgName}`)
.then(() => {
setInstalledState('uninstall');
notifyInstall();
});
}
function update() {
setInstalledState('updating');
client.get(`/api/v1/extension/update/${pkgName}`)
.then(() => {
setInstalledState('uninstall');
notifyInstall();
});
}
function uninstall() {
setInstalledState('uninstalling');
client.get(`/api/v1/extension/uninstall/${pkgName}`)
.then(() => {
// setInstalledState('install');
notifyInstall();
});
}
function handleButtonClick() {
switch (installedState) {
case 'install':
install();
break;
case 'update':
update();
break;
case 'obsolete':
uninstall();
setTimeout(() => window.location.reload(), 3000);
break;
case 'uninstall':
uninstall();
break;
default:
break;
}
}
return (
<Card>
<CardContent className={classes.root}>
<div style={{ display: 'flex' }}>
<Avatar
variant="rounded"
className={classes.icon}
alt={name}
src={serverAddress + iconUrl}
/>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2">
{name}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{langPress}
{' '}
{versionName}
</Typography>
</div>
</div>
<Button
variant="outlined"
style={{ color: installedState === 'obsolete' ? 'red' : 'inherit' }}
onClick={() => handleButtonClick()}
>
{installedState}
</Button>
</CardContent>
</Card>
);
}

View File

@@ -1,109 +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, createStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
import Switch from '@material-ui/core/Switch';
import IconButton from '@material-ui/core/IconButton';
import FilterListIcon from '@material-ui/icons/FilterList';
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
import ListItem from '@material-ui/core/ListItem';
import { langCodeToName } from 'util/language';
import cloneObject from 'util/cloneObject';
const useStyles = makeStyles(() => createStyles({
paper: {
maxHeight: 435,
width: '80%',
},
}));
interface IProps {
shownLangs: string[]
setShownLangs: (arg0: string[]) => void
allLangs: string[]
}
export default function ExtensionLangSelect(props: IProps) {
const { shownLangs, setShownLangs, allLangs } = props;
// hold a copy and only sate state on parent when OK pressed, improves performance
const [mShownLangs, setMShownLangs] = useState(shownLangs);
const classes = useStyles();
const [open, setOpen] = useState<boolean>(false);
const handleCancel = () => {
setOpen(false);
};
const handleOk = () => {
setOpen(false);
setShownLangs(mShownLangs);
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, lang: string) => {
const { checked } = event.target as HTMLInputElement;
if (checked) {
setMShownLangs([...mShownLangs, lang]);
} else {
const clone = cloneObject(mShownLangs);
clone.splice(clone.indexOf(lang), 1);
setMShownLangs(clone);
}
};
return (
<>
<IconButton
onClick={() => setOpen(true)}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton>
<Dialog
classes={classes}
maxWidth="xs"
open={open}
>
<DialogTitle>Enabled Languages</DialogTitle>
<DialogContent dividers style={{ padding: 0 }}>
<List>
{allLangs.map((lang) => (
<ListItem key={lang}>
<ListItemText primary={langCodeToName(lang)} />
<ListItemSecondaryAction>
<Switch
checked={mShownLangs.indexOf(lang) !== -1}
onChange={(e) => handleChange(e, lang)}
/>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel} color="primary">
Cancel
</Button>
<Button onClick={handleOk} color="primary">
Ok
</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,87 +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 '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea';
import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom';
import { Grid } from '@material-ui/core';
import useLocalStorage from 'util/useLocalStorage';
import SpinnerImage from 'components/SpinnerImage';
const useStyles = makeStyles({
root: {
height: '100%',
width: '100%',
display: 'flex',
},
wrapper: {
position: 'relative',
height: '100%',
},
gradient: {
position: 'absolute',
top: 0,
width: '100%',
height: '100%',
background: 'linear-gradient(to bottom, transparent, #000000)',
opacity: 0.5,
},
title: {
position: 'absolute',
bottom: 0,
padding: '0.5em',
color: 'white',
},
image: {
height: '100%',
width: '100%',
},
spinner: {
minHeight: '400px',
padding: '180px calc(50% - 20px)',
},
});
interface IProps {
manga: IMangaCard
}
const MangaCard = React.forwardRef((props: IProps, ref) => {
const {
manga: {
id, title, thumbnailUrl,
},
} = props;
const classes = useStyles();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
return (
<Grid item xs={6} sm={4} md={3} lg={2}>
<Link to={`/manga/${id}/`}>
<Card className={classes.root} ref={ref}>
<CardActionArea>
<div className={classes.wrapper}>
<SpinnerImage
alt={title}
src={serverAddress + thumbnailUrl}
spinnerClassName={classes.spinner}
imgClassName={classes.image}
/>
<div className={classes.gradient} />
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
</div>
</CardActionArea>
</Card>
</Link>
</Grid>
);
});
export default MangaCard;

View File

@@ -1,257 +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 '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';
import { Theme } from '@material-ui/core/styles';
import FavoriteIcon from '@material-ui/icons/Favorite';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import FilterListIcon from '@material-ui/icons/FilterList';
import PublicIcon from '@material-ui/icons/Public';
import React, { useContext, useEffect, useState } from 'react';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
import CategorySelect from './CategorySelect';
const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
root: {
width: '100%',
[theme.breakpoints.up('md')]: {
position: 'sticky',
top: '64px',
left: '0px',
width: '50vw',
height: 'calc(100vh - 64px)',
alignSelf: 'flex-start',
overflowY: 'auto',
},
},
top: {
padding: '10px',
// [theme.breakpoints.up('md')]: {
// minWidth: '50%',
// },
},
leftRight: {
display: 'flex',
},
leftSide: {
'& img': {
borderRadius: 4,
maxWidth: '100%',
minWidth: '100%',
height: 'auto',
},
maxWidth: '50%',
// [theme.breakpoints.up('md')]: {
// minWidth: '100px',
// },
},
rightSide: {
marginLeft: 15,
maxWidth: '100%',
'& span': {
fontWeight: '400',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
buttons: {
display: 'flex',
justifyContent: 'space-around',
'& button': {
color: inLibrary === 'In Library' ? '#2196f3' : 'inherit',
},
'& span': {
display: 'block',
fontSize: '0.85em',
},
'& a': {
textDecoration: 'none',
color: '#858585',
'& button': {
color: 'inherit',
},
},
},
bottom: {
paddingLeft: '10px',
paddingRight: '10px',
[theme.breakpoints.up('md')]: {
fontSize: '1.2em',
// maxWidth: '50%',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
description: {
'& h4': {
marginTop: '1em',
marginBottom: 0,
},
'& p': {
textAlign: 'justify',
textJustify: 'inter-word',
},
},
genre: {
display: 'flex',
flexWrap: 'wrap',
'& h5': {
border: '2px solid #2196f3',
borderRadius: '1.13em',
marginRight: '1em',
marginTop: 0,
marginBottom: '10px',
padding: '0.3em',
color: '#2196f3',
},
},
}));
interface IProps{
manga: IManga
}
function getSourceName(source: ISource) {
if (source.name !== null) {
return `${source.name} (${source.lang.toLocaleUpperCase()})`;
}
return source.id;
}
function getValueOrUnknown(val: string) {
return val || 'UNKNOWN';
}
export default function MangaDetails(props: IProps) {
const { setAction } = useContext(NavbarContext);
const { manga } = props;
if (manga.genre == null) {
manga.genre = '';
}
const [inLibrary, setInLibrary] = useState<string>(
manga.inLibrary ? 'In Library' : 'Add To Library',
);
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
useEffect(() => {
if (inLibrary === 'In Library') {
setAction(
<>
<IconButton
onClick={() => setCategoryDialogOpen(true)}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</>,
);
} else { setAction(<></>); }
}, [inLibrary, categoryDialogOpen]);
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles(inLibrary)();
function addToLibrary() {
// setInLibrary('adding');
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('In Library');
});
}
function removeFromLibrary() {
// setInLibrary('removing');
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('Add To Library');
});
}
function handleButtonClick() {
if (inLibrary === 'Add To Library') {
addToLibrary();
} else {
removeFromLibrary();
}
}
return (
<div className={classes.root}>
<div className={classes.top}>
<div className={classes.leftRight}>
<div className={classes.leftSide}>
<img src={`${serverAddress}${manga.thumbnailUrl}`} alt="Manga Thumbnail" />
</div>
<div className={classes.rightSide}>
<h1>
{manga.title}
</h1>
<h3>
Author:
{' '}
<span>{getValueOrUnknown(manga.author)}</span>
</h3>
<h3>
Artist:
{' '}
<span>{getValueOrUnknown(manga.artist)}</span>
</h3>
<h3>
Status:
{' '}
{manga.status}
</h3>
<h3>
Source:
{' '}
{getSourceName(manga.source)}
</h3>
</div>
</div>
<div className={classes.buttons}>
<div>
<IconButton onClick={() => handleButtonClick()}>
{inLibrary === 'In Library' && <FavoriteIcon />}
{inLibrary !== 'In Library' && <FavoriteBorderIcon />}
<span>{inLibrary}</span>
</IconButton>
</div>
{ /* eslint-disable-next-line react/jsx-no-target-blank */ }
<a href={manga.url} target="_blank">
<IconButton>
<PublicIcon />
<span>Open Site</span>
</IconButton>
</a>
</div>
</div>
<div className={classes.bottom}>
<div className={classes.description}>
<h4>About</h4>
<p>{manga.description}</p>
</div>
<div className={classes.genre}>
{manga.genre.split(', ').map((g) => <h5 key={g}>{g}</h5>)}
</div>
</div>
</div>
);
}

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 React, { useEffect, useRef } from 'react';
import Grid from '@material-ui/core/Grid';
import MangaCard from './MangaCard';
interface IProps{
mangas: IMangaCard[]
message?: string
hasNextPage: boolean
lastPageNum: number
setLastPageNum: (lastPageNum: number) => void
}
export default function MangaGrid(props: IProps) {
const {
mangas, message, hasNextPage, lastPageNum, setLastPageNum,
} = props;
let mapped;
const lastManga = useRef<HTMLInputElement>();
const scrollHandler = () => {
if (lastManga.current) {
const rect = lastManga.current.getBoundingClientRect();
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) {
setLastPageNum(lastPageNum + 1);
}
}
};
useEffect(() => {
window.addEventListener('scroll', scrollHandler, true);
return () => {
window.removeEventListener('scroll', scrollHandler, true);
};
}, [hasNextPage, mangas]);
if (mangas.length === 0) {
mapped = <h3>{message}</h3>;
} else {
mapped = mangas.map((it, idx) => {
if (idx === mangas.length - 1) {
return <MangaCard manga={it} ref={lastManga} />;
}
return <MangaCard manga={it} />;
});
}
return (
<Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
{mapped}
</Grid>
);
}
MangaGrid.defaultProps = {
message: 'loading...',
};

View File

@@ -1,87 +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 '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import useLocalStorage from 'util/useLocalStorage';
import { langCodeToName } from 'util/language';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
},
bullet: {
display: 'inline-block',
margin: '0 2px',
transform: 'scale(0.8)',
},
title: {
fontSize: 14,
},
pos: {
marginBottom: 12,
},
icon: {
width: theme.spacing(7),
height: theme.spacing(7),
flex: '0 0 auto',
marginRight: 16,
},
}));
interface IProps {
source: ISource
}
export default function SourceCard(props: IProps) {
const {
source: {
id, name, lang, iconUrl, supportsLatest, isConfigurable,
},
} = props;
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles();
return (
<Card>
<CardContent className={classes.root}>
<div style={{ display: 'flex' }}>
<Avatar
variant="rounded"
className={classes.icon}
alt={name}
src={serverAddress + iconUrl}
/>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2">
{name}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{langCodeToName(lang)}
</Typography>
</div>
</div>
<div style={{ display: 'flex' }}>
{isConfigurable && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/configure/`; }}>Configure</Button>}
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/search/`; }}>Search</Button>
{supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/latest/`; }}>Latest</Button>}
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/popular/`; }}>Browse</Button>
</div>
</CardContent>
</Card>
);
}

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 '@material-ui/core/styles';
import React from 'react';
const useStyles = (settings: IReaderSettings) => makeStyles({
image: {
display: 'block',
marginBottom: 0,
width: 'auto',
minHeight: '99vh',
height: 'auto',
maxHeight: '99vh',
objectFit: 'contain',
},
page: {
display: 'flex',
flexDirection: settings.readerType === 'DoubleLTR' ? 'row' : 'row-reverse',
justifyContent: 'center',
margin: '0 auto',
width: 'auto',
height: 'auto',
overflowX: 'scroll',
},
});
interface IProps {
index: number
image1src: string
image2src: string
settings: IReaderSettings
}
const DoublePage = React.forwardRef((props: IProps, ref: any) => {
const {
image1src, image2src, index, settings,
} = props;
const classes = useStyles(settings)();
return (
<div ref={ref} className={classes.page}>
<img
className={classes.image}
src={image1src}
alt={`Page #${index}`}
/>
<img
className={classes.image}
src={image2src}
alt={`Page #${index + 1}`}
/>
</div>
);
});
export default DoublePage;

View File

@@ -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 { makeStyles } from '@material-ui/core/styles';
import { CSSProperties } from '@material-ui/core/styles/withStyles';
import React, { useEffect, useRef } from 'react';
import SpinnerImage from 'components/SpinnerImage';
function imageStyle(settings: IReaderSettings): CSSProperties {
if (settings.readerType === 'DoubleLTR'
|| settings.readerType === 'DoubleRTL'
|| settings.readerType === 'ContinuesHorizontalLTR'
|| settings.readerType === 'ContinuesHorizontalRTL') {
return {
display: 'block',
marginLeft: '7px',
marginRight: '7px',
width: 'auto',
minHeight: '99vh',
height: 'auto',
maxHeight: '99vh',
objectFit: 'contain',
pointerEvents: 'none',
};
}
return {
display: 'block',
marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0,
minWidth: '50vw',
width: '100%',
maxWidth: '100%',
};
}
const useStyles = (settings: IReaderSettings) => makeStyles({
loading: {
margin: '100px auto',
height: '100vh',
width: '100vw',
},
loadingImage: {
height: '100vh',
width: '70vw',
padding: '50px calc(50% - 20px)',
backgroundColor: '#525252',
marginBottom: 10,
},
image: imageStyle(settings),
});
interface IProps {
src: string
index: number
onImageLoad: () => void
setCurPage: React.Dispatch<React.SetStateAction<number>>
settings: IReaderSettings
}
const Page = React.forwardRef((props: IProps, ref: any) => {
const {
src, index, onImageLoad, setCurPage, settings,
} = props;
const classes = useStyles(settings)();
const imgRef = useRef<HTMLImageElement>(null);
const handleVerticalScroll = () => {
if (imgRef.current) {
const rect = imgRef.current.getBoundingClientRect();
if (rect.y < 0 && rect.y + rect.height > 0) {
setCurPage(index);
}
}
};
const handleHorizontalScroll = () => {
if (imgRef.current) {
const rect = imgRef.current.getBoundingClientRect();
if (rect.left <= window.innerWidth / 2 && rect.right > window.innerWidth / 2) {
setCurPage(index);
}
}
};
useEffect(() => {
switch (settings.readerType) {
case 'Webtoon':
case 'ContinuesVertical':
window.addEventListener('scroll', handleVerticalScroll);
return () => window.removeEventListener('scroll', handleVerticalScroll);
case 'ContinuesHorizontalLTR':
case 'ContinuesHorizontalRTL':
window.addEventListener('scroll', handleHorizontalScroll);
return () => window.removeEventListener('scroll', handleHorizontalScroll);
default:
return () => {};
}
}, [handleVerticalScroll]);
return (
<div ref={ref} style={{ margin: '0 auto' }}>
<SpinnerImage
src={src}
onImageLoad={onImageLoad}
alt={`Page #${index}`}
imgRef={imgRef}
spinnerClassName={`${classes.image} ${classes.loadingImage}`}
imgClassName={classes.image}
/>
</div>
);
});
export default Page;

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 '@material-ui/core/styles';
import React from 'react';
const useStyles = (settings: IReaderSettings) => makeStyles({
pageNumber: {
display: settings.showPageNumber ? 'block' : 'none',
position: 'fixed',
bottom: '50px',
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
width: '50px',
textAlign: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
borderRadius: '10px',
},
});
interface IProps {
settings: IReaderSettings
curPage: number
pageCount: number
}
export default function PageNumber(props: IProps) {
const { settings, curPage, pageCount } = props;
const classes = useStyles(settings)();
return (
<div className={classes.pageNumber}>
{`${curPage + 1} / ${pageCount}`}
</div>
);
}

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 '@material-ui/core/styles';
import React, { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import Page from '../Page';
import DoublePage from '../DoublePage';
const useStyles = (settings: IReaderSettings) => makeStyles({
preload: {
display: 'none',
},
reader: {
display: 'flex',
flexDirection: (settings.readerType === 'DoubleLTR') ? 'row' : 'row-reverse',
justifyContent: 'center',
margin: '0 auto',
width: 'auto',
height: 'auto',
overflowX: 'scroll',
},
});
export default function DoublePagedPager(props: IReaderProps) {
const {
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
} = props;
const classes = useStyles(settings)();
const selfRef = useRef<HTMLDivElement>(null);
const pagesRef = useRef<HTMLImageElement[]>([]);
const pagesDisplayed = useRef<number>(0);
const pageLoaded = useRef<boolean[]>(Array(pages.length).fill(false));
function setPagesToDisplay() {
pagesDisplayed.current = 0;
if (curPage < pages.length && pagesRef.current[curPage]) {
if (pageLoaded.current[curPage]) {
pagesDisplayed.current = 1;
const imgElem = pagesRef.current[curPage];
const aspectRatio = imgElem.height / imgElem.width;
if (aspectRatio < 1) {
return;
}
}
}
if (curPage + 1 < pages.length && pagesRef.current[curPage + 1]) {
if (pageLoaded.current[curPage + 1]) {
const imgElem = pagesRef.current[curPage + 1];
const aspectRatio = imgElem.height / imgElem.width;
if (aspectRatio < 1) {
return;
}
pagesDisplayed.current = 2;
}
}
}
function displayPages() {
if (pagesDisplayed.current === 2) {
ReactDOM.render(
<DoublePage
key={curPage}
index={curPage}
image1src={pages[curPage].src}
image2src={pages[curPage + 1].src}
settings={settings}
/>,
document.getElementById('display'),
);
} else {
ReactDOM.render(
<Page
key={curPage}
index={curPage}
src={(pagesDisplayed.current === 1) ? pages[curPage].src : ''}
onImageLoad={() => {}}
setCurPage={setCurPage}
settings={settings}
/>,
document.getElementById('display'),
);
}
}
function pagesToGoBack() {
for (let i = 1; i <= 2; i++) {
if (curPage - i > 0 && pagesRef.current[curPage - i]) {
if (pageLoaded.current[curPage - i]) {
const imgElem = pagesRef.current[curPage - i];
const aspectRatio = imgElem.height / imgElem.width;
if (aspectRatio < 1) {
return 1;
}
}
}
}
return 2;
}
function nextPage() {
if (curPage < pages.length - 1) {
const nextCurPage = curPage + pagesDisplayed.current;
setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage);
} else if (settings.loadNextonEnding) {
nextChapter();
}
}
function prevPage() {
if (curPage > 0) {
const nextCurPage = curPage - pagesToGoBack();
setCurPage((nextCurPage < 0) ? 0 : nextCurPage);
} else {
prevChapter();
}
}
function goLeft() {
if (settings.readerType === 'DoubleLTR') {
prevPage();
} else {
nextPage();
}
}
function goRight() {
if (settings.readerType === 'DoubleLTR') {
nextPage();
} else {
prevPage();
}
}
function keyboardControl(e:KeyboardEvent) {
switch (e.code) {
case 'Space':
e.preventDefault();
nextPage();
break;
case 'ArrowRight':
goRight();
break;
case 'ArrowLeft':
goLeft();
break;
default:
break;
}
}
function clickControl(e:MouseEvent) {
if (e.clientX > window.innerWidth / 2) {
goRight();
} else {
goLeft();
}
}
function handleImageLoad(index: number) {
return () => {
pageLoaded.current[index] = true;
};
}
useEffect(() => {
const retryDisplay = setInterval(() => {
const isLastPage = (curPage === pages.length - 1);
if ((!isLastPage && pageLoaded.current[curPage] && pageLoaded.current[curPage + 1])
|| pageLoaded.current[curPage]) {
setPagesToDisplay();
displayPages();
clearInterval(retryDisplay);
}
}, 50);
document.addEventListener('keydown', keyboardControl);
selfRef.current?.addEventListener('click', clickControl);
return () => {
clearInterval(retryDisplay);
document.removeEventListener('keydown', keyboardControl);
selfRef.current?.removeEventListener('click', clickControl);
};
}, [selfRef, curPage, settings.readerType]);
return (
<div ref={selfRef}>
<div id="preload" className={classes.preload}>
{
pages.map((page) => (
<img
ref={(e:HTMLImageElement) => { pagesRef.current[page.index] = e; }}
key={`${page.index}`}
src={page.src}
onLoad={handleImageLoad(page.index)}
alt={`${page.index}`}
/>
))
}
</div>
<div id="display" className={classes.reader} />
</div>
);
}

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 '@material-ui/core/styles';
import React, { useEffect, useRef } from 'react';
import Page from '../Page';
const useStyles = (settings: IReaderSettings) => makeStyles({
reader: {
display: 'flex',
flexDirection: (settings.readerType === 'ContinuesHorizontalLTR') ? 'row' : 'row-reverse',
justifyContent: (settings.readerType === 'ContinuesHorizontalLTR') ? 'flex-start' : 'flex-end',
margin: '0 auto',
width: 'auto',
height: 'auto',
overflowX: 'visible',
userSelect: 'none',
},
});
export default function HorizontalPager(props: IReaderProps) {
const {
pages, curPage, settings, setCurPage, prevChapter, nextChapter,
} = props;
const classes = useStyles(settings)();
const selfRef = useRef<HTMLDivElement>(null);
const pagesRef = useRef<HTMLDivElement[]>([]);
function nextPage() {
if (curPage < pages.length - 1) {
pagesRef.current[curPage + 1]?.scrollIntoView({ inline: 'center' });
setCurPage((page) => page + 1);
} else if (settings.loadNextonEnding) {
nextChapter();
}
}
function prevPage() {
if (curPage > 0) {
pagesRef.current[curPage - 1]?.scrollIntoView({ inline: 'center' });
setCurPage(curPage - 1);
} else if (curPage === 0) {
prevChapter();
}
}
function goLeft() {
if (settings.readerType === 'ContinuesHorizontalLTR') {
prevPage();
} else {
nextPage();
}
}
function goRight() {
if (settings.readerType === 'ContinuesHorizontalLTR') {
nextPage();
} else {
prevPage();
}
}
const mouseXPos = useRef<number>(0);
function dragScreen(e: MouseEvent) {
window.scrollBy(mouseXPos.current - e.pageX, 0);
}
function dragControl(e:MouseEvent) {
mouseXPos.current = e.pageX;
selfRef.current?.addEventListener('mousemove', dragScreen);
}
function removeDragControl() {
selfRef.current?.removeEventListener('mousemove', dragScreen);
}
function clickControl(e:MouseEvent) {
if (e.clientX >= window.innerWidth * 0.85) {
goRight();
} else if (e.clientX <= window.innerWidth * 0.15) {
goLeft();
}
}
const handleLoadNextonEnding = () => {
if (settings.readerType === 'ContinuesHorizontalLTR') {
if (window.scrollX + window.innerWidth >= document.body.scrollWidth) {
nextChapter();
}
} else if (settings.readerType === 'ContinuesHorizontalRTL') {
if (window.scrollX <= window.innerWidth) {
nextChapter();
}
}
};
useEffect(() => {
pagesRef.current[curPage]?.scrollIntoView({ inline: 'center' });
}, [settings.readerType]);
useEffect(() => {
selfRef.current?.addEventListener('mousedown', dragControl);
selfRef.current?.addEventListener('mouseup', removeDragControl);
return () => {
selfRef.current?.removeEventListener('mousedown', dragControl);
selfRef.current?.removeEventListener('mouseup', removeDragControl);
};
}, [selfRef]);
useEffect(() => {
if (settings.loadNextonEnding) {
document.addEventListener('scroll', handleLoadNextonEnding);
}
selfRef.current?.addEventListener('mousedown', clickControl);
return () => {
document.removeEventListener('scroll', handleLoadNextonEnding);
selfRef.current?.removeEventListener('mousedown', clickControl);
};
}, [selfRef, curPage]);
return (
<div ref={selfRef} className={classes.reader}>
{
pages.map((page) => (
<Page
key={page.index}
index={page.index}
src={page.src}
onImageLoad={() => {}}
setCurPage={setCurPage}
settings={settings}
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
/>
))
}
</div>
);
}

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 '@material-ui/core/styles';
import React, { useEffect, useRef } from 'react';
import Page from '../Page';
const useStyles = makeStyles({
reader: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '0 auto',
width: '100%',
height: '100vh',
},
});
export default function PagedReader(props: IReaderProps) {
const {
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
} = props;
const classes = useStyles();
const selfRef = useRef<HTMLDivElement>(null);
function nextPage() {
if (curPage < pages.length - 1) {
setCurPage(curPage + 1);
} else if (settings.loadNextonEnding) {
nextChapter();
}
}
function prevPage() {
if (curPage > 0) {
setCurPage(curPage - 1);
} else {
prevChapter();
}
}
function goLeft() {
if (settings.readerType === 'SingleLTR') {
prevPage();
} else if (settings.readerType === 'SingleRTL') {
nextPage();
}
}
function goRight() {
if (settings.readerType === 'SingleLTR') {
nextPage();
} else if (settings.readerType === 'SingleRTL') {
prevPage();
}
}
function keyboardControl(e:KeyboardEvent) {
switch (e.code) {
case 'Space':
e.preventDefault();
nextPage();
break;
case 'ArrowRight':
goRight();
break;
case 'ArrowLeft':
goLeft();
break;
default:
break;
}
}
function clickControl(e:MouseEvent) {
if (e.clientX > window.innerWidth / 2) {
goRight();
} else {
goLeft();
}
}
useEffect(() => {
document.addEventListener('keydown', keyboardControl);
selfRef.current?.addEventListener('click', clickControl);
return () => {
document.removeEventListener('keydown', keyboardControl);
selfRef.current?.removeEventListener('click', clickControl);
};
}, [selfRef, curPage, settings.readerType]);
return (
<div ref={selfRef} className={classes.reader}>
<Page
key={curPage}
index={curPage}
onImageLoad={() => {}}
src={pages[curPage].src}
setCurPage={setCurPage}
settings={settings}
/>
</div>
);
}

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 '@material-ui/core/styles';
import React, { useEffect, useRef } from 'react';
import Page from '../Page';
const useStyles = makeStyles({
reader: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
margin: '0 auto',
width: '100%',
},
});
export default function VerticalReader(props: IReaderProps) {
const {
pages, settings, setCurPage, curPage, chapter, nextChapter, prevChapter,
} = props;
const classes = useStyles();
const selfRef = useRef<HTMLDivElement>(null);
const pagesRef = useRef<HTMLDivElement[]>([]);
useEffect(() => {
pagesRef.current = pagesRef.current.slice(0, pages.length);
}, [pages.length]);
function nextPage() {
if (curPage < pages.length - 1) {
pagesRef.current[curPage + 1]?.scrollIntoView();
setCurPage((page) => page + 1);
} else if (settings.loadNextonEnding) {
nextChapter();
}
}
function prevPage() {
if (curPage > 0) {
const rect = pagesRef.current[curPage].getBoundingClientRect();
if (rect.y < 0 && rect.y + rect.height > 0) {
pagesRef.current[curPage]?.scrollIntoView();
} else {
pagesRef.current[curPage - 1]?.scrollIntoView();
setCurPage(curPage - 1);
}
} else if (curPage === 0) {
prevChapter();
}
}
function keyboardControl(e:KeyboardEvent) {
switch (e.code) {
case 'Space':
e.preventDefault();
nextPage();
break;
case 'ArrowRight':
nextPage();
break;
case 'ArrowLeft':
prevPage();
break;
default:
break;
}
}
function clickControl(e:MouseEvent) {
if (e.clientX > window.innerWidth / 2) {
nextPage();
} else {
prevPage();
}
}
const handleLoadNextonEnding = () => {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
nextChapter();
}
};
useEffect(() => {
if (settings.loadNextonEnding) { document.addEventListener('scroll', handleLoadNextonEnding); }
document.addEventListener('keydown', keyboardControl, false);
selfRef.current?.addEventListener('click', clickControl);
return () => {
document.removeEventListener('scroll', handleLoadNextonEnding);
document.removeEventListener('keydown', keyboardControl);
selfRef.current?.removeEventListener('click', clickControl);
};
}, [selfRef, curPage]);
useEffect(() => {
// scroll last read page into view
let initialPage = (chapter as IChapter).lastPageRead;
if (initialPage > pages.length - 1) {
initialPage = pages.length - 1;
}
if (initialPage > -1) {
pagesRef.current[initialPage].scrollIntoView();
}
}, [pagesRef.current.length]);
return (
<div ref={selfRef} className={classes.reader}>
{
pages.map((page) => (
<Page
key={page.index}
index={page.index}
src={page.src}
onImageLoad={() => {}}
setCurPage={setCurPage}
settings={settings}
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
/>
))
}
</div>
);
}

View File

@@ -1,21 +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 ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import React from 'react';
export default function CheckBoxPreference({ title, summary }: CheckBoxPreferenceProps) {
return (
<ListItem>
<ListItemText
primary={title}
secondary={summary}
/>
</ListItem>
);
}

View File

@@ -1,67 +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, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import NavBarContext from '../../context/NavbarContext';
import DarkTheme from '../../context/DarkTheme';
import TemporaryDrawer from '../TemporaryDrawer';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
}));
export default function NavBar() {
const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false);
const { title, action, override } = useContext(NavBarContext);
const { darkTheme } = useContext(DarkTheme);
return (
<>
{override.status && override.value}
{!override.status
&& (
<div className={classes.root}>
<AppBar position="fixed" color={darkTheme ? 'default' : 'primary'}>
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
disableRipple
onClick={() => setDrawerOpen(true)}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
{title}
</Typography>
{action}
</Toolbar>
</AppBar>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</div>
)}
</>
);
}

View File

@@ -1,393 +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 IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft';
import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
import { makeStyles } from '@material-ui/core/styles';
import React, { useContext, useEffect, useState } from 'react';
import Typography from '@material-ui/core/Typography';
import { useHistory, Link } from 'react-router-dom';
import Slide from '@material-ui/core/Slide';
import Fade from '@material-ui/core/Fade';
import Zoom from '@material-ui/core/Zoom';
import { Switch } from '@material-ui/core';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import Collapse from '@material-ui/core/Collapse';
import Button from '@material-ui/core/Button';
import NavBarContext from '../../context/NavbarContext';
const useStyles = (settings: IReaderSettings) => makeStyles({
// main container and root div need to change classes...
AppMainContainer: {
display: 'none',
},
AppRootElment: {
display: 'flex',
},
root: {
position: settings.staticNav ? 'sticky' : 'fixed',
top: 0,
left: 0,
width: '300px',
height: '100vh',
overflowY: 'auto',
backgroundColor: '#0a0b0b',
'& header': {
backgroundColor: '#363b3d',
display: 'flex',
alignItems: 'center',
minHeight: '64px',
paddingLeft: '24px',
paddingRight: '24px',
transition: 'left 2s ease',
'& button': {
flexGrow: 0,
flexShrink: 0,
},
'& button:nth-child(1)': {
marginRight: '16px',
},
'& button:nth-child(3)': {
marginRight: '-12px',
},
'& h1': {
fontSize: '1.25rem',
flexGrow: 1,
},
},
'& hr': {
margin: '0 16px',
height: '1px',
border: '0',
backgroundColor: 'rgb(38, 41, 43)',
},
},
navigation: {
margin: '0 16px',
'& > span:nth-child(1)': {
textAlign: 'center',
display: 'block',
marginTop: '16px',
},
'& $navigationChapters': {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gridTemplateAreas: '"prev next"',
gridColumnGap: '5px',
margin: '10px 0',
'& a': {
flexGrow: 1,
textDecoration: 'none',
'& button': {
width: '100%',
padding: '5px 8px',
textTransform: 'none',
},
},
},
},
navigationChapters: {}, // dummy rule
settingsCollapsseHeader: {
'& span': {
fontWeight: 'bold',
},
},
openDrawerButton: {
position: 'fixed',
top: 0 + 20,
left: 10 + 20,
height: '40px',
width: '40px',
borderRadius: 5,
backgroundColor: 'black',
'&:hover': {
backgroundColor: 'black',
},
},
});
export const defaultReaderSettings = () => ({
staticNav: false,
showPageNumber: true,
continuesPageGap: false,
loadNextonEnding: false,
readerType: 'ContinuesVertical',
} as IReaderSettings);
interface IProps {
settings: IReaderSettings
setSettings: React.Dispatch<React.SetStateAction<IReaderSettings>>
manga: IManga | IMangaCard
chapter: IChapter | IPartialChpter
curPage: number
}
export default function ReaderNavBar(props: IProps) {
const { title } = useContext(NavBarContext);
const history = useHistory();
const {
settings, setSettings, manga, chapter, curPage,
} = props;
const [drawerOpen, setDrawerOpen] = useState(false || settings.staticNav);
const [hideOpenButton, setHideOpenButton] = useState(false);
const [prevScrollPos, setPrevScrollPos] = useState(0);
const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(true);
const classes = useStyles(settings)();
const setSettingValue = (key: string, value: any) => setSettings({ ...settings, [key]: value });
const handleScroll = () => {
const currentScrollPos = window.pageYOffset;
if (Math.abs(currentScrollPos - prevScrollPos) > 20) {
setHideOpenButton(currentScrollPos > prevScrollPos);
setPrevScrollPos(currentScrollPos);
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
const rootEl = document.querySelector('#root')!;
const mainContainer = document.querySelector('#appMainContainer')!;
rootEl.classList.add(classes.AppRootElment);
mainContainer.classList.add(classes.AppMainContainer);
return () => {
rootEl.classList.remove(classes.AppRootElment);
mainContainer.classList.remove(classes.AppMainContainer);
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);// handleScroll changes on every render
return (
<>
<Slide
direction="right"
in={drawerOpen}
timeout={200}
appear={false}
mountOnEnter
unmountOnExit
>
<div className={classes.root}>
<header>
<IconButton
edge="start"
color="inherit"
aria-label="menu"
disableRipple
onClick={() => history.push(`/manga/${manga.id}`)}
>
<CloseIcon />
</IconButton>
<Typography variant="h1">
{title}
</Typography>
{!settings.staticNav
&& (
<IconButton
edge="start"
color="inherit"
aria-label="menu"
disableRipple
onClick={() => setDrawerOpen(false)}
>
<KeyboardArrowLeftIcon />
</IconButton>
) }
</header>
<ListItem ContainerComponent="div" className={classes.settingsCollapsseHeader}>
<ListItemText primary="Reader Settings" />
<ListItemSecondaryAction>
<IconButton
edge="start"
color="inherit"
aria-label="menu"
disableRipple
disableFocusRipple
onClick={() => setSettingsCollapseOpen(!settingsCollapseOpen)}
>
{settingsCollapseOpen && <KeyboardArrowUpIcon />}
{!settingsCollapseOpen && <KeyboardArrowDownIcon />}
</IconButton>
</ListItemSecondaryAction>
</ListItem>
<Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit>
<List>
<ListItem>
<ListItemText primary="Static Navigation" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={settings.staticNav}
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Show page number" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={settings.showPageNumber}
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Load next chapter at ending" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={settings.loadNextonEnding}
onChange={(e) => setSettingValue('loadNextonEnding', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Reader Type" />
<Select
value={settings.readerType}
onChange={(e) => setSettingValue('readerType', e.target.value)}
>
<MenuItem value="SingleLTR">
Single Page (LTR)
</MenuItem>
<MenuItem value="SingleRTL">
Single Page (RTL)
</MenuItem>
{/* <MenuItem value="SingleVertical">
Vertical(WIP)
</MenuItem> */}
<MenuItem value="DoubleLTR">
Double Page (LTR)
</MenuItem>
<MenuItem value="DoubleRTL">
Double Page (RTL)
</MenuItem>
<MenuItem value="Webtoon">
Webtoon
</MenuItem>
<MenuItem value="ContinuesVertical">
Continues Vertical
</MenuItem>
<MenuItem value="ContinuesHorizontalLTR">
Horizontal (LTR)
</MenuItem>
<MenuItem value="ContinuesHorizontalRTL">
Horizontal (RTL)
</MenuItem>
</Select>
</ListItem>
</List>
</Collapse>
<hr />
<div className={classes.navigation}>
<span>
Currently on page
{' '}
{curPage + 1}
{' '}
of
{' '}
{chapter.pageCount}
</span>
<div className={classes.navigationChapters}>
{chapter.index > 1
&& (
<Link
style={{ gridArea: 'prev' }}
to={`/manga/${manga.id}/chapter/${chapter.index - 1}`}
>
<Button
variant="outlined"
startIcon={<KeyboardArrowLeftIcon />}
>
Chapter
{' '}
{chapter.index - 1}
</Button>
</Link>
)}
{chapter.index < chapter.chapterCount
&& (
<Link
style={{ gridArea: 'next' }}
to={`/manga/${manga.id}/chapter/${chapter.index + 1}`}
>
<Button
variant="outlined"
endIcon={<KeyboardArrowRightIcon />}
>
Chapter
{' '}
{chapter.index + 1}
</Button>
</Link>
)}
</div>
</div>
</div>
</Slide>
<Zoom in={!drawerOpen}>
<Fade in={!hideOpenButton}>
<IconButton
className={classes.openDrawerButton}
edge="start"
color="inherit"
aria-label="menu"
disableRipple
disableFocusRipple
onClick={() => setDrawerOpen(true)}
>
<KeyboardArrowRightIcon />
</IconButton>
</Fade>
</Zoom>
</>
);
}

View File

@@ -1,20 +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';
type ContextType = {
darkTheme: boolean
setDarkTheme: React.Dispatch<React.SetStateAction<boolean>>
};
const DarkTheme = React.createContext<ContextType>({
darkTheme: true,
setDarkTheme: ():void => {},
});
export default DarkTheme;

View File

@@ -1,28 +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';
type ContextType = {
title: string
setTitle: React.Dispatch<React.SetStateAction<string>>
action: any
setAction: React.Dispatch<React.SetStateAction<any>>
override: INavbarOverride
setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>>
};
const NavBarContext = React.createContext<ContextType>({
title: 'Tachidesk',
setTitle: ():void => {},
action: <div />,
setAction: ():void => {},
override: { status: false, value: <div /> },
setOverride: ():void => {},
});
export default NavBarContext;

View File

@@ -1,10 +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/. */
body {
margin: 0;
}

View File

@@ -1,20 +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 ReactDOM from 'react-dom';
import App from './App';
import './index.css';
// roboto font
import '@fontsource/roboto';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);

View File

@@ -1,8 +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/. */
/// <reference types="react-scripts" />

View File

@@ -1,134 +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 List from '@material-ui/core/List';
import ListAltIcon from '@material-ui/icons/ListAlt';
import BackupIcon from '@material-ui/icons/Backup';
import Brightness6Icon from '@material-ui/icons/Brightness6';
import DnsIcon from '@material-ui/icons/Dns';
import EditIcon from '@material-ui/icons/Edit';
import InfoIcon from '@material-ui/icons/Info';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import Switch from '@material-ui/core/Switch';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import TextField from '@material-ui/core/TextField';
import NavbarContext from '../context/NavbarContext';
import DarkTheme from '../context/DarkTheme';
import useLocalStorage from '../util/useLocalStorage';
import ListItemLink from '../util/ListItemLink';
export default function Settings() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Settings'); setAction(<></>); }, []);
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
const [serverAddress, setServerAddress] = useLocalStorage<String>('serverBaseURL', '');
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(serverAddress);
const handleDialogOpen = () => {
setDialogValue(serverAddress);
setDialogOpen(true);
};
const handleDialogCancel = () => {
setDialogOpen(false);
};
const handleDialogSubmit = () => {
setDialogOpen(false);
setServerAddress(dialogValue);
};
return (
<>
<List style={{ padding: 0 }}>
<ListItemLink href="/settings/categories">
<ListItemIcon>
<ListAltIcon />
</ListItemIcon>
<ListItemText primary="Categories" />
</ListItemLink>
<ListItemLink href="/settings/backup">
<ListItemIcon>
<BackupIcon />
</ListItemIcon>
<ListItemText primary="Backup" />
</ListItemLink>
<ListItem>
<ListItemIcon>
<Brightness6Icon />
</ListItemIcon>
<ListItemText primary="Dark Theme" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={darkTheme}
onChange={() => setDarkTheme(!darkTheme)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemIcon>
<DnsIcon />
</ListItemIcon>
<ListItemText primary="Server Address" secondary={serverAddress} />
<ListItemSecondaryAction>
<IconButton
onClick={() => {
handleDialogOpen();
}}
>
<EditIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
<ListItemLink href="/settings/about">
<ListItemIcon>
<InfoIcon />
</ListItemIcon>
<ListItemText primary="About" />
</ListItemLink>
</List>
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
<DialogContent>
<DialogContentText>
Enter new category name.
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="name"
label="Category Name"
type="text"
fullWidth
value={dialogValue}
onChange={(e) => setDialogValue(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogCancel} color="primary">
Cancel
</Button>
<Button onClick={handleDialogSubmit} color="primary">
Set
</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,118 +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 { makeStyles, Theme } from '@material-ui/core/styles';
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<IManga>();
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: IManga) => {
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>
);
}

View File

@@ -1,112 +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 ExtensionLangSelect from 'components/manga/ExtensionLangSelect';
import { defualtLangs, 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;
}
function extensionDefaultLangs() {
return [...defualtLangs(), 'all'];
}
export default function AnimeExtensions() {
const { setTitle, setAction } = useContext(NavbarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
useEffect(() => {
setTitle('Extensions');
setAction(
<ExtensionLangSelect
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>
))
))
}
</>
);
}

View File

@@ -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 ExtensionLangSelect from 'components/manga/ExtensionLangSelect';
import SourceCard from 'components/anime/SourceCard';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import { defualtLangs, 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', defualtLangs());
const [sources, setSources] = useState<ISource[]>([]);
const [fetched, setFetched] = useState<boolean>(false);
useEffect(() => {
setTitle('Sources');
setAction(
<ExtensionLangSelect
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>
)
))}
</>
);
}

View File

@@ -1,77 +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 '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles';
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',
},
video: {
maxWidth: '100%',
maxHeight: '100%',
},
});
const initialEpisode = () => ({ linkUrl: '', index: -1, episodeCount: 0 });
export default function Player() {
const classes = useStyles();
const { episodeIndex, animeId } = useParams<{ episodeIndex: string, animeId: string }>();
const [episode, setEpisode] = useState<IEpisode | IPartialEpisode>(initialEpisode());
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: IManga) => {
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);
setEpisodeLink(data.linkUrl);
});
}, [episodeIndex]);
// return spinner while chpater data is loading
if (episode.linkUrl === '') {
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>
<source src={episodeLink} />
</video>
</div>
);
}

View File

@@ -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: IManga[], 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}
/>
);
}

View File

@@ -1,153 +0,0 @@
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable react/destructuring-assignment */
/* eslint-disable react/jsx-props-no-spreading */
/*
* 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 NavbarContext from 'context/NavbarContext';
import React, { useContext, useEffect, useState } from 'react';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import PauseIcon from '@material-ui/icons/Pause';
import IconButton from '@material-ui/core/IconButton';
import client from 'util/client';
import {
DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle,
} from 'react-beautiful-dnd';
import { useTheme } from '@material-ui/core/styles';
import { Palette } from '@material-ui/core/styles/createPalette';
import List from '@material-ui/core/List';
import DragHandleIcon from '@material-ui/icons/DragHandle';
import ListItem from '@material-ui/core/ListItem';
import { ListItemIcon } from '@material-ui/core';
import ListItemText from '@material-ui/core/ListItemText';
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
const getItemStyle = (isDragging: boolean,
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
// styles we need to apply on draggables
...draggableStyle,
...(isDragging && {
background: palette.type === 'dark' ? '#424242' : 'rgb(235,235,235)',
}),
});
const initialQueue = {
status: 'Stopped',
queue: [],
} as IQueue;
export default function DownloadQueue() {
const [, setWsClient] = useState<WebSocket>();
const [queueState, setQueueState] = useState<IQueue>(initialQueue);
const { queue, status } = queueState;
const theme = useTheme();
const { setTitle, setAction } = useContext(NavbarContext);
const toggleQueueStatus = () => {
if (status === 'Stopped') {
client.get('/api/v1/downloads/start');
} else {
client.get('/api/v1/downloads/stop');
}
};
useEffect(() => {
setTitle('Download Queue');
setAction(() => {
if (status === 'Stopped') {
return (
<IconButton onClick={toggleQueueStatus}>
<PlayArrowIcon />
</IconButton>
);
}
return (
<IconButton onClick={toggleQueueStatus}>
<PauseIcon />
</IconButton>
);
});
}, [status]);
useEffect(() => {
const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`);
wsc.onmessage = (e) => {
setQueueState(JSON.parse(e.data));
};
setWsClient(wsc);
}, []);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onDragEnd = (result: DropResult) => {
};
return (
<>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<List ref={provided.innerRef}>
{queue.map((item, index) => (
<Draggable
key={`${item.mangaId}-${item.chapterIndex}`}
draggableId={`${item.mangaId}-${item.chapterIndex}`}
index={index}
>
{(provided, snapshot) => (
<ListItem
ContainerProps={{ ref: provided.innerRef } as any}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style,
theme.palette,
)}
ref={provided.innerRef}
>
<ListItemIcon>
<DragHandleIcon />
</ListItemIcon>
<ListItemText
primary={
`${item.chapter.name} | `
+ ` (${item.progress * 100}%)`
+ ` => state: ${item.state}`
}
/>
{/* <IconButton
onClick={() => {
handleEditDialogOpen(index);
}}
>
<EditIcon />
</IconButton>
<IconButton
onClick={() => {
deleteCategory(index);
}}
>
<DeleteIcon />
</IconButton> */}
</ListItem>
)}
</Draggable>
))}
{provided.placeholder}
</List>
)}
</Droppable>
</DragDropContext>
</>
);
}

View File

@@ -1,161 +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 { Tab, Tabs } from '@material-ui/core';
import React, { useContext, useEffect, useState } from 'react';
import MangaGrid from 'components/manga/MangaGrid';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import cloneObject from 'util/cloneObject';
interface IMangaCategory {
category: ICategory
mangas: IManga[]
isFetched: boolean
}
interface TabPanelProps {
children: React.ReactNode;
index: any;
value: any;
}
function TabPanel(props: TabPanelProps) {
const {
children, value, index,
} = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
>
{value === index && children}
</div>
);
}
export default function Library() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Library'); setAction(<></>); }, []);
const [tabs, setTabs] = useState<IMangaCategory[]>([]);
const [tabNum, setTabNum] = useState<number>(0);
// a hack so MangaGrid doesn't stop working. I won't change it in case
// if I do manga pagination for library..
const [lastPageNum, setLastPageNum] = useState<number>(1);
const handleTabChange = (newTab: number) => {
setTabNum(newTab);
};
useEffect(() => {
Promise.all<IManga[], ICategory[]>([
client.get('/api/v1/library').then((response) => response.data),
client.get('/api/v1/category').then((response) => response.data),
])
.then(
([libraryMangas, categories]) => {
const categoryTabs = categories.map((category) => ({
category,
mangas: [] as IManga[],
isFetched: false,
}));
if (libraryMangas.length > 0 || categoryTabs.length === 0) {
const defaultCategoryTab = {
category: {
name: 'Default',
default: true,
order: 0,
id: -1,
},
mangas: libraryMangas,
isFetched: true,
};
setTabs(
[defaultCategoryTab, ...categoryTabs],
);
} else {
setTabs(categoryTabs);
setTabNum(1);
}
},
);
}, []);
// console.log(client.defaults.baseURL);
// fetch the current tab
useEffect(() => {
tabs.forEach((tab, index) => {
if (tab.category.order === tabNum && !tab.isFetched) {
// eslint-disable-next-line @typescript-eslint/no-shadow
client.get(`/api/v1/category/${tab.category.id}`)
.then((response) => response.data)
.then((data: IManga[]) => {
const tabsClone = cloneObject(tabs);
tabsClone[index].mangas = data;
tabsClone[index].isFetched = true;
setTabs(tabsClone); // clone the object
});
}
});
}, [tabNum]);
let toRender;
if (tabs.length > 1) {
// eslint-disable-next-line max-len
const tabDefines = tabs.map((tab) => (<Tab label={tab.category.name} value={tab.category.order} />));
const tabBodies = tabs.map((tab) => (
<TabPanel value={tabNum} index={tab.category.order}>
<MangaGrid
mangas={tab.mangas}
hasNextPage={false}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
message={tab.isFetched ? 'Category is Empty' : 'Loading...'}
/>
</TabPanel>
));
// Visual Hack: 160px is min-width for viewport width of >600
const scrollableTabs = window.innerWidth < tabs.length * 160;
toRender = (
<>
<Tabs
value={tabNum}
onChange={(e, newTab) => handleTabChange(newTab)}
indicatorColor="primary"
textColor="primary"
centered={!scrollableTabs}
variant={scrollableTabs ? 'scrollable' : 'fullWidth'}
scrollButtons="on"
>
{tabDefines}
</Tabs>
{tabBodies}
</>
);
} else {
const mangas = tabs.length === 1 ? tabs[0].mangas : [];
toRender = (
<MangaGrid
mangas={mangas}
hasNextPage={false}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
message={tabs.length > 0 ? 'Library is Empty' : undefined}
/>
);
}
return toRender;
}

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, { useEffect, useState, useContext } from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { useParams } from 'react-router-dom';
import { Virtuoso } from 'react-virtuoso';
import ChapterCard from 'components/manga/ChapterCard';
import MangaDetails from 'components/manga/MangaDetails';
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',
},
}));
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
const initialQueue = {
status: 'Stopped',
queue: [],
} as IQueue;
export default function Manga() {
const classes = useStyles();
const { setTitle } = useContext(NavbarContext);
useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails
const { id } = useParams<{ id: string }>();
const [manga, setManga] = useState<IManga>();
const [chapters, setChapters] = useState<IChapter[]>([]);
const [fetchedChapters, setFetchedChapters] = useState(false);
const [noChaptersFound, setNoChaptersFound] = useState(false);
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
const [, setWsClient] = useState<WebSocket>();
const [{ queue }, setQueueState] = useState<IQueue>(initialQueue);
function triggerChaptersUpdate() {
setChapterUpdateTriggerer(chapterUpdateTriggerer + 1);
}
useEffect(() => {
const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`);
wsc.onmessage = (e) => {
const data = JSON.parse(e.data) as IQueue;
setQueueState(data);
let shouldUpdate = false;
data.queue.forEach((q) => {
if (q.mangaId === manga?.id && q.state === 'Finished') {
shouldUpdate = true;
}
});
if (shouldUpdate) {
triggerChaptersUpdate();
}
};
setWsClient(wsc);
return () => wsc.close();
}, [queue.length]);
const downloadingStringFor = (chapter: IChapter) => {
let rtn = '';
if (chapter.downloaded) {
rtn = ' • Downloaded';
}
queue.forEach((q) => {
if (chapter.index === q.chapterIndex && chapter.mangaId === q.mangaId) {
rtn = ` • Downloading (${q.progress * 100}%)`;
}
});
return rtn;
};
useEffect(() => {
if (manga === undefined || !manga.freshData) {
client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`)
.then((response) => response.data)
.then((data: IManga) => {
setManga(data);
setTitle(data.title);
});
}
}, [manga]);
useEffect(() => {
const shouldFetchOnline = fetchedChapters && chapterUpdateTriggerer === 0;
client.get(`/api/v1/manga/${id}/chapters?onlineFetch=${shouldFetchOnline}`)
.then((response) => response.data)
.then((data) => {
if (data.length === 0 && fetchedChapters) {
makeToast('No chapters found', 'warning');
setNoChaptersFound(true);
}
setChapters(data);
})
.then(() => setFetchedChapters(true));
}, [chapters.length, fetchedChapters, chapterUpdateTriggerer]);
return (
<div className={classes.root}>
<LoadingPlaceholder
shouldRender={manga !== undefined}
component={MangaDetails}
componentProps={{ manga }}
/>
<LoadingPlaceholder
shouldRender={chapters.length > 0 || noChaptersFound}
>
<Virtuoso
style={{ // override Virtuoso default values and set them with class
height: 'undefined',
overflowY: window.innerWidth < 960 ? 'visible' : 'auto',
}}
className={classes.chapters}
totalCount={chapters.length}
itemContent={(index:number) => (
<ChapterCard
chapter={chapters[index]}
downloadingString={downloadingStringFor(chapters[index])}
triggerChaptersUpdate={triggerChaptersUpdate}
/>
)}
useWindowScroll={window.innerWidth < 960}
overscan={window.innerHeight * 0.5}
/>
</LoadingPlaceholder>
</div>
);
}

View File

@@ -1,112 +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/manga/ExtensionCard';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
import ExtensionLangSelect from 'components/manga/ExtensionLangSelect';
import { defualtLangs, 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;
}
function extensionDefaultLangs() {
return [...defualtLangs(), 'all'];
}
export default function MangaExtensions() {
const { setTitle, setAction } = useContext(NavbarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
useEffect(() => {
setTitle('Extensions');
setAction(
<ExtensionLangSelect
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/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>
))
))
}
</>
);
}

View File

@@ -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 ExtensionLangSelect from 'components/manga/ExtensionLangSelect';
import SourceCard from 'components/manga/SourceCard';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import { defualtLangs, 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 MangaSources() {
const { setTitle, setAction } = useContext(NavbarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', defualtLangs());
const [sources, setSources] = useState<ISource[]>([]);
const [fetched, setFetched] = useState<boolean>(false);
useEffect(() => {
setTitle('Sources');
setAction(
<ExtensionLangSelect
shownLangs={shownLangs}
setShownLangs={setShownLangs}
allLangs={sourceToLangList(sources)}
/>,
);
}, [shownLangs, sources]);
useEffect(() => {
client.get('/api/v1/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>
)
))}
</>
);
}

View File

@@ -1,196 +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 '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles';
import React, { useContext, useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import HorizontalPager from 'components/manga/reader/pager/HorizontalPager';
import PageNumber from 'components/manga/reader/PageNumber';
import PagedPager from 'components/manga/reader/pager/PagedPager';
import DoublePagedPager from 'components/manga/reader/pager/DoublePagedPager';
import VerticalPager from 'components/manga/reader/pager/VerticalPager';
import ReaderNavBar, { defaultReaderSettings } from 'components/navbar/ReaderNavBar';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
import cloneObject from 'util/cloneObject';
const useStyles = (settings: IReaderSettings) => makeStyles({
root: {
width: settings.staticNav ? 'calc(100vw - 300px)' : '100vw',
},
loading: {
margin: '50px auto',
},
});
const getReaderComponent = (readerType: ReaderType) => {
switch (readerType) {
case 'ContinuesVertical':
case 'Webtoon':
return VerticalPager;
break;
case 'SingleVertical':
case 'SingleRTL':
case 'SingleLTR':
return PagedPager;
break;
case 'DoubleVertical':
case 'DoubleRTL':
case 'DoubleLTR':
return DoublePagedPager;
break;
case 'ContinuesHorizontalLTR':
case 'ContinuesHorizontalRTL':
return HorizontalPager;
default:
return VerticalPager;
break;
}
};
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
const initialChapter = () => ({ pageCount: -1, index: -1, chapterCount: 0 });
export default function Reader() {
const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings);
const classes = useStyles(settings)();
const history = useHistory();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string, mangaId: string }>();
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
const [chapter, setChapter] = useState<IChapter | IPartialChpter>(initialChapter());
const [curPage, setCurPage] = useState<number>(0);
const { setOverride, setTitle } = useContext(NavbarContext);
useEffect(() => {
// make sure settings has all the keys
const settingsClone = cloneObject(settings) as any;
const defualtSettings = defaultReaderSettings();
let shouldUpdateSettings = false;
Object.keys(defualtSettings).forEach((key) => {
const keyOf = key as keyof IReaderSettings;
if (settings[keyOf] === undefined) {
settingsClone[keyOf] = defualtSettings[keyOf];
shouldUpdateSettings = true;
}
});
if (shouldUpdateSettings) { setSettings(settingsClone); }
// set the custom navbar
setOverride(
{
status: true,
value: (
<ReaderNavBar
settings={settings}
setSettings={setSettings}
manga={manga}
chapter={chapter}
curPage={curPage}
/>
),
},
);
// clean up for when we leave the reader
return () => setOverride({ status: false, value: <div /> });
}, [manga, chapter, settings, curPage, chapterIndex]);
useEffect(() => {
setTitle('Reader');
client.get(`/api/v1/manga/${mangaId}/`)
.then((response) => response.data)
.then((data: IManga) => {
setManga(data);
setTitle(data.title);
});
}, [chapterIndex]);
useEffect(() => {
setChapter(initialChapter);
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
.then((response) => response.data)
.then((data:IChapter) => {
setChapter(data);
setCurPage(data.lastPageRead);
});
}, [chapterIndex]);
useEffect(() => {
if (curPage !== -1) {
const formData = new FormData();
formData.append('lastPageRead', curPage.toString());
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
}
if (curPage === chapter.pageCount - 1) {
const formDataRead = new FormData();
formDataRead.append('read', 'true');
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formDataRead);
}
}, [curPage]);
// return spinner while chpater data is loading
if (chapter.pageCount === -1) {
return (
<div className={classes.loading}>
<CircularProgress thickness={5} />
</div>
);
}
const nextChapter = () => {
if (chapter.index < chapter.chapterCount) {
const formData = new FormData();
formData.append('lastPageRead', `${chapter.pageCount - 1}`);
formData.append('read', 'true');
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
history.push(`/manga/${manga.id}/chapter/${chapter.index + 1}`);
}
};
const prevChapter = () => {
if (chapter.index > 1) {
history.push(`/manga/${manga.id}/chapter/${chapter.index - 1}`);
}
};
const pages = range(chapter.pageCount).map((index) => ({
index,
src: `${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`,
}));
const ReaderComponent = getReaderComponent(settings.readerType);
return (
<div className={classes.root}>
<PageNumber
settings={settings}
curPage={curPage}
pageCount={chapter.pageCount}
/>
<ReaderComponent
pages={pages}
pageCount={chapter.pageCount}
setCurPage={setCurPage}
curPage={curPage}
settings={settings}
manga={manga}
chapter={chapter}
nextChapter={nextChapter}
prevChapter={prevChapter}
/>
</div>
);
}

View File

@@ -1,109 +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 '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import { useParams } from 'react-router-dom';
import MangaGrid from 'components/manga/MangaGrid';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
const useStyles = makeStyles((theme) => ({
root: {
TextField: {
margin: theme.spacing(1),
width: '25ch',
},
},
}));
export default function SearchSingle() {
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/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/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
.then((response) => response.data)
.then((data: { mangaList: IManga[], 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 = (
<MangaGrid
mangas={mangas}
message={message}
hasNextPage={hasNextPage}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
);
return (
<>
<div className={classes.root}>
<TextField
inputRef={textInput}
error={error}
id="standard-basic"
label="Search text.."
onKeyDown={(e) => e.key === 'Enter' && processInput()}
/>
<Button variant="contained" color="primary" onClick={() => processInput()}>
Search
</Button>
</div>
{mangaGrid}
</>
);
}

View File

@@ -1,49 +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 NavbarContext from 'context/NavbarContext';
import { useParams } from 'react-router-dom';
import client from 'util/client';
import CheckBoxPreference from 'components/manga/sourceConfiguration/CheckBoxPreference';
import List from '@material-ui/core/List';
function getPrefComponent(type: string) {
switch (type) {
case 'CheckBoxPreference':
return CheckBoxPreference;
default:
return CheckBoxPreference;
}
}
export default function SourceConfigure() {
const [sourcePreferences, setSourcePreferences] = useState<SourcePreferences[]>([]);
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Source Configuration'); setAction(<></>); }, []);
const { sourceId } = useParams<{ sourceId: string }>();
useEffect(() => {
client.get(`/api/v1/source/${sourceId}/preferences`)
.then((response) => response.data)
.then((data) => setSourcePreferences(data));
}, []);
console.log(sourcePreferences);
return (
<>
<List style={{ padding: 0 }}>
{sourcePreferences.map(
(it) => React.createElement(getPrefComponent(it.type), it.props),
)}
</List>
</>
);
}

View File

@@ -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 MangaGrid from 'components/manga/MangaGrid';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
export default function SourceMangas(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/source/${sourceId}`)
.then((response) => response.data)
.then((data: { name: string }) => setTitle(data.name));
}, []);
useEffect(() => {
const sourceType = props.popular ? 'popular' : 'latest';
client.get(`/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
.then((response) => response.data)
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
setMangas([
...mangas,
...data.mangaList.map((it) => ({
title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
}))]);
setHasNextPage(data.hasNextPage);
});
}, [lastPageNum]);
return (
<MangaGrid
mangas={mangas}
hasNextPage={hasNextPage}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
);
}

View File

@@ -1,70 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import { CircularProgress, makeStyles } from '@material-ui/core';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import client from '../../util/client';
import ListItemLink from '../../util/ListItemLink';
import NavbarContext from '../../context/NavbarContext';
const useStyles = makeStyles({
loading: {
width: '100vw',
'& div': {
margin: '50px auto',
display: 'block',
},
},
});
export default function About() {
const { setTitle, setAction } = useContext(NavbarContext);
const classes = useStyles();
const [about, setAbout] = useState<IAbout>();
useEffect(() => { setTitle('About'); setAction(<></>); }, []);
useEffect(() => {
client.get('/api/v1/settings/about')
.then((response) => response.data)
.then((data:IAbout) => {
setAbout(data);
});
}, []);
if (about === undefined) {
return (
<div className={classes.loading}>
<CircularProgress thickness={5} />
</div>
);
}
const version = () => {
if (about.buildType === 'Stable') return `${about.version}`;
return `${about.version}-${about.revision}`;
};
const buildTime = () => new Date(about.buildTime * 1000).toUTCString();
return (
<List>
<ListItem>
<ListItemText primary="Server" secondary={`${about.name} ${about.buildType}`} />
</ListItem>
<ListItem>
<ListItemText primary="Server version" secondary={version()} />
</ListItem>
<ListItem>
<ListItemText primary="Build time" secondary={buildTime()} />
</ListItem>
<ListItemLink href={about.github}>
<ListItemText primary="Github" secondary={about.github} />
</ListItemLink>
<ListItemLink href={about.discord}>
<ListItemText primary="Discord" secondary={about.discord} />
</ListItemLink>
</List>
);
}

View File

@@ -1,91 +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 } from 'react';
import { ListItemIcon } from '@material-ui/core';
import List from '@material-ui/core/List';
import InboxIcon from '@material-ui/icons/Inbox';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import { fromEvent } from 'file-selector';
import ListItemLink from '../../util/ListItemLink';
import NavbarContext from '../../context/NavbarContext';
import client from '../../util/client';
export default function Backup() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Backup'); setAction(<></>); }, []);
const { baseURL } = client.defaults;
const submitBackup = (file: File) => {
file.text()
.then(
(fileContent: string) => {
client.post('/api/v1/backup/legacy/import',
fileContent, { headers: { 'Content-Type': 'application/json' } });
},
);
};
const dropHandler = async (e: Event) => {
e.preventDefault();
const files = await fromEvent(e);
submitBackup(files[0] as File);
};
const dragOverHandler = (e: Event) => {
e.preventDefault();
};
useEffect(() => {
document.addEventListener('drop', dropHandler);
document.addEventListener('dragover', dragOverHandler);
const input = document.getElementById('backup-file');
input?.addEventListener('change', async (evt) => {
const files = await fromEvent(evt);
submitBackup(files[0] as File);
});
return () => {
document.removeEventListener('drop', dropHandler);
document.removeEventListener('dragover', dragOverHandler);
};
}, []);
return (
<List style={{ padding: 0 }}>
<ListItemLink href={`${baseURL}/api/v1/backup/legacy/export/file`}>
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText
primary="Create Legacy Backup"
secondary="Backup library as a Tachiyomi legacy backup"
/>
</ListItemLink>
<ListItem button onClick={() => document.getElementById('backup-file')?.click()}>
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText
primary="Restore Legacy Backup"
secondary="You can also drop the backup file anywhere to restore"
/>
<input
type="file"
name="backup.json"
id="backup-file"
style={{ display: 'none' }}
/>
</ListItem>
</List>
);
}

View File

@@ -1,249 +0,0 @@
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable react/destructuring-assignment */
/* eslint-disable react/jsx-props-no-spreading */
/*
* 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, useContext, useEffect } from 'react';
import {
List,
ListItem,
ListItemText,
ListItemIcon,
IconButton,
} from '@material-ui/core';
import {
DragDropContext, Droppable, Draggable, DropResult, DraggingStyle, NotDraggingStyle,
} from 'react-beautiful-dnd';
import DragHandleIcon from '@material-ui/icons/DragHandle';
import EditIcon from '@material-ui/icons/Edit';
import { useTheme } from '@material-ui/core/styles';
import Fab from '@material-ui/core/Fab';
import AddIcon from '@material-ui/icons/Add';
import DeleteIcon from '@material-ui/icons/Delete';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import { Palette } from '@material-ui/core/styles/createPalette';
const getItemStyle = (isDragging: boolean,
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
// styles we need to apply on draggables
...draggableStyle,
...(isDragging && {
background: palette.type === 'dark' ? '#424242' : 'rgb(235,235,235)',
}),
});
export default function Categories() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);
const [categories, setCategories] = useState<ICategory[]>([]);
const [categoryToEdit, setCategoryToEdit] = useState<number>(-1); // -1 means new category
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [dialogName, setDialogName] = useState<string>('');
const [dialogDefault, setDialogDefault] = useState<boolean>(false);
const theme = useTheme();
const [updateTriggerHolder, setUpdateTriggerHolder] = useState<number>(0); // just a hack
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
useEffect(() => {
if (!dialogOpen) {
client.get('/api/v1/category/')
.then((response) => response.data)
.then((data) => setCategories(data));
}
}, [updateTriggerHolder]);
const categoryReorder = (list: ICategory[], from: number, to: number) => {
const category = list[from];
const formData = new FormData();
formData.append('from', `${from + 1}`);
formData.append('to', `${to + 1}`);
client.post(`/api/v1/category/${category.id}/reorder`, formData)
.finally(() => triggerUpdate());
// also move it in local state to avoid jarring moving behviour...
const result = Array.from(list);
const [removed] = result.splice(from, 1);
result.splice(to, 0, removed);
return result;
};
const onDragEnd = (result: DropResult) => {
// dropped outside the list?
if (!result.destination) {
return;
}
setCategories(categoryReorder(
categories,
result.source.index,
result.destination.index,
));
};
const resetDialog = () => {
setDialogName('');
setDialogDefault(false);
setCategoryToEdit(-1);
};
const handleDialogOpen = () => {
resetDialog();
setDialogOpen(true);
};
const handleEditDialogOpen = (index:number) => {
setDialogName(categories[index].name);
setDialogDefault(categories[index].default);
setCategoryToEdit(index);
setDialogOpen(true);
};
const handleDialogCancel = () => {
setDialogOpen(false);
};
const handleDialogSubmit = () => {
setDialogOpen(false);
const formData = new FormData();
formData.append('name', dialogName);
formData.append('default', dialogDefault.toString());
if (categoryToEdit === -1) {
client.post('/api/v1/category/', formData)
.finally(() => triggerUpdate());
} else {
const category = categories[categoryToEdit];
client.patch(`/api/v1/category/${category.id}`, formData)
.finally(() => triggerUpdate());
}
};
const deleteCategory = (index:number) => {
const category = categories[index];
client.delete(`/api/v1/category/${category.id}`)
.finally(() => triggerUpdate());
};
return (
<>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<List ref={provided.innerRef}>
{categories.map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id.toString()}
index={index}
>
{(provided, snapshot) => (
<ListItem
ContainerProps={{ ref: provided.innerRef } as any}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style,
theme.palette,
)}
ref={provided.innerRef}
>
<ListItemIcon>
<DragHandleIcon />
</ListItemIcon>
<ListItemText
primary={item.name}
/>
<IconButton
onClick={() => {
handleEditDialogOpen(index);
}}
>
<EditIcon />
</IconButton>
<IconButton
onClick={() => {
deleteCategory(index);
}}
>
<DeleteIcon />
</IconButton>
</ListItem>
)}
</Draggable>
))}
{provided.placeholder}
</List>
)}
</Droppable>
</DragDropContext>
<Fab
color="primary"
aria-label="add"
style={{
position: 'absolute',
bottom: theme.spacing(2),
right: theme.spacing(2),
}}
onClick={handleDialogOpen}
>
<AddIcon />
</Fab>
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
<DialogTitle id="form-dialog-title">
{categoryToEdit === -1 ? 'New Catalog' : 'Edit Catalog'}
</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
id="name"
label="Category Name"
type="text"
fullWidth
value={dialogName}
onChange={(e) => setDialogName(e.target.value)}
/>
<FormControlLabel
control={(
<Checkbox
checked={dialogDefault}
onChange={(e) => setDialogDefault(e.target.checked)}
color="default"
/>
)}
label="Default category when adding new manga to library"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogCancel} color="primary">
Cancel
</Button>
<Button onClick={handleDialogSubmit} color="primary">
Submit
</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,188 +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/. */
interface IExtension {
name: string
pkgName: string
versionName: string
versionCode: number
lang: string
isNsfw: boolean
apkName: string
iconUrl: string
installed: boolean
hasUpdate: boolean
obsolete: boolean
}
interface ISource {
id: string
name: string
lang: string
iconUrl: string
supportsLatest: boolean
history: any
isConfigurable: boolean
}
interface IMangaCard {
id: number
title: string
thumbnailUrl: string
}
interface IManga {
id: number
sourceId: string
url: string
title: string
thumbnailUrl: string
artist: string
author: string
description: string
genre: string
status: string
inLibrary: boolean
source: ISource
freshData: boolean
}
interface IChapter {
url: string
name: string
uploadDate: number
chapterNumber: number
scanlator: String
mangaId: number
read: boolean
bookmarked: boolean
lastPageRead: number
index: number
chapterCount: number
pageCount: number
downloaded: boolean
}
interface IEpisode {
url: string
name: string
uploadDate: number
episodeNumber: number
scanlator: String
animeId: number
read: boolean
bookmarked: boolean
lastPageRead: number
index: number
episodeCount: number
linkUrl: string
}
interface IPartialChpter {
pageCount: number
index: number
chapterCount: number
}
interface IPartialEpisode {
linkUrl: string
index: number
episodeCount: number
}
interface ICategory {
id: number
order: number
name: string
default: boolean
}
interface INavbarOverride {
status: boolean
value: any
}
type ReaderType =
'ContinuesVertical'|
'Webtoon' |
'SingleVertical' |
'SingleRTL' |
'SingleLTR' |
'DoubleVertical' |
'DoubleRTL' |
'DoubleLTR' |
'ContinuesHorizontalLTR'|
'ContinuesHorizontalRTL';
interface IReaderSettings{
staticNav: boolean
showPageNumber: boolean
loadNextonEnding: boolean
readerType: ReaderType
}
interface IReaderPage {
index: number
src: string
}
interface IReaderProps {
pages: Array<IReaderPage>
pageCount: number
setCurPage: React.Dispatch<React.SetStateAction<number>>
curPage: number
settings: IReaderSettings
manga: IMangaCard | IManga
chapter: IChapter | IPartialChpter
nextChapter: () => void
prevChapter: () => void
}
interface IAbout {
name: string
version: string
revision: string
buildType: 'Stable' | 'Preview'
buildTime: number
github: string
discord: string
}
interface IDownloadChapter{
chapterIndex: number
mangaId: number
state: 'Queued' | 'Downloading' | 'Finished' | 'Error'
progress: number
chapter: IChapter
}
interface IQueue {
status: 'Stopped' | 'Started'
queue: IDownloadChapter[]
}
interface SourcePreferences {
type: string
props: any
}
interface PreferenceProps {
key: string
title: string
summary: string
defaultValue: any
currentValue: any
defaultValueType: string
}
interface CheckBoxPreferenceProps extends PreferenceProps {
}

View File

@@ -1,14 +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 ListItem, { ListItemProps } from '@material-ui/core/ListItem';
export default function ListItemLink(props: ListItemProps<'a', { button?: true }>) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <ListItem button component="a" {...props} />;
}

View File

@@ -1,29 +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 axios from 'axios';
import storage from './localStorage';
const { hostname, port, protocol } = window.location;
// if port is 3000 it's probably running from webpack devlopment server
let inferredPort;
if (port === '3000') { inferredPort = '4567'; } else { inferredPort = port; }
const client = axios.create({
// baseURL must not have traling slash
baseURL: storage.getItem('serverBaseURL', `${protocol}//${hostname}:${inferredPort}`),
});
client.interceptors.request.use((config) => {
if (config.data instanceof FormData) {
Object.assign(config.headers, { 'Content-Type': 'multipart/form-data' });
}
return config;
});
export default client;

View File

@@ -1,10 +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/. */
export default function cloneObject<T extends object>(obj: T) {
return JSON.parse(JSON.stringify(obj)) as T;
}

View File

@@ -1,88 +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/. */
export const ISOLanguages = [
{ code: 'all', name: 'All', nativeName: 'All' },
{ code: 'installed', name: 'Installed', nativeName: 'Installed' },
{ code: 'updates pending', name: 'Updates pending', nativeName: 'Updates pending' },
// full list: https://github.com/meikidd/iso-639-1/blob/master/src/data.js
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'ca', name: 'Catalan; Valencian', nativeName: 'Català' },
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
{ code: 'es', name: 'Spanish; Castilian', nativeName: 'Español' },
{ code: 'fr', name: 'French', nativeName: 'Français' },
{ code: 'id', name: 'Indonesian', nativeName: 'Indonesia' },
{ code: 'it', name: 'Italian', nativeName: 'Italiano' },
{ code: 'pt', name: 'Portuguese', nativeName: 'Português' },
{ code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt' },
{ code: 'tr', name: 'Turkish', nativeName: 'Türkçe' },
{ code: 'ru', name: 'Russian', nativeName: 'русский' },
{ code: 'ar', name: 'Arabic', nativeName: 'العربية' },
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' },
{ code: 'th', name: 'Thai', nativeName: 'ไทย' },
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
{ code: 'ja', name: 'Japanese', nativeName: '日本語' },
{ code: 'ko', name: 'Korean', nativeName: '한국어' },
{ code: 'zu', name: 'Zulu', nativeName: 'isiZulu' },
{ code: 'xh', name: 'Xhosa', nativeName: 'isiXhosa' },
{ code: 'uk', name: 'Ukrainian', nativeName: 'Українська' },
{ code: 'ro', name: 'Romanian', nativeName: 'Română' },
{ code: 'bg', name: 'Bulgarian', nativeName: 'български' },
{ code: 'cs', name: 'Czech', nativeName: 'čeština' },
{ code: 'pl', name: 'Polish', nativeName: 'polski' },
{ code: 'no', name: 'Norwegian', nativeName: 'Norsk' },
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
{ code: 'my', name: 'Burmese', nativeName: 'ဗမာစာ' },
{ code: 'ms', name: 'Malay', nativeName: 'Malaysia' },
{ code: 'mn', name: 'Mongolian', nativeName: 'Монгол' },
{ code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം' },
{ code: 'ku', name: 'Kurdish', nativeName: 'Kurdî' },
{ code: 'hu', name: 'Hungarian', nativeName: 'Magyar' },
{ code: 'hr', name: 'Croatian', nativeName: 'Hrvatski' },
{ code: 'he', name: 'Hebrew', nativeName: 'עברית' },
{ code: 'fil', name: 'Filipino', nativeName: 'Filipino' },
{ code: 'fi', name: 'Finnish', nativeName: 'suomi' },
{ code: 'fa', name: 'Persian', nativeName: 'فارسی' },
{ code: 'eu', name: 'Basque', nativeName: 'euskara' },
{ code: 'el', name: 'Greek', nativeName: 'Ελληνικά' },
{ code: 'da', name: 'Danish', nativeName: 'dansk' },
];
export function langCodeToName(code: string): string {
const whereToCut = code.indexOf('-') !== -1 ? code.indexOf('-') : code.length;
const proccessedCode = code.toLocaleLowerCase().substring(0, whereToCut);
let result = 'Error';
for (let i = 0; i < ISOLanguages.length; i++) {
if (ISOLanguages[i].code === proccessedCode) result = ISOLanguages[i].nativeName;
}
if (code.indexOf('-') !== -1) {
result = `${result} (${code.substring(whereToCut + 1)})`;
}
return result;
}
export function defualtLangs() {
return [
// todo: infer this from the browser
'en',
];
}
export const langSortCmp = (a: string, b: string) => {
// puts english first for convience
const aLang = langCodeToName(a);
const bLang = langCodeToName(b);
if (a === 'en') return -1;
if (b === 'en') return 1;
return aLang > bLang ? 1 : -1;
};

View File

@@ -1,31 +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/. */
function getItem<T>(key: string, defaultValue: T) : T {
try {
const item = window.localStorage.getItem(key);
if (item !== null) {
return JSON.parse(item);
}
window.localStorage.setItem(key, JSON.stringify(defaultValue));
/* eslint-disable no-empty */
} finally { }
return defaultValue;
}
function setItem<T>(key: string, value: T): void {
try {
window.localStorage.setItem(key, JSON.stringify(value));
// eslint-disable-next-line no-empty
} finally { }
}
export default { getItem, setItem };

View File

@@ -1,24 +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, Dispatch, SetStateAction } from 'react';
import storage from './localStorage';
// eslint-disable-next-line max-len
export default function useLocalStorage<T>(key: string, defaultValue: T | (() => T)) : [T, Dispatch<SetStateAction<T>>] {
const initialState = defaultValue instanceof Function ? defaultValue() : defaultValue;
const [storedValue, setStoredValue] = useState<T>(storage.getItem(key, initialState));
const setValue = ((value: T | ((prevState: T) => T)) => {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
storage.setItem(key, valueToStore);
}) as React.Dispatch<React.SetStateAction<T>>;
return [storedValue, setValue];
}

View File

@@ -1,27 +0,0 @@
{
"compilerOptions": {
"baseUrl": "./src",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

File diff suppressed because it is too large Load Diff