diff --git a/server/src/main/kotlin/suwayomi/tachidesk/impl/download/DownloadManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/impl/download/DownloadManager.kt index f7eda6a..d415286 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/impl/download/DownloadManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/impl/download/DownloadManager.kt @@ -83,6 +83,7 @@ object DownloadManager { ) ) ) + start() } notifyAllClients() } @@ -93,10 +94,14 @@ object DownloadManager { } fun start() { + if (downloader != null && !downloader?.isAlive!!) // doesn't exist or is dead + downloader = null + if (downloader == null) { downloader = Downloader(downloadQueue) { notifyAllClients() } downloader!!.start() } + notifyAllClients() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/impl/download/Downloader.kt b/server/src/main/kotlin/suwayomi/tachidesk/impl/download/Downloader.kt index bbd393c..07e192e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/impl/download/Downloader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/impl/download/Downloader.kt @@ -35,7 +35,10 @@ class Downloader(private val downloadQueue: CopyOnWriteArrayList('Tachidesk'); @@ -125,6 +126,9 @@ export default function App() { + + + <> diff --git a/webUI/react/src/components/TemporaryDrawer.tsx b/webUI/react/src/components/TemporaryDrawer.tsx index 6701e6f..cab3bd3 100644 --- a/webUI/react/src/components/TemporaryDrawer.tsx +++ b/webUI/react/src/components/TemporaryDrawer.tsx @@ -14,6 +14,7 @@ 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'; @@ -87,6 +88,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) { + + + + + + + + diff --git a/webUI/react/src/components/manga/ChapterCard.tsx b/webUI/react/src/components/manga/ChapterCard.tsx index ccf4c0b..949fb60 100644 --- a/webUI/react/src/components/manga/ChapterCard.tsx +++ b/webUI/react/src/components/manga/ChapterCard.tsx @@ -47,12 +47,13 @@ const useStyles = makeStyles((theme) => ({ interface IProps{ chapter: IChapter triggerChaptersUpdate: () => void + downloadingString: string } export default function ChapterCard(props: IProps) { const classes = useStyles(); const theme = useTheme(); - const { chapter, triggerChaptersUpdate } = props; + const { chapter, triggerChaptersUpdate, downloadingString } = props; const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10); @@ -75,6 +76,11 @@ export default function ChapterCard(props: IProps) { .then(() => triggerChaptersUpdate()); }; + const downloadChapter = () => { + client.get(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`); + handleClose(); + }; + const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0'; return ( <> @@ -101,6 +107,7 @@ export default function ChapterCard(props: IProps) { {chapter.scanlator} {chapter.scanlator && ' '} {dateStr} + {downloadingString} @@ -115,7 +122,8 @@ export default function ChapterCard(props: IProps) { open={Boolean(anchorEl)} onClose={handleClose} > - {/* Download */} + {downloadingString.length === 0 + && Download } sendChange('bookmarked', !chapter.bookmarked)}> {chapter.bookmarked && 'Remove bookmark'} {!chapter.bookmarked && 'Bookmark'} diff --git a/webUI/react/src/components/manga/MangaDetails.tsx b/webUI/react/src/components/manga/MangaDetails.tsx index fbfd0fc..abe05d9 100644 --- a/webUI/react/src/components/manga/MangaDetails.tsx +++ b/webUI/react/src/components/manga/MangaDetails.tsx @@ -198,7 +198,7 @@ export default function MangaDetails(props: IProps) {
- Manga Thumbnail + Manga Thumbnail

diff --git a/webUI/react/src/screens/manga/DownloadQueue.tsx b/webUI/react/src/screens/manga/DownloadQueue.tsx new file mode 100644 index 0000000..1c7c7f2 --- /dev/null +++ b/webUI/react/src/screens/manga/DownloadQueue.tsx @@ -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(); + const [queueState, setQueueState] = useState(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 ( + + + + ); + } + return ( + + + + ); + }); + }, [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 ( + <> + + + {(provided) => ( + + {queue.map((item, index) => ( + + {(provided, snapshot) => ( + + + + + state: ${item.state}` + } + /> + {/* { + handleEditDialogOpen(index); + }} + > + + + { + deleteCategory(index); + }} + > + + */} + + )} + + ))} + {provided.placeholder} + + )} + + + + ); +} diff --git a/webUI/react/src/screens/manga/Manga.tsx b/webUI/react/src/screens/manga/Manga.tsx index 0bfa10d..cbd4c1a 100644 --- a/webUI/react/src/screens/manga/Manga.tsx +++ b/webUI/react/src/screens/manga/Manga.tsx @@ -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() { const classes = useStyles(); @@ -55,10 +61,48 @@ export default function Manga() { const [noChaptersFound, setNoChaptersFound] = useState(false); const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0); + const [, setWsClient] = useState(); + const [{ queue }, setQueueState] = useState(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}`) @@ -105,6 +149,7 @@ export default function Manga() { itemContent={(index:number) => ( )} diff --git a/webUI/react/src/screens/settings/Categories.jsx b/webUI/react/src/screens/settings/Categories.tsx similarity index 87% rename from webUI/react/src/screens/settings/Categories.jsx rename to webUI/react/src/screens/settings/Categories.tsx index 0d61986..90c0c50 100644 --- a/webUI/react/src/screens/settings/Categories.jsx +++ b/webUI/react/src/screens/settings/Categories.tsx @@ -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 * @@ -5,9 +8,6 @@ * 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/. */ -/* 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 { List, @@ -16,7 +16,9 @@ import { ListItemIcon, IconButton, } 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 EditIcon from '@material-ui/icons/Edit'; 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 Checkbox from '@material-ui/core/Checkbox'; import FormControlLabel from '@material-ui/core/FormControlLabel'; -import NavbarContext from '../../context/NavbarContext'; -import client from '../../util/client'; +import NavbarContext from 'context/NavbarContext'; +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 ...draggableStyle, @@ -47,14 +51,14 @@ export default function Categories() { const { setTitle, setAction } = useContext(NavbarContext); useEffect(() => { setTitle('Categories'); setAction(<>); }, []); - const [categories, setCategories] = useState([]); - const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category - const [dialogOpen, setDialogOpen] = useState(false); - const [dialogName, setDialogName] = useState(''); - const [dialogDefault, setDialogDefault] = useState(false); + const [categories, setCategories] = useState([]); + const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category + const [dialogOpen, setDialogOpen] = useState(false); + const [dialogName, setDialogName] = useState(''); + const [dialogDefault, setDialogDefault] = useState(false); const theme = useTheme(); - const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack + const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack useEffect(() => { @@ -65,12 +69,12 @@ export default function Categories() { } }, [updateTriggerHolder]); - const categoryReorder = (list, from, to) => { + 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); + formData.append('from', `${from + 1}`); + formData.append('to', `${to + 1}`); client.post(`/api/v1/category/${category.id}/reorder`, formData) .finally(() => triggerUpdate()); @@ -81,7 +85,7 @@ export default function Categories() { return result; }; - const onDragEnd = (result) => { + const onDragEnd = (result: DropResult) => { // dropped outside the list? if (!result.destination) { return; @@ -105,7 +109,7 @@ export default function Categories() { setDialogOpen(true); }; - const handleEditDialogOpen = (index) => { + const handleEditDialogOpen = (index:number) => { setDialogName(categories[index].name); setDialogDefault(categories[index].default); setCategoryToEdit(index); @@ -121,7 +125,7 @@ export default function Categories() { const formData = new FormData(); formData.append('name', dialogName); - formData.append('default', dialogDefault); + formData.append('default', dialogDefault.toString()); if (categoryToEdit === -1) { 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]; client.delete(`/api/v1/category/${category.id}`) .finally(() => triggerUpdate()); @@ -153,8 +157,7 @@ export default function Categories() { > {(provided, snapshot) => (