Use background service for downloads and updates (fixes #36)

This commit is contained in:
Rajarshee Chatterjee
2021-06-19 13:47:07 +05:30
parent 8e1a9274e5
commit edcc4c0bcd
17 changed files with 352 additions and 65 deletions
+2
View File
@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
@@ -32,5 +33,6 @@
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
</application>
</manifest>
+13
View File
@@ -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",
+1
View File
@@ -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",
+14 -10
View File
@@ -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 && (
<List.Icon
color={theme.colorAccent}
icon={icon}
style={{ marginVertical: 0 }}
/>
)
}
left={() => (
<View style={{ justifyContent: "center" }}>
{icon && (
<List.Icon
color={theme.colorAccent}
icon={icon}
style={{
marginVertical: 0,
}}
/>
)}
</View>
)}
right={() =>
right && (
<MaterialCommunityIcons
+2
View File
@@ -11,6 +11,7 @@ import ReaderSettings from "../screens/More/ReaderSettings";
import BackupSettings from "../screens/More/BackupSettings";
import AdvancedSettings from "../screens/More/AdvancedSettings";
import GeneralSettings from "../screens/More/GeneralSettings";
import DownloadQueue from "../screens/More/DownloadQueue";
const Stack = createStackNavigator();
@@ -56,6 +57,7 @@ const MoreStack = () => {
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
</Stack.Navigator>
</View>
);
+2
View File
@@ -0,0 +1,2 @@
export const SET_DOWNLOAD_QUEUE = "SET_DOWNLOAD_QUEUE";
export const CANCEL_DOWNLOAD = "CANCEL_DOWNLOAD";
+8
View File
@@ -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 });
};
+35
View File
@@ -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;
+99 -26
View File
@@ -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);
}
+2
View File
@@ -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,
})
);
@@ -37,7 +37,7 @@ const ChapterAppbar = ({
style={{ backgroundColor: "transparent", elevation: 0 }}
>
<Appbar.BackAction
onPress={() => navigation.goBack()}
onPress={navigation.goBack}
color="#FFFFFF"
size={26}
style={{ marginRight: 0 }}
+76
View File
@@ -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 (
<ScreenContainer theme={theme}>
<Appbar title="Download queue" onBackAction={navigation.goBack} />
<FlatList
contentContainerStyle={{ flexGrow: 1 }}
keyExtractor={(item) => item.chapterId.toString()}
data={downloadQueue}
renderItem={({ item }) => (
<View style={{ padding: 16 }}>
<Text style={{ color: theme.textColorPrimary }}>
{item.chapterName}
</Text>
<ProgressBar
indeterminate={true}
color={theme.colorAccent}
style={{ marginTop: 8 }}
/>
</View>
)}
ListEmptyComponent={
<EmptyView icon="(・o・;)" description="No downloads" />
}
/>
{downloadQueue.length > 0 && (
<FAB
style={[styles.fab, { backgroundColor: theme.colorAccent }]}
color={theme.textColorPrimary}
label="Cancel"
uppercase={false}
small
icon="close"
onPress={() => dispatch(cancelDownload())}
/>
)}
</ScreenContainer>
);
};
export default DownloadQueue;
const styles = StyleSheet.create({
fab: {
position: "absolute",
margin: 16,
right: 0,
bottom: 0,
},
});
+18 -1
View File
@@ -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 }) => {
</View>
</View>
<ListSection>
<ListItem
title="Download queue"
description={
downloadQueue.length > 0 &&
downloadQueue.length + " remaining"
}
icon="download-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "DownloadQueue",
})
}
theme={theme}
/>
<Divider theme={theme} />
<ListItem
title="Settings"
icon="cog-outline"
+3 -1
View File
@@ -10,7 +10,7 @@ import {
} from "react-native";
import { Provider, Portal, Appbar, IconButton, Menu } from "react-native-paper";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import {
bookmarkChapterAction,
@@ -46,6 +46,7 @@ const Novel = ({ route, navigation }) => {
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}
/>
);
+4 -1
View File
@@ -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 (
<ActivityIndicator
color={theme.textColorHint}
+60 -23
View File
@@ -5,6 +5,8 @@ import { getLibrary } from "../database/queries/LibraryQueries";
import { fetchChapters, fetchNovel } from "../services/Source/source";
import * as Notifications from "expo-notifications";
import BackgroundService from "react-native-background-actions";
Notifications.setNotificationHandler({
handleNotification: async () => {
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) => {
+12 -2
View File
@@ -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;