From 08980982e23a25feb365df6bba25645c16506c91 Mon Sep 17 00:00:00 2001 From: Aria Moradi Date: Tue, 28 Sep 2021 00:41:12 +0330 Subject: [PATCH] add support for Updates --- src/App.tsx | 4 + src/components/TemporaryDrawer.tsx | 28 ++-- src/screens/manga/Updates.tsx | 212 +++++++++++++++++++++++++++++ src/typings.d.ts | 6 + 4 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 src/screens/manga/Updates.tsx diff --git a/src/App.tsx b/src/App.tsx index a808b0f5..8441e0a9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,6 +37,7 @@ 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 Updates from 'screens/manga/Updates'; import AnimeExtensions from 'screens/anime/AnimeExtensions'; import DownloadQueue from 'screens/manga/DownloadQueue'; @@ -152,6 +153,9 @@ export default function App() { + + + {/* Anime Routes */} diff --git a/src/components/TemporaryDrawer.tsx b/src/components/TemporaryDrawer.tsx index beaf68f6..acdb8856 100644 --- a/src/components/TemporaryDrawer.tsx +++ b/src/components/TemporaryDrawer.tsx @@ -17,6 +17,7 @@ import VideoLibraryOutlinedIcon from '@mui/icons-material/VideoLibraryOutlined'; import ExploreIcon from '@mui/icons-material/Explore'; import ExtensionIcon from '@mui/icons-material/Extension'; import GetAppIcon from '@mui/icons-material/GetApp'; +import NewReleasesIcon from '@mui/icons-material/NewReleases'; import ListItemText from '@mui/material/ListItemText'; import SettingsIcon from '@mui/icons-material/Settings'; import { Link } from 'react-router-dom'; @@ -64,14 +65,25 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) { {workingMode === 'manga' && ( - - - - - - - - + <> + + + + + + + + + + + + + + + + + + )} diff --git a/src/screens/manga/Updates.tsx b/src/screens/manga/Updates.tsx new file mode 100644 index 00000000..3d850a85 --- /dev/null +++ b/src/screens/manga/Updates.tsx @@ -0,0 +1,212 @@ +/* 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, { useContext, useEffect, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import makeStyles from '@mui/styles/makeStyles'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import IconButton from '@mui/material/IconButton'; +import DownloadIcon from '@mui/icons-material/Download'; +import Avatar from '@mui/material/Avatar'; +import Typography from '@mui/material/Typography'; +import NavbarContext from 'context/NavbarContext'; +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, + imageRendering: 'pixelated', + }, + card: { + margin: '10px', + '&:hover': { + backgroundColor: theme.palette.action.hover, + transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', + }, + '&:active': { + backgroundColor: theme.palette.action.selected, + transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', + }, + }, +})); + +function epochToDate(epoch: number) { + const date = new Date(0); // The 0 there is the key, which sets the date to the epoch + date.setUTCSeconds(epoch); + return date; +} + +function isTheSameDay(first:Date, second:Date) { + return first.getDate() === second.getDate() + && first.getMonth() === second.getMonth() + && first.getFullYear() === second.getFullYear(); +} + +function getDateString(date: Date) { + const today = new Date(); + if (isTheSameDay(today, date)) return 'TODAY'; + // calculate yesterday + const yesterday = new Date(); + yesterday.setDate(today.getDate() - 1); + if (isTheSameDay(yesterday, date)) return 'YESTERDAY'; + return date.toLocaleDateString(); +} + +function groupByDate(updates: IMangaChapter[]): [string, IMangaChapter[]][] { + if (updates.length === 0) return []; + + const groups = {}; + updates.forEach((item) => { + const key = getDateString(epochToDate(item.chapter.fetchedAt)); + // @ts-ignore + if (groups[key] === undefined) groups[key] = []; + // @ts-ignore + groups[key].push(item); + }); + + // @ts-ignore + return Object.keys(groups).map((key) => [key, groups[key]]); +} + +const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws'); +const initialQueue = { + status: 'Stopped', + queue: [], +} as IQueue; + +export default function Updates() { + const classes = useStyles(); + const history = useHistory(); + + const { setTitle, setAction } = useContext(NavbarContext); + const [updateEntries, setUpdateEntries] = useState([]); + + const [serverAddress] = useLocalStorage('serverBaseURL', ''); + const [useCache] = useLocalStorage('useCache', true); + + const [, setWsClient] = useState(); + const [{ queue }, setQueueState] = useState(initialQueue); + + useEffect(() => { + const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`); + wsc.onmessage = (e) => { + const data = JSON.parse(e.data) as IQueue; + setQueueState(data); + }; + + setWsClient(wsc); + + return () => wsc.close(); + }, []); + + useEffect(() => { + setTitle('Updates'); + + setAction(<>); + }, []); + + useEffect(() => { + client.get('/api/v1/update/recentChapters') + .then((response) => response.data) + .then((updates: IMangaChapter[]) => { + setUpdateEntries(updates); + }); + }, []); + + const downloadStatusStringFor = (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).toFixed(2)}%)`; + } + }); + return rtn; + }; + + const downloadChapter = (chapter: IChapter) => { + client.get(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`); + }; + + return ( + <> + {groupByDate(updateEntries).map((dateGroup) => ( +
+

+ {dateGroup[0]} +

+ {dateGroup[1].map(({ chapter, manga }) => ( + history.push(`/manga/${chapter.mangaId}/chapter/${chapter.index}`)} + > + +
+ +
+ + {manga.title} + + + {chapter.name} + {downloadStatusStringFor(chapter)} + +
+
+ {downloadStatusStringFor(chapter) === '' + && ( + { + downloadChapter(chapter); + // prevent parent tags from getting the event + e.stopPropagation(); + }} + size="large" + > + + + )} +
+
+ ))} +
+ ))} + + ); +} diff --git a/src/typings.d.ts b/src/typings.d.ts index ba9164d8..390ddd11 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -91,11 +91,17 @@ interface IChapter { bookmarked: boolean lastPageRead: number index: number + fetchedAt: number chapterCount: number pageCount: number downloaded: boolean } +interface IMangaChapter { + manga: IManga + chapter: IChapter +} + interface Video{ url: string quality: string