From edcc4c0bcd7db8e11bfe1d574d92fecb942eeb50 Mon Sep 17 00:00:00 2001 From: Rajarshee Chatterjee Date: Sat, 19 Jun 2021 13:47:07 +0530 Subject: [PATCH] Use background service for downloads and updates (fixes #36) --- android/app/src/main/AndroidManifest.xml | 2 + package-lock.json | 13 ++ package.json | 1 + src/components/List.js | 24 ++-- src/navigators/More.js | 2 + src/redux/downloads/donwloads.types.js | 2 + src/redux/downloads/downloads.actions.js | 8 ++ src/redux/downloads/downloads.reducer.js | 35 +++++ src/redux/novel/novel.actions.js | 125 ++++++++++++++---- src/redux/store.js | 2 + .../Chapter/components/ChapterAppbar.js | 2 +- src/screens/More/DownloadQueue.js | 76 +++++++++++ src/screens/More/More.js | 19 ++- src/screens/Novel/Novel.js | 4 +- src/screens/Novel/components/ChapterItem.js | 5 +- src/services/updates.js | 83 ++++++++---- src/sources/en/comrademao.js | 14 +- 17 files changed, 352 insertions(+), 65 deletions(-) create mode 100644 src/redux/downloads/donwloads.types.js create mode 100644 src/redux/downloads/downloads.actions.js create mode 100644 src/redux/downloads/downloads.reducer.js create mode 100644 src/screens/More/DownloadQueue.js diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b4c7a7aa..86fe71d9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + @@ -32,5 +33,6 @@ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f1cc69f4..e10d6115 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3534,6 +3534,11 @@ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-1.0.5.tgz", "integrity": "sha1-+YNhBRexc3wLncZDvsqTiTwE3xg=" }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "exec-sh": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", @@ -6827,6 +6832,14 @@ } } }, + "react-native-background-actions": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/react-native-background-actions/-/react-native-background-actions-2.6.1.tgz", + "integrity": "sha512-4YQSEq7Y+qLKU9BxuzMG0DEIeZkvD54jTqA1IKai/w3sV+hc4cJNoSjZScRj1+663oWrEAZwLUk2I8P5QRvgiQ==", + "requires": { + "eventemitter3": "^4.0.7" + } + }, "react-native-cheerio": { "version": "1.0.0-rc.4", "resolved": "https://registry.npmjs.org/react-native-cheerio/-/react-native-cheerio-1.0.0-rc.4.tgz", diff --git a/package.json b/package.json index 0058c032..0318055e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "react": "16.13.1", "react-dom": "16.13.1", "react-native": "^0.64.2", + "react-native-background-actions": "^2.6.1", "react-native-cheerio": "^1.0.0-rc.4", "react-native-collapsible-header-views": "^1.1.2", "react-native-device-info": "^8.1.3", diff --git a/src/components/List.js b/src/components/List.js index 402b5409..fc5ac4e1 100644 --- a/src/components/List.js +++ b/src/components/List.js @@ -1,5 +1,5 @@ import React from "react"; -import { StyleSheet } from "react-native"; +import { StyleSheet, View } from "react-native"; import { List, Divider as PaperDivider } from "react-native-paper"; import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; @@ -34,15 +34,19 @@ const ListItem = ({ description={description} descriptionStyle={{ color: theme.textColorSecondary }} descriptionNumberOfLines={1} - left={() => - icon && ( - - ) - } + left={() => ( + + {icon && ( + + )} + + )} right={() => right && ( { + ); diff --git a/src/redux/downloads/donwloads.types.js b/src/redux/downloads/donwloads.types.js new file mode 100644 index 00000000..c505183b --- /dev/null +++ b/src/redux/downloads/donwloads.types.js @@ -0,0 +1,2 @@ +export const SET_DOWNLOAD_QUEUE = "SET_DOWNLOAD_QUEUE"; +export const CANCEL_DOWNLOAD = "CANCEL_DOWNLOAD"; diff --git a/src/redux/downloads/downloads.actions.js b/src/redux/downloads/downloads.actions.js new file mode 100644 index 00000000..89db78fd --- /dev/null +++ b/src/redux/downloads/downloads.actions.js @@ -0,0 +1,8 @@ +import { SET_DOWNLOAD_QUEUE, CANCEL_DOWNLOAD } from "./donwloads.types"; +import BackgroundService from "react-native-background-actions"; + +export const cancelDownload = () => async (dispatch) => { + await BackgroundService.stop(); + + dispatch({ type: CANCEL_DOWNLOAD }); +}; diff --git a/src/redux/downloads/downloads.reducer.js b/src/redux/downloads/downloads.reducer.js new file mode 100644 index 00000000..1000a328 --- /dev/null +++ b/src/redux/downloads/downloads.reducer.js @@ -0,0 +1,35 @@ +import { CHAPTER_DOWNLOADED } from "../novel/novel.types"; +import { CANCEL_DOWNLOAD, SET_DOWNLOAD_QUEUE } from "./donwloads.types"; + +const initialState = { + downloadQueue: [], +}; + +const downloadsReducer = (state = initialState, action) => { + const { type, payload } = action; + + switch (type) { + case SET_DOWNLOAD_QUEUE: + return { + ...state, + downloadQueue: [...state.downloadQueue, ...payload], + }; + + case CHAPTER_DOWNLOADED: + return { + ...state, + downloadQueue: state.downloadQueue.filter( + (chapter) => chapter.chapterId !== payload + ), + }; + case CANCEL_DOWNLOAD: + return { + ...state, + downloadQueue: [], + }; + default: + return state; + } +}; + +export default downloadsReducer; diff --git a/src/redux/novel/novel.actions.js b/src/redux/novel/novel.actions.js index f40947cd..0b465935 100644 --- a/src/redux/novel/novel.actions.js +++ b/src/redux/novel/novel.actions.js @@ -50,6 +50,22 @@ import { getLibrary } from "../../database/queries/LibraryQueries"; import { showToast } from "../../hooks/showToast"; import { store } from "../store"; +import * as Notifications from "expo-notifications"; + +Notifications.setNotificationHandler({ + handleNotification: async () => { + return { + shouldShowAlert: true, + shouldPlaySound: true, + shouldSetBadge: true, + }; + }, +}); + +import BackgroundService from "react-native-background-actions"; +import { setDownloadQueue } from "../downloads/downloads.actions"; +import { SET_DOWNLOAD_QUEUE } from "../downloads/donwloads.types"; + export const setNovel = (novel) => async (dispatch) => { dispatch({ type: SET_NOVEL, payload: novel }); }; @@ -267,9 +283,13 @@ export const markChapterUnreadAction = export const downloadChapterAction = (sourceId, novelUrl, chapterUrl, chapterName, chapterId) => async (dispatch) => { + // dispatch({ + // type: CHAPTER_DOWNLOADING, + // payload: chapterId, + // }); dispatch({ - type: CHAPTER_DOWNLOADING, - payload: chapterId, + type: SET_DOWNLOAD_QUEUE, + payload: [{ chapterId, chapterName }], }); await downloadChapter(sourceId, novelUrl, chapterUrl, chapterId); @@ -283,34 +303,87 @@ export const downloadChapterAction = }; export const downloadAllChaptersAction = - (sourceId, novelUrl, chapters) => async (dispatch) => { + (sourceId, novelUrl, chaps) => async (dispatch) => { try { - await chapters.map((chapter, index) => { - setTimeout(async () => { - dispatch({ - type: CHAPTER_DOWNLOADING, - payload: chapter.chapterId, - }); + let chapters = chaps.filter((chapter) => chapter.downloaded === 0); - if (!chapter.downloaded) { - await downloadChapter( - sourceId, - novelUrl, - chapter.chapterUrl, - chapter.chapterId - ); + dispatch({ type: SET_DOWNLOAD_QUEUE, payload: chapters }); + const options = { + taskName: "Library Update", + taskTitle: chapters[0].chapterName, + taskDesc: "0/" + chapters.length, + taskIcon: { + name: "notification_icon", + type: "drawable", + }, + color: "#00adb5", + parameters: { + delay: 1000, + }, + progressBar: { + max: chapters.length, + value: 0, + }, + }; + + const sleep = (time) => + new Promise((resolve) => setTimeout(() => resolve(), time)); + + const veryIntensiveTask = async (taskData) => { + await new Promise(async (resolve) => { + for ( + let i = 0; + BackgroundService.isRunning() && i < chapters.length; + i++ + ) { + if (BackgroundService.isRunning()) { + // dispatch({ + // type: CHAPTER_DOWNLOADING, + // payload: chapters[i].chapterId, + // }); + + if (!chapters[i].downloaded) { + await downloadChapter( + sourceId, + novelUrl, + chapters[i].chapterUrl, + chapters[i].chapterId + ); + } + + dispatch({ + type: CHAPTER_DOWNLOADED, + payload: chapters[i].chapterId, + }); + + await BackgroundService.updateNotification({ + taskTitle: chapters[i].chapterName, + taskDesc: i + 1 + "/" + chapters.length, + progressBar: { + max: chapters.length, + value: i + 1, + }, + }); + + if (i + 1 === chapters.length) { + resolve(); + await BackgroundService.stop(); + + Notifications.scheduleNotificationAsync({ + content: { + title: "Downloader", + body: "Download completed", + }, + trigger: null, + }); + } + await sleep(taskData.delay); + } } + }); + }; - dispatch({ - type: CHAPTER_DOWNLOADED, - payload: chapter.chapterId, - }); - - if (index + 1 === chapters.length) { - showToast(`Download completed `); - } - }, 1000 * index); - }); + await BackgroundService.start(veryIntensiveTask, options); } catch (error) { showToast(error.message); } diff --git a/src/redux/store.js b/src/redux/store.js index 6ffcb53d..9049344f 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -11,6 +11,7 @@ import historyReducer from "./history/history.reducer"; import updatesReducer from "./updates/updates.reducer"; import trackerReducer from "./tracker/tracker.reducer"; import preferenceReducer from "./preferences/preference.reducer"; +import downloadsReducer from "./downloads/downloads.reducer"; const persistConfig = { key: "root", @@ -29,6 +30,7 @@ const persistedReducer = persistReducer( updatesReducer, trackerReducer, preferenceReducer, + downloadsReducer, }) ); diff --git a/src/screens/Chapter/components/ChapterAppbar.js b/src/screens/Chapter/components/ChapterAppbar.js index 4821d2d5..7fac4393 100644 --- a/src/screens/Chapter/components/ChapterAppbar.js +++ b/src/screens/Chapter/components/ChapterAppbar.js @@ -37,7 +37,7 @@ const ChapterAppbar = ({ style={{ backgroundColor: "transparent", elevation: 0 }} > navigation.goBack()} + onPress={navigation.goBack} color="#FFFFFF" size={26} style={{ marginRight: 0 }} diff --git a/src/screens/More/DownloadQueue.js b/src/screens/More/DownloadQueue.js new file mode 100644 index 00000000..725f6647 --- /dev/null +++ b/src/screens/More/DownloadQueue.js @@ -0,0 +1,76 @@ +import React, { useEffect } from "react"; +import { FlatList, View, Text, StyleSheet } from "react-native"; +import { FAB, ProgressBar } from "react-native-paper"; +import { useDispatch, useSelector } from "react-redux"; + +import { Appbar } from "../../components/Appbar"; +import { ScreenContainer } from "../../components/Common"; +import EmptyView from "../../components/EmptyView"; + +import { useTheme } from "../../hooks/reduxHooks"; + +import { cancelDownload } from "../../redux/downloads/downloads.actions"; + +import BackgroundService from "react-native-background-actions"; +import { downloadAllChaptersAction } from "../../redux/novel/novel.actions"; + +const DownloadQueue = ({ navigation }) => { + const theme = useTheme(); + const { downloadQueue } = useSelector((state) => state.downloadsReducer); + + const dispatch = useDispatch(); + + useEffect(() => { + if (!BackgroundService.isRunning() && downloadQueue.length > 0) { + dispatch(cancelDownload()); + } + }, []); + + return ( + + + item.chapterId.toString()} + data={downloadQueue} + renderItem={({ item }) => ( + + + {item.chapterName} + + + + )} + ListEmptyComponent={ + + } + /> + {downloadQueue.length > 0 && ( + dispatch(cancelDownload())} + /> + )} + + ); +}; + +export default DownloadQueue; + +const styles = StyleSheet.create({ + fab: { + position: "absolute", + margin: 16, + right: 0, + bottom: 0, + }, +}); diff --git a/src/screens/More/More.js b/src/screens/More/More.js index 74b10872..0dd55a7c 100644 --- a/src/screens/More/More.js +++ b/src/screens/More/More.js @@ -1,13 +1,15 @@ import React from "react"; import { StyleSheet, View, Image } from "react-native"; +import { useSelector } from "react-redux"; import { Appbar } from "../../components/Appbar"; -import { ListItem, ListSection } from "../../components/List"; +import { Divider, ListItem, ListSection } from "../../components/List"; import { useTheme } from "../../hooks/reduxHooks"; const MoreScreen = ({ navigation }) => { const theme = useTheme(); + const { downloadQueue } = useSelector((state) => state.downloadsReducer); return ( <> @@ -34,6 +36,21 @@ const MoreScreen = ({ navigation }) => { + 0 && + downloadQueue.length + " remaining" + } + icon="download-outline" + onPress={() => + navigation.navigate("MoreStack", { + screen: "DownloadQueue", + }) + } + theme={theme} + /> + { const theme = useTheme(); const dispatch = useDispatch(); const { novel, chapters, loading, updating, downloading } = useNovel(); + const { downloadQueue } = useSelector((state) => state.downloadsReducer); const [selected, setSelected] = useState([]); const [downloadMenu, showDownloadMenu] = useState(false); @@ -103,6 +104,7 @@ const Novel = ({ route, navigation }) => { setSelected={setSelected} showChapterTitles={showChapterTitles} chapterActionsSheetRef={chapterActionsSheetRef} + downloadQueue={downloadQueue} /> ); diff --git a/src/screens/Novel/components/ChapterItem.js b/src/screens/Novel/components/ChapterItem.js index 54bc4675..36174a09 100644 --- a/src/screens/Novel/components/ChapterItem.js +++ b/src/screens/Novel/components/ChapterItem.js @@ -31,6 +31,7 @@ const ChapterItem = ({ setSelected, showChapterTitles, chapterActionsSheetRef, + downloadQueue, }) => { const [deleteChapterMenu, setDeleteChapterMenu] = useState(false); const showDeleteChapterMenu = () => setDeleteChapterMenu(true); @@ -49,7 +50,9 @@ const ChapterItem = ({ }); const displayDownloadButton = () => { - if (downloading.indexOf(chapter.chapterId) !== -1) { + if ( + downloadQueue.some((chap) => chap.chapterId === chapter.chapterId) + ) { return ( { return { @@ -113,32 +115,67 @@ export const updateNovelChapters = async (sourceId, novelUrl, novelId) => { export const updateAllNovels = async () => { const libraryNovels = await getLibrary(); - Notifications.scheduleNotificationAsync({ - identifier: "updatingNotif", - content: { title: "Updating library" }, - trigger: null, - }); + const options = { + taskName: "Library Update", + taskTitle: "Updating library", + taskDesc: "0/" + libraryNovels.length, + taskIcon: { + name: "notification_icon", + type: "drawable", + }, + color: "#00adb5", + parameters: { + delay: 1000, + }, + progressBar: { + max: libraryNovels.length, + value: 0, + }, + }; - libraryNovels.map((novel, index) => - setTimeout(async () => { - await updateNovelChapters( - novel.sourceId, - novel.novelUrl, - novel.novelId - ); - console.log(novel.novelName + " Updated"); + const sleep = (time) => + new Promise((resolve) => setTimeout(() => resolve(), time)); - if (index + 1 === libraryNovels.length) { - Notifications.scheduleNotificationAsync({ - content: { - title: "Library Updated", - body: libraryNovels.length + " novels updated", - }, - trigger: null, - }); + const veryIntensiveTask = async (taskData) => + await new Promise(async (resolve) => { + for ( + let i = 0; + BackgroundService.isRunning() && i < libraryNovels.length; + i++ + ) { + if (BackgroundService.isRunning()) { + await updateNovelChapters( + libraryNovels[i].sourceId, + libraryNovels[i].novelUrl, + libraryNovels[i].novelId + ); + console.log(libraryNovels[i].novelName + " Updated"); + await BackgroundService.updateNotification({ + taskTitle: libraryNovels[i].novelName, + taskDesc: i + 1 + "/" + libraryNovels.length, + progressBar: { + max: libraryNovels.length, + value: i + 1, + }, + }); + if (libraryNovels.length === i + 1) { + resolve(); + await BackgroundService.stop(); + Notifications.scheduleNotificationAsync({ + content: { + title: "Library Updated", + body: libraryNovels.length + " novels updated", + }, + trigger: null, + }); + } + + await sleep(taskData.delay); + } } - }, 3000 * index) - ); + }); + + await BackgroundService.start(veryIntensiveTask, options); }; export const parseChapterNumber = (chapterName) => { diff --git a/src/sources/en/comrademao.js b/src/sources/en/comrademao.js index 4fa9238f..d438c611 100644 --- a/src/sources/en/comrademao.js +++ b/src/sources/en/comrademao.js @@ -83,6 +83,17 @@ const parseNovelAndChapters = async (novelUrl) => { let novelChapters = []; + let chapterUrlPrefix = $("tbody > tr") + .first() + .next() + .find("td > a") + .text() + .split(" "); + + chapterUrlPrefix.pop(); + + chapterUrlPrefix = chapterUrlPrefix.join("-").toLowerCase(); + const latestChapter = $("tbody > tr") .first() .next() @@ -93,7 +104,7 @@ const parseNovelAndChapters = async (novelUrl) => { for (let i = 1; i <= latestChapter; i++) { const chapterName = "Chapter " + i; const releaseDate = null; - const chapterUrl = novelUrl.slice(0, -1) + "-chapter-" + i + "/"; + const chapterUrl = chapterUrlPrefix + "-" + i + "/"; const chapter = { chapterName, @@ -103,7 +114,6 @@ const parseNovelAndChapters = async (novelUrl) => { novelChapters.push(chapter); } - novel.chapters = novelChapters; return novel;