mirror of
https://github.com/tachiyomiorg/tachiyomi-extensions-inspector.git
synced 2024-12-26 16:51:50 +01:00
Implemented Dowloads front-end
This commit is contained in:
parent
224c24ee9f
commit
5023e96301
@ -83,6 +83,7 @@ object DownloadManager {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
start()
|
||||||
}
|
}
|
||||||
notifyAllClients()
|
notifyAllClients()
|
||||||
}
|
}
|
||||||
@ -93,10 +94,14 @@ object DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
|
if (downloader != null && !downloader?.isAlive!!) // doesn't exist or is dead
|
||||||
|
downloader = null
|
||||||
|
|
||||||
if (downloader == null) {
|
if (downloader == null) {
|
||||||
downloader = Downloader(downloadQueue) { notifyAllClients() }
|
downloader = Downloader(downloadQueue) { notifyAllClients() }
|
||||||
downloader!!.start()
|
downloader!!.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyAllClients()
|
notifyAllClients()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,10 @@ class Downloader(private val downloadQueue: CopyOnWriteArrayList<DownloadChapter
|
|||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
do {
|
do {
|
||||||
val download = downloadQueue.firstOrNull { it.state == Queued } ?: break
|
val download = downloadQueue.firstOrNull {
|
||||||
|
it.state == Queued ||
|
||||||
|
(it.state == Error && it.tries < 3) // 3 re-tries per download
|
||||||
|
} ?: break
|
||||||
|
|
||||||
try {
|
try {
|
||||||
download.state = Downloading
|
download.state = Downloading
|
||||||
@ -44,7 +47,7 @@ class Downloader(private val downloadQueue: CopyOnWriteArrayList<DownloadChapter
|
|||||||
download.chapter = runBlocking { getChapter(download.chapterIndex, download.mangaId) }
|
download.chapter = runBlocking { getChapter(download.chapterIndex, download.mangaId) }
|
||||||
step()
|
step()
|
||||||
|
|
||||||
val pageCount = download.chapter!!.pageCount!!
|
val pageCount = download.chapter!!.pageCount
|
||||||
for (pageNum in 0 until pageCount) {
|
for (pageNum in 0 until pageCount) {
|
||||||
runBlocking { getPageImage(download.mangaId, download.chapterIndex, pageNum) }
|
runBlocking { getPageImage(download.mangaId, download.chapterIndex, pageNum) }
|
||||||
// TODO: retry on error with 2,4,8 seconds of wait
|
// TODO: retry on error with 2,4,8 seconds of wait
|
||||||
@ -60,12 +63,15 @@ class Downloader(private val downloadQueue: CopyOnWriteArrayList<DownloadChapter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
step()
|
step()
|
||||||
|
|
||||||
|
downloadQueue.removeIf { it.mangaId == download.mangaId && it.chapterIndex == download.chapterIndex }
|
||||||
|
step()
|
||||||
} catch (e: DownloadShouldStopException) {
|
} catch (e: DownloadShouldStopException) {
|
||||||
println("Downloader was stopped")
|
println("Downloader was stopped")
|
||||||
downloadQueue.filter { it.state == Downloading }.forEach { it.state = Queued }
|
downloadQueue.filter { it.state == Downloading }.forEach { it.state = Queued }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Downloader faced an exception")
|
println("Downloader faced an exception")
|
||||||
downloadQueue.filter { it.state == Downloading }.forEach { it.state = Error }
|
downloadQueue.filter { it.state == Downloading }.forEach { it.state = Error; it.tries++ }
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
} finally {
|
} finally {
|
||||||
notifier()
|
notifier()
|
||||||
|
@ -14,5 +14,6 @@ class DownloadChapter(
|
|||||||
val mangaId: Int,
|
val mangaId: Int,
|
||||||
var state: DownloadState = DownloadState.Queued,
|
var state: DownloadState = DownloadState.Queued,
|
||||||
var progress: Float = 0f,
|
var progress: Float = 0f,
|
||||||
|
var tries: Int = 0,
|
||||||
var chapter: ChapterDataClass? = null,
|
var chapter: ChapterDataClass? = null,
|
||||||
)
|
)
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^17.0.2",
|
"@types/react": "^17.0.2",
|
||||||
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-dom": "^17.0.2",
|
"@types/react-dom": "^17.0.2",
|
||||||
"@types/react-lazyload": "^3.1.0",
|
"@types/react-lazyload": "^3.1.0",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
|
@ -34,6 +34,7 @@ import SourceAnimes from 'screens/anime/SourceAnimes';
|
|||||||
import Reader from 'screens/manga/Reader';
|
import Reader from 'screens/manga/Reader';
|
||||||
import Player from 'screens/anime/Player';
|
import Player from 'screens/anime/Player';
|
||||||
import AnimeExtensions from 'screens/anime/AnimeExtensions';
|
import AnimeExtensions from 'screens/anime/AnimeExtensions';
|
||||||
|
import DownloadQueue from 'screens/manga/DownloadQueue';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [title, setTitle] = useState<string>('Tachidesk');
|
const [title, setTitle] = useState<string>('Tachidesk');
|
||||||
@ -125,6 +126,9 @@ export default function App() {
|
|||||||
<Route path="/manga/sources">
|
<Route path="/manga/sources">
|
||||||
<MangaSources />
|
<MangaSources />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/manga/downloads">
|
||||||
|
<DownloadQueue />
|
||||||
|
</Route>
|
||||||
<Route path="/manga/:mangaId/chapter/:chapterNum">
|
<Route path="/manga/:mangaId/chapter/:chapterNum">
|
||||||
<></>
|
<></>
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -14,6 +14,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
|
|||||||
import CollectionsBookmarkIcon from '@material-ui/icons/CollectionsBookmark';
|
import CollectionsBookmarkIcon from '@material-ui/icons/CollectionsBookmark';
|
||||||
import ExploreIcon from '@material-ui/icons/Explore';
|
import ExploreIcon from '@material-ui/icons/Explore';
|
||||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||||
|
import GetAppIcon from '@material-ui/icons/GetApp';
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
import ListItemText from '@material-ui/core/ListItemText';
|
||||||
import SettingsIcon from '@material-ui/icons/Settings';
|
import SettingsIcon from '@material-ui/icons/Settings';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -87,6 +88,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
|||||||
<ListItemText primary="Anime Sources" />
|
<ListItemText primary="Anime Sources" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link to="/manga/downloads" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
|
<ListItem button key="Manga Download Queue">
|
||||||
|
<ListItemIcon>
|
||||||
|
<GetAppIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Manga Download Queue" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
<ListItem button key="settings">
|
<ListItem button key="settings">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
@ -47,12 +47,13 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
interface IProps{
|
interface IProps{
|
||||||
chapter: IChapter
|
chapter: IChapter
|
||||||
triggerChaptersUpdate: () => void
|
triggerChaptersUpdate: () => void
|
||||||
|
downloadingString: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChapterCard(props: IProps) {
|
export default function ChapterCard(props: IProps) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { chapter, triggerChaptersUpdate } = props;
|
const { chapter, triggerChaptersUpdate, downloadingString } = props;
|
||||||
|
|
||||||
const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10);
|
const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10);
|
||||||
|
|
||||||
@ -75,6 +76,11 @@ export default function ChapterCard(props: IProps) {
|
|||||||
.then(() => triggerChaptersUpdate());
|
.then(() => triggerChaptersUpdate());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadChapter = () => {
|
||||||
|
client.get(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0';
|
const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0';
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -101,6 +107,7 @@ export default function ChapterCard(props: IProps) {
|
|||||||
{chapter.scanlator}
|
{chapter.scanlator}
|
||||||
{chapter.scanlator && ' '}
|
{chapter.scanlator && ' '}
|
||||||
{dateStr}
|
{dateStr}
|
||||||
|
{downloadingString}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -115,7 +122,8 @@ export default function ChapterCard(props: IProps) {
|
|||||||
open={Boolean(anchorEl)}
|
open={Boolean(anchorEl)}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
>
|
>
|
||||||
{/* <MenuItem onClick={handleClose}>Download</MenuItem> */}
|
{downloadingString.length === 0
|
||||||
|
&& <MenuItem onClick={downloadChapter}>Download</MenuItem> }
|
||||||
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
||||||
{chapter.bookmarked && 'Remove bookmark'}
|
{chapter.bookmarked && 'Remove bookmark'}
|
||||||
{!chapter.bookmarked && 'Bookmark'}
|
{!chapter.bookmarked && 'Bookmark'}
|
||||||
|
@ -198,7 +198,7 @@ export default function MangaDetails(props: IProps) {
|
|||||||
<div className={classes.top}>
|
<div className={classes.top}>
|
||||||
<div className={classes.leftRight}>
|
<div className={classes.leftRight}>
|
||||||
<div className={classes.leftSide}>
|
<div className={classes.leftSide}>
|
||||||
<img src={`${serverAddress}${manga.thumbnailUrl}?x=${Math.random()}`} alt="Manga Thumbnail" />
|
<img src={`${serverAddress}${manga.thumbnailUrl}`} alt="Manga Thumbnail" />
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.rightSide}>
|
<div className={classes.rightSide}>
|
||||||
<h1>
|
<h1>
|
||||||
|
153
webUI/react/src/screens/manga/DownloadQueue.tsx
Normal file
153
webUI/react/src/screens/manga/DownloadQueue.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
/* 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -41,6 +41,12 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
|
||||||
|
const initialQueue = {
|
||||||
|
status: 'Stopped',
|
||||||
|
queue: [],
|
||||||
|
} as IQueue;
|
||||||
|
|
||||||
export default function Manga() {
|
export default function Manga() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
@ -55,10 +61,48 @@ export default function Manga() {
|
|||||||
const [noChaptersFound, setNoChaptersFound] = useState(false);
|
const [noChaptersFound, setNoChaptersFound] = useState(false);
|
||||||
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
|
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
|
||||||
|
|
||||||
|
const [, setWsClient] = useState<WebSocket>();
|
||||||
|
const [{ queue }, setQueueState] = useState<IQueue>(initialQueue);
|
||||||
|
|
||||||
function triggerChaptersUpdate() {
|
function triggerChaptersUpdate() {
|
||||||
setChapterUpdateTriggerer(chapterUpdateTriggerer + 1);
|
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(() => {
|
useEffect(() => {
|
||||||
if (manga === undefined || !manga.freshData) {
|
if (manga === undefined || !manga.freshData) {
|
||||||
client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`)
|
client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`)
|
||||||
@ -105,6 +149,7 @@ export default function Manga() {
|
|||||||
itemContent={(index:number) => (
|
itemContent={(index:number) => (
|
||||||
<ChapterCard
|
<ChapterCard
|
||||||
chapter={chapters[index]}
|
chapter={chapters[index]}
|
||||||
|
downloadingString={downloadingStringFor(chapters[index])}
|
||||||
triggerChaptersUpdate={triggerChaptersUpdate}
|
triggerChaptersUpdate={triggerChaptersUpdate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
/* 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
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
*
|
*
|
||||||
@ -5,9 +8,6 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-shadow */
|
|
||||||
/* eslint-disable react/destructuring-assignment */
|
|
||||||
/* eslint-disable react/jsx-props-no-spreading */
|
|
||||||
import React, { useState, useContext, useEffect } from 'react';
|
import React, { useState, useContext, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
List,
|
List,
|
||||||
@ -16,7 +16,9 @@ import {
|
|||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
IconButton,
|
IconButton,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
import {
|
||||||
|
DragDropContext, Droppable, Draggable, DropResult, DraggingStyle, NotDraggingStyle,
|
||||||
|
} from 'react-beautiful-dnd';
|
||||||
import DragHandleIcon from '@material-ui/icons/DragHandle';
|
import DragHandleIcon from '@material-ui/icons/DragHandle';
|
||||||
import EditIcon from '@material-ui/icons/Edit';
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
import { useTheme } from '@material-ui/core/styles';
|
import { useTheme } from '@material-ui/core/styles';
|
||||||
@ -31,10 +33,12 @@ import DialogContent from '@material-ui/core/DialogContent';
|
|||||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
import Checkbox from '@material-ui/core/Checkbox';
|
import Checkbox from '@material-ui/core/Checkbox';
|
||||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||||
import NavbarContext from '../../context/NavbarContext';
|
import NavbarContext from 'context/NavbarContext';
|
||||||
import client from '../../util/client';
|
import client from 'util/client';
|
||||||
|
import { Palette } from '@material-ui/core/styles/createPalette';
|
||||||
|
|
||||||
const getItemStyle = (isDragging, draggableStyle, palette) => ({
|
const getItemStyle = (isDragging: boolean,
|
||||||
|
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
|
||||||
// styles we need to apply on draggables
|
// styles we need to apply on draggables
|
||||||
...draggableStyle,
|
...draggableStyle,
|
||||||
|
|
||||||
@ -47,14 +51,14 @@ export default function Categories() {
|
|||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);
|
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);
|
||||||
|
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState<ICategory[]>([]);
|
||||||
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
|
const [categoryToEdit, setCategoryToEdit] = useState<number>(-1); // -1 means new category
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
const [dialogName, setDialogName] = useState('');
|
const [dialogName, setDialogName] = useState<string>('');
|
||||||
const [dialogDefault, setDialogDefault] = useState(false);
|
const [dialogDefault, setDialogDefault] = useState<boolean>(false);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
const [updateTriggerHolder, setUpdateTriggerHolder] = useState<number>(0); // just a hack
|
||||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -65,12 +69,12 @@ export default function Categories() {
|
|||||||
}
|
}
|
||||||
}, [updateTriggerHolder]);
|
}, [updateTriggerHolder]);
|
||||||
|
|
||||||
const categoryReorder = (list, from, to) => {
|
const categoryReorder = (list: ICategory[], from: number, to: number) => {
|
||||||
const category = list[from];
|
const category = list[from];
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('from', from + 1);
|
formData.append('from', `${from + 1}`);
|
||||||
formData.append('to', to + 1);
|
formData.append('to', `${to + 1}`);
|
||||||
client.post(`/api/v1/category/${category.id}/reorder`, formData)
|
client.post(`/api/v1/category/${category.id}/reorder`, formData)
|
||||||
.finally(() => triggerUpdate());
|
.finally(() => triggerUpdate());
|
||||||
|
|
||||||
@ -81,7 +85,7 @@ export default function Categories() {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDragEnd = (result) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
// dropped outside the list?
|
// dropped outside the list?
|
||||||
if (!result.destination) {
|
if (!result.destination) {
|
||||||
return;
|
return;
|
||||||
@ -105,7 +109,7 @@ export default function Categories() {
|
|||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditDialogOpen = (index) => {
|
const handleEditDialogOpen = (index:number) => {
|
||||||
setDialogName(categories[index].name);
|
setDialogName(categories[index].name);
|
||||||
setDialogDefault(categories[index].default);
|
setDialogDefault(categories[index].default);
|
||||||
setCategoryToEdit(index);
|
setCategoryToEdit(index);
|
||||||
@ -121,7 +125,7 @@ export default function Categories() {
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('name', dialogName);
|
formData.append('name', dialogName);
|
||||||
formData.append('default', dialogDefault);
|
formData.append('default', dialogDefault.toString());
|
||||||
|
|
||||||
if (categoryToEdit === -1) {
|
if (categoryToEdit === -1) {
|
||||||
client.post('/api/v1/category/', formData)
|
client.post('/api/v1/category/', formData)
|
||||||
@ -133,7 +137,7 @@ export default function Categories() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCategory = (index) => {
|
const deleteCategory = (index:number) => {
|
||||||
const category = categories[index];
|
const category = categories[index];
|
||||||
client.delete(`/api/v1/category/${category.id}`)
|
client.delete(`/api/v1/category/${category.id}`)
|
||||||
.finally(() => triggerUpdate());
|
.finally(() => triggerUpdate());
|
||||||
@ -153,8 +157,7 @@ export default function Categories() {
|
|||||||
>
|
>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
ContainerComponent="li"
|
ContainerProps={{ ref: provided.innerRef } as any}
|
||||||
ContainerProps={{ ref: provided.innerRef }}
|
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
style={getItemStyle(
|
style={getItemStyle(
|
16
webUI/react/src/typings.d.ts
vendored
16
webUI/react/src/typings.d.ts
vendored
@ -67,6 +67,7 @@ interface IChapter {
|
|||||||
index: number
|
index: number
|
||||||
chapterCount: number
|
chapterCount: number
|
||||||
pageCount: number
|
pageCount: number
|
||||||
|
downloaded: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEpisode {
|
interface IEpisode {
|
||||||
@ -99,7 +100,7 @@ interface IPartialEpisode {
|
|||||||
interface ICategory {
|
interface ICategory {
|
||||||
id: number
|
id: number
|
||||||
order: number
|
order: number
|
||||||
name: String
|
name: string
|
||||||
default: boolean
|
default: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,3 +154,16 @@ interface IAbout {
|
|||||||
github: string
|
github: string
|
||||||
discord: 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[]
|
||||||
|
}
|
||||||
|
@ -1891,6 +1891,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
||||||
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
|
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
|
||||||
|
|
||||||
|
"@types/react-beautiful-dnd@^13.0.0":
|
||||||
|
version "13.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4"
|
||||||
|
integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-dom@^17.0.2":
|
"@types/react-dom@^17.0.2":
|
||||||
version "17.0.5"
|
version "17.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.5.tgz#df44eed5b8d9e0b13bb0cd38e0ea6572a1231227"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.5.tgz#df44eed5b8d9e0b13bb0cd38e0ea6572a1231227"
|
||||||
|
Loading…
Reference in New Issue
Block a user