Use redux for novel details and chapters
This commit is contained in:
@@ -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";
|
||||
|
||||
Generated
+5
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ const History = ({ navigation }) => {
|
||||
]}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={{ flex: 1 }}
|
||||
data={history}
|
||||
keyExtractor={(item) => item.historyId.toString()}
|
||||
renderItem={renderHistoryCard}
|
||||
|
||||
@@ -78,6 +78,7 @@ const LibraryScreen = ({
|
||||
/>
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={{ flex: 1 }}
|
||||
numColumns={3}
|
||||
data={novels}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
|
||||
+28
-94
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -303,7 +303,7 @@ const NovelInfoHeader = ({
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{noOfChapters + " Chapters "}
|
||||
{noOfChapters + " Chapters"}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="filter-variant"
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user