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;