From 625cdff2128969594fa033e6f58f689c91bcc323 Mon Sep 17 00:00:00 2001 From: Rajarshee Chatterjee Date: Sun, 21 Mar 2021 09:18:49 +0530 Subject: [PATCH] Use redux for novel details and chapters --- App.js | 2 +- package-lock.json | 5 + package.json | 1 + src/components/common/SearchBar.js | 27 ----- src/redux/actions/chapters.js | 57 +++++++++ src/redux/actions/novel.js | 52 +++++++++ src/redux/actions/types.js | 14 +++ src/redux/reducers/chapters.js | 41 +++++++ src/redux/reducers/novel.js | 60 ++++++++++ src/redux/store/configureStore.js | 3 + src/screens/History/History.js | 1 + src/screens/Library/Library.js | 1 + src/screens/Novel/Novel.js | 122 +++++--------------- src/screens/Novel/components/ChapterCard.js | 23 ++-- src/screens/Novel/components/NovelHeader.js | 2 +- src/services/db.js | 50 ++++++++ 16 files changed, 327 insertions(+), 134 deletions(-) delete mode 100644 src/components/common/SearchBar.js create mode 100644 src/redux/actions/chapters.js create mode 100644 src/redux/actions/novel.js create mode 100644 src/redux/reducers/chapters.js create mode 100644 src/redux/reducers/novel.js diff --git a/App.js b/App.js index b7143355..20df56d9 100644 --- a/App.js +++ b/App.js @@ -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"; diff --git a/package-lock.json b/package-lock.json index 09738d7d..70f89a0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bdd70f27..d488b43b 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/common/SearchBar.js b/src/components/common/SearchBar.js deleted file mode 100644 index 717e4a85..00000000 --- a/src/components/common/SearchBar.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import { TextInput, StyleSheet } from "react-native"; -import { theme } from "../../theme/theme"; - -const SearchBar = ({ searchText, onChangeText, onSubmitEditing }) => ( - -); - -export default SearchBar; - -const styles = StyleSheet.create({ - searchBar: { - flex: 1, - fontSize: 18, - color: "white", - marginLeft: 15, - }, -}); diff --git a/src/redux/actions/chapters.js b/src/redux/actions/chapters.js new file mode 100644 index 00000000..61fdc30a --- /dev/null +++ b/src/redux/actions/chapters.js @@ -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); +}; diff --git a/src/redux/actions/novel.js b/src/redux/actions/novel.js new file mode 100644 index 00000000..1630bb08 --- /dev/null +++ b/src/redux/actions/novel.js @@ -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); + } + } +}; diff --git a/src/redux/actions/types.js b/src/redux/actions/types.js index 25425fef..ef28b4f5 100644 --- a/src/redux/actions/types.js +++ b/src/redux/actions/types.js @@ -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"; diff --git a/src/redux/reducers/chapters.js b/src/redux/reducers/chapters.js new file mode 100644 index 00000000..f2f44833 --- /dev/null +++ b/src/redux/reducers/chapters.js @@ -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; diff --git a/src/redux/reducers/novel.js b/src/redux/reducers/novel.js new file mode 100644 index 00000000..8be74c67 --- /dev/null +++ b/src/redux/reducers/novel.js @@ -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; diff --git a/src/redux/store/configureStore.js b/src/redux/store/configureStore.js index 12d7f82e..62a3be98 100644 --- a/src/redux/store/configureStore.js +++ b/src/redux/store/configureStore.js @@ -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, }) ); diff --git a/src/screens/History/History.js b/src/screens/History/History.js index f88264bc..70bc999a 100644 --- a/src/screens/History/History.js +++ b/src/screens/History/History.js @@ -57,6 +57,7 @@ const History = ({ navigation }) => { ]} > item.historyId.toString()} renderItem={renderHistoryCard} diff --git a/src/screens/Library/Library.js b/src/screens/Library/Library.js index daa60aa3..dfd8e1e7 100644 --- a/src/screens/Library/Library.js +++ b/src/screens/Library/Library.js @@ -78,6 +78,7 @@ const LibraryScreen = ({ /> ) : ( item.novelUrl} diff --git a/src/screens/Novel/Novel.js b/src/screens/Novel/Novel.js index 628fefad..f742f2be 100644 --- a/src/screens/Novel/Novel.js +++ b/src/screens/Novel/Novel.js @@ -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={ @@ -245,8 +173,14 @@ const NovelItem = ({ route, navigation }) => { ); }; +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: { diff --git a/src/screens/Novel/components/ChapterCard.js b/src/screens/Novel/components/ChapterCard.js index 1f405d2c..38866ee6 100644 --- a/src/screens/Novel/components/ChapterCard.js +++ b/src/screens/Novel/components/ChapterCard.js @@ -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 ( 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", + }, +}); diff --git a/src/screens/Novel/components/NovelHeader.js b/src/screens/Novel/components/NovelHeader.js index 44252780..9ecbc434 100644 --- a/src/screens/Novel/components/NovelHeader.js +++ b/src/screens/Novel/components/NovelHeader.js @@ -303,7 +303,7 @@ const NovelInfoHeader = ({ fontWeight: "bold", }} > - {noOfChapters + " Chapters "} + {noOfChapters + " Chapters"} { ); }); }; + +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) + ); + }); +};