Use redux for novel details and chapters

This commit is contained in:
Rajarshee Chatterjee
2021-03-21 09:18:49 +05:30
parent 83afe9c254
commit 625cdff212
16 changed files with 327 additions and 134 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ import Router from "./src/navigation/Router";
import { createTables, createIndexes } from "./src/services/db";
import { setStatusBar } from "./src/services/asyncStorage";
// Redyx
// Redux
import { Provider } from "react-redux";
import { persistor, store } from "./src/redux/store/configureStore";
import { PersistGate } from "redux-persist/lib/integration/react";
+5
View File
@@ -7270,6 +7270,11 @@
}
}
},
"redux-devtools-extension": {
"version": "2.13.9",
"resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz",
"integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A=="
},
"redux-persist": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
+1
View File
@@ -40,6 +40,7 @@
"react-native-webview": "11.0.0",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4"
-27
View File
@@ -1,27 +0,0 @@
import React from "react";
import { TextInput, StyleSheet } from "react-native";
import { theme } from "../../theme/theme";
const SearchBar = ({ searchText, onChangeText, onSubmitEditing }) => (
<TextInput
autoFocus
placeholder="Search..."
defaultValue={searchText}
style={styles.searchBar}
placeholderTextColor={theme.textColorHintDark}
blurOnSubmit={true}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
/>
);
export default SearchBar;
const styles = StyleSheet.create({
searchBar: {
flex: 1,
fontSize: 18,
color: "white",
marginLeft: 15,
},
});
+57
View File
@@ -0,0 +1,57 @@
import {
LOADING_CHAPTERS,
GET_CHAPTERS,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
CHAPTER_DOWNLOADING,
} from "./types";
import {
deleteChapterFromDb,
downloadChapterFromSource,
} from "../../services/db";
import { ToastAndroid } from "react-native";
export const downloadChapter = (
extensionId,
novelUrl,
chapterUrl,
chapterName
) => async (dispatch) => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: {
extensionId,
novelUrl,
chapterUrl,
downloading: true,
downloaded: false,
},
});
await downloadChapterFromSource(extensionId, novelUrl, chapterUrl);
dispatch({
type: CHAPTER_DOWNLOADED,
payload: { extensionId, novelUrl, chapterUrl },
});
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
export const deleteChapter = (
extensionId,
novelUrl,
chapterUrl,
chapterName
) => (dispatch) => {
deleteChapterFromDb(extensionId, novelUrl, chapterUrl);
dispatch({
type: CHAPTER_DELETED,
payload: { extensionId, novelUrl, chapterUrl },
});
ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
};
+52
View File
@@ -0,0 +1,52 @@
import { LOADING_NOVEL, GET_NOVEL, GET_CHAPTERS } from "./types";
import {
checkNovelInDb,
getChaptersFromDb,
getNovelInfoFromDb,
insertChaptersInDb,
insertNovelInfoInDb,
} from "../../services/db";
import {
fetchChaptersFromSource,
fetchNovelFromSource,
} from "../../services/api";
export const getNovel = (navigatingFrom, extensionId, novelUrl) => async (
dispatch
) => {
dispatch({ type: LOADING_NOVEL });
if (navigatingFrom === 1) {
const chapters = await getChaptersFromDb(novelUrl, "", "");
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
const inCache = await checkNovelInDb(novelUrl);
if (inCache) {
const novel = await getNovelInfoFromDb(novelUrl);
const chapters = await getChaptersFromDb(novelUrl, "", "");
dispatch({
type: GET_NOVEL,
payload: { novel, chapters },
});
} else {
const novel = await fetchNovelFromSource(extensionId, novelUrl);
const chapters = await fetchChaptersFromSource(
extensionId,
novelUrl
);
dispatch({
type: GET_NOVEL,
payload: { novel, chapters },
});
insertNovelInfoInDb(novel);
insertChaptersInDb(novelUrl, chapters);
}
}
};
+14
View File
@@ -7,3 +7,17 @@ export const SET_DISPLAY_MODE = "SET_DISPLAY_MODE";
export const SET_ITEMS_PER_ROW = "SET_ITEMS_PER_ROW";
export const GET_EXTENSIONS = "GET_EXTENSIONS";
export const LOADING_NOVEL = "LOADING_NOVEL";
export const LOADING_CHAPTERS = "LOADING_CHAPTERS";
export const GET_NOVEL = "GET_NOVEL";
export const GET_CHAPTERS = "GET_CHAPTERS";
export const CHAPTER_DOWNLOADED = "CHAPTER_DOWNLOADED";
export const CHAPTER_DELETED = "CHAPTER_DELETED";
export const CHAPTER_DOWNLOADING = "CHAPTER_DOWNLOADING";
+41
View File
@@ -0,0 +1,41 @@
import {
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
} from "../actions/types";
const initialState = {
chapters: [],
loading: true,
};
const downloadsReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case CHAPTER_DOWNLOADING:
return { ...state, downloads: [...state.downloads, payload] };
case CHAPTER_DOWNLOADED:
return {
...state,
downloads: state.downloads.map((chapter) =>
chapter.chapterUrl === payload.chapterUrl &&
chapter.extensionId === payload.extensionId
? { ...chapter, downloading: false, downloaded: true }
: chapter
),
};
case CHAPTER_DELETED:
return {
downloads: state.downloads.filter(
(chapter) =>
chapter.chapterUrl === payload.chapterUrl &&
chapter.extensionId === payload.extensionId
),
};
default:
return state;
}
};
export default downloadsReducer;
+60
View File
@@ -0,0 +1,60 @@
import {
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
GET_CHAPTERS,
LOADING_NOVEL,
GET_NOVEL,
} from "../actions/types";
const initialState = {
novel: null,
chapters: [],
loading: true,
};
const novelReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case LOADING_NOVEL:
return { ...state, loading: true };
case GET_NOVEL:
return {
...state,
novel: payload.novel,
chapters: payload.chapters,
loading: false,
};
case GET_CHAPTERS:
return {
...state,
chapters: payload,
loading: false,
};
case CHAPTER_DOWNLOADING:
return { ...state, downloads: [...state.downloads, payload] };
case CHAPTER_DOWNLOADED:
return {
...state,
downloads: state.downloads.map((chapter) =>
chapter.chapterUrl === payload.chapterUrl &&
chapter.extensionId === payload.extensionId
? { ...chapter, downloading: false, downloaded: true }
: chapter
),
};
case CHAPTER_DELETED:
return {
downloads: state.downloads.filter(
(chapter) =>
chapter.chapterUrl === payload.chapterUrl &&
chapter.extensionId === payload.extensionId
),
};
default:
return state;
}
};
export default novelReducer;
+3
View File
@@ -7,10 +7,12 @@ import themeReducer from "../reducers/theme";
import settingsReducer from "../reducers/settings";
import libraryReducer from "../reducers/library";
import extensionReducer from "../reducers/extension";
import novelReducer from "../reducers/novel";
const persistConfig = {
key: "root",
storage: AsyncStorage,
blacklist: ["novelReducer"],
};
const persistedReducer = persistReducer(
@@ -20,6 +22,7 @@ const persistedReducer = persistReducer(
settingsReducer,
libraryReducer,
extensionReducer,
novelReducer,
})
);
+1
View File
@@ -57,6 +57,7 @@ const History = ({ navigation }) => {
]}
>
<FlatList
contentContainerStyle={{ flex: 1 }}
data={history}
keyExtractor={(item) => item.historyId.toString()}
renderItem={renderHistoryCard}
+1
View File
@@ -78,6 +78,7 @@ const LibraryScreen = ({
/>
) : (
<FlatList
contentContainerStyle={{ flex: 1 }}
numColumns={3}
data={novels}
keyExtractor={(item) => item.novelUrl}
+28 -94
View File
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useRef } from "react";
import React, { useState, useCallback, useRef, useEffect } from "react";
import { useFocusEffect } from "@react-navigation/native";
import {
StyleSheet,
@@ -13,25 +13,22 @@ import ChapterCard from "./components/ChapterCard";
import NovelInfoHeader from "./components/NovelHeader";
import { BottomSheet } from "./components/BottomSheet";
import {
fetchNovelFromSource,
fetchChaptersFromSource,
} from "../../services/api";
import {
insertNovelInfoInDb,
insertChaptersInDb,
getChaptersFromDb,
checkNovelInDb,
getNovelInfoFromDb,
toggleFavourite,
downloadOrDeleteChapter,
} from "../../services/db";
import { toggleFavourite, downloadOrDeleteChapter } from "../../services/db";
import { useSelector } from "react-redux";
import { connect } from "react-redux";
const NovelItem = ({ route, navigation }) => {
import { getNovel } from "../../redux/actions/novel";
const Novel = ({
route,
navigation,
theme,
novel,
chapters,
loading,
getNovel,
}) => {
const item = route.params;
const theme = useSelector((state) => state.themeReducer.theme);
const {
extensionId,
@@ -41,11 +38,7 @@ const NovelItem = ({ route, navigation }) => {
novelCover,
} = route.params;
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [novel, setNovel] = useState(item);
const [chapters, setChapters] = useState();
// const [refreshing, setRefreshing] = useState(false);
const [libraryStatus, setlibraryStatus] = useState(item.libraryStatus);
@@ -59,50 +52,6 @@ const NovelItem = ({ route, navigation }) => {
let _panel = useRef(null); // Bottomsheet ref
/**
* Fetch novel and chapters from source and set loading false
*/
const getNovelFromSource = async (extensionId, novelUrl) => {
await fetchNovelFromSource(extensionId, novelUrl).then((response) => {
setNovel(response);
insertNovelInfoInDb(response);
});
await fetchChaptersFromSource(extensionId, novelUrl).then(
(response) => {
setChapters(response);
insertChaptersInDb(novelUrl, response);
}
);
setlibraryStatus(0);
setLoading(false);
setRefreshing(false);
};
/**
* Get chapters from db and set loading false
*/
const getChapters = async (novelUrl) => {
await getChaptersFromDb(novelUrl, filter, sort).then((response) =>
setChapters(response)
);
setLoading(false);
setRefreshing(false);
};
/**
* Get novel and chapters from database
*/
const getNovelFromDb = async () => {
await getNovelInfoFromDb(novelUrl).then((res) => {
setNovel(res);
setlibraryStatus(res.libraryStatus);
});
getChapters(novelUrl);
};
/**
* Insert or remove novel from library
*/
@@ -147,36 +96,15 @@ const NovelItem = ({ route, navigation }) => {
});
};
/**
* Check if the novel is in db. If not, fetch from source
*
* If navigating from library then directly get chapters from db
*/
const checkIfExistsInDb = () => {
if (navigatingFrom === 1) {
getChapters(novelUrl);
} else {
setRefreshing(true);
checkNovelInDb(novelUrl).then((res) => {
if (res) {
getNovelFromDb(novelUrl);
} else {
getNovelFromSource(extensionId, novelUrl);
}
});
}
};
useFocusEffect(
useCallback(() => {
checkIfExistsInDb();
getNovel(navigatingFrom, extensionId, novelUrl);
}, [sort, filter])
);
const onRefresh = async () => {
setRefreshing(true);
checkIfExistsInDb();
// setRefreshing(true);
// checkIfExistsInDb();
};
const renderChapterCard = ({ item }) => (
@@ -215,7 +143,7 @@ const NovelItem = ({ route, navigation }) => {
novelName: novelName,
}}
novel={novel}
noOfChapters={!loading && chapters.length}
noOfChapters={chapters.length}
libraryStatus={libraryStatus}
insertNovelInLib={insertNovelInLib}
navigatingFrom={navigatingFrom}
@@ -225,8 +153,8 @@ const NovelItem = ({ route, navigation }) => {
)}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
refreshing={loading}
// onRefresh={onRefresh}
colors={[theme.textColorPrimary]}
progressBackgroundColor={theme.colorPrimary}
/>
@@ -245,8 +173,14 @@ const NovelItem = ({ route, navigation }) => {
</Provider>
);
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
novel: state.novelReducer.novel,
chapters: state.novelReducer.chapters,
loading: state.novelReducer.loading,
});
export default NovelItem;
export default connect(mapStateToProps, { getNovel })(Novel);
const styles = StyleSheet.create({
container: {
+12 -11
View File
@@ -1,6 +1,5 @@
import React, { useState } from "react";
import { View, Text, ActivityIndicator } from "react-native";
import { View, Text, ActivityIndicator, StyleSheet } from "react-native";
import { TouchableRipple, IconButton } from "react-native-paper";
import { useSelector } from "react-redux";
@@ -19,15 +18,7 @@ const ChapterCard = ({
return (
<TouchableRipple
style={{
paddingHorizontal: 15,
paddingVertical: 7,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
// borderTopColor: "rgba(255,255,255,.12)",
// borderTopWidth: 1,
}}
style={styles.chapterCardContainer}
onPress={() =>
navigation.navigate("ChapterItem", {
chapterUrl: chapter.chapterUrl,
@@ -113,3 +104,13 @@ const ChapterCard = ({
};
export default ChapterCard;
const styles = StyleSheet.create({
chapterCardContainer: {
paddingHorizontal: 15,
paddingVertical: 7,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
});
+1 -1
View File
@@ -303,7 +303,7 @@ const NovelInfoHeader = ({
fontWeight: "bold",
}}
>
{noOfChapters + " Chapters "}
{noOfChapters + " Chapters"}
</Text>
<IconButton
icon="filter-variant"
+50
View File
@@ -373,3 +373,53 @@ export const deleteDatabase = () => {
);
});
};
export const downloadChapterFromSource = async (
extensionId,
novelUrl,
chapterUrl
) => {
const downloadUrl = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
const response = await fetch(downloadUrl);
const chapter = await response.json();
db.transaction((tx) => {
tx.executeSql(
`UPDATE ChapterTable SET downloaded = 1 WHERE chapterUrl = ?`,
[chapterUrl]
);
tx.executeSql(
`INSERT INTO DownloadsTable (chapterUrl, novelUrl, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?, ?)`,
[
chapterUrl,
novelUrl,
chapter.chapterName,
chapter.chapterText,
chapter.prevChapter,
chapter.nextChapter,
],
(tx, res) => console.log(`Downloaded Chapter ${chapterUrl}`),
(txObj, error) => console.log("Error ", error)
);
});
};
export const deleteChapterFromDb = (extensionId, novelUrl, chapterUrl) => {
const updateIsDownloadedQuery = `UPDATE ChapterTable SET downloaded = 0 WHERE chapterUrl = ? AND novelUrl = ? AND extensionId = ?`;
const deleteChapterQuery = `DELETE FROM DownloadsTable WHERE chapterUrl = ? AND novelUrl = ? AND extensionId = ?`;
db.transaction((tx) => {
tx.executeSql(updateIsDownloadedQuery, [
chapterUrl,
novelUrl,
extensionId,
]);
tx.executeSql(
deleteChapterQuery,
[chapterUrl, novelUrl, extensionId],
(tx, res) => console.log(`Chapter deleted`),
(txObj, error) => console.log("Error ", error)
);
});
};