Compare commits

...

31 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 419420efd1 Add items per row settings 2021-03-23 18:34:58 +05:30
Rajarshee Chatterjee 871fc905ce Fix sorting and filter options 2021-03-23 07:59:08 +05:30
Rajarshee Chatterjee 5a8c652f68 Move delete chapter logic into redux 2021-03-22 21:34:04 +05:30
Rajarshee Chatterjee 97f2f84fa5 Move download chapter logic into redux 2021-03-22 20:52:39 +05:30
Rajarshee Chatterjee 65a3e8fd2d Fix statusbar 2021-03-22 14:23:32 +05:30
Rajarshee Chatterjee d32cb41475 Move some reader logic into redux 2021-03-22 09:20:20 +05:30
Rajarshee Chatterjee 5bb3be576e Fix chapters not marked as read 2021-03-22 07:38:09 +05:30
Rajarshee Chatterjee 2f09b7e29e Fix unique constraint in DownloadsTable 2021-03-22 07:26:32 +05:30
Rajarshee Chatterjee b672aba6ff Move chapter logic into redux 2021-03-21 21:15:24 +05:30
Rajarshee Chatterjee 80363d6016 Fix chapters flatlist not responding while loading 2021-03-21 18:39:16 +05:30
Rajarshee Chatterjee 195fcaf9b2 Move history logic into redux 2021-03-21 15:39:10 +05:30
Rajarshee Chatterjee 5ec08b3add Move more novel logic into redux 2021-03-21 11:11:09 +05:30
Rajarshee Chatterjee 625cdff212 Use redux for novel details and chapters 2021-03-21 09:18:49 +05:30
Rajarshee Chatterjee 83afe9c254 Refactor search and extension UI 2021-03-20 14:28:16 +05:30
Rajarshee Chatterjee 404f59323a Update version 2021-03-20 14:13:21 +05:30
Rajarshee Chatterjee 20ad556039 Change theme variables 2021-03-20 13:58:51 +05:30
Rajarshee Chatterjee 14c994486c Create ExtensionCard 2021-03-20 13:55:48 +05:30
Rajarshee Chatterjee c6884458a1 Update BoxNovel extension UI and refactor settings 2021-03-20 12:37:27 +05:30
Rajarshee Chatterjee 9dab2eccaa Update ReadLightNovel extension UI 2021-03-20 11:53:18 +05:30
Rajarshee Chatterjee 2e21a9fd33 UI improvements 2021-03-20 08:55:32 +05:30
Rajarshee Chatterjee 781e8df740 Redo previous changes 2021-03-16 14:46:41 +05:30
Rajarshee Chatterjee fa4d810ccb Revert dependency updates 2021-03-16 11:56:44 +05:30
Rajarshee Chatterjee 37f531dd93 Update dependencies 2021-03-16 10:33:16 +05:30
Rajarshee Chatterjee 1ea98ec031 Revert dependency updates 2021-03-16 09:58:15 +05:30
Rajarshee Chatterjee 65e5377345 Refactor history screen 2021-03-16 06:49:05 +05:30
Rajarshee Chatterjee 2b78350ad9 Refactor library screen 2021-03-16 06:13:10 +05:30
Rajarshee Chatterjee 69217b415d Update dependencies 2021-03-16 05:18:22 +05:30
Rajarshee Chatterjee 8c04ee3d33 Minor cleanup 2021-03-15 20:20:22 +05:30
Rajarshee Chatterjee 1f78188fd1 Use redux for library states 2021-03-15 17:10:39 +05:30
Rajarshee Chatterjee 5fb9d70036 Use modals for theme and display settings 2021-03-15 09:52:44 +05:30
Rajarshee Chatterjee b3ec043e32 Use redux to store user preferences 2021-03-15 09:04:26 +05:30
51 changed files with 2418 additions and 1981 deletions
+1 -4
View File
@@ -7,9 +7,8 @@ import AppLoading from "expo-app-loading";
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";
@@ -22,8 +21,6 @@ const getFonts = () =>
const App = () => {
const [fontsLoaded, setFontsLoaded] = useState(false);
setStatusBar();
const createDb = () => {
createTables();
createIndexes();
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 32 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+13
View File
@@ -7120,6 +7120,14 @@
"prop-types": "^15.7.2"
}
},
"react-native-input-spinner": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/react-native-input-spinner/-/react-native-input-spinner-1.7.7.tgz",
"integrity": "sha512-6zYFU2uhChJ+JC/5yujlZu+r5PYQSYIXluUqTNR3jOCKai0dlCkWiz7d3K2rM7Q2SAIiw4bSBR6mWZIuhL+DEg==",
"requires": {
"prop-types": "^15.7.2"
}
},
"react-native-iphone-x-helper": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
@@ -7270,6 +7278,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",
+2
View File
@@ -30,6 +30,7 @@
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
"react-native-collapsible-header-views": "^1.1.2",
"react-native-gesture-handler": "^1.8.0",
"react-native-input-spinner": "^1.7.7",
"react-native-offline": "^5.7.0",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "^1.13.2",
@@ -40,6 +41,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"
+131 -9
View File
@@ -1,19 +1,141 @@
import React from "react";
import { Appbar } from "react-native-paper";
import React, { useRef } from "react";
import { View, TextInput, StyleSheet } from "react-native";
import {
TouchableRipple,
IconButton,
Appbar as MaterialAppbar,
} from "react-native-paper";
import Constants from "expo-constants";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
export const CustomAppbar = ({ title, onBackAction }) => {
export const Appbar = ({ title, onBackAction }) => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<Appbar.Header style={{ backgroundColor: theme.colorDarkPrimary }}>
{onBackAction && <Appbar.BackAction onPress={onBackAction} />}
<Appbar.Content
<MaterialAppbar.Header style={{ backgroundColor: theme.colorPrimary }}>
{onBackAction && (
<MaterialAppbar.BackAction onPress={onBackAction} />
)}
<MaterialAppbar.Content
title={title}
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
/>
</Appbar.Header>
</MaterialAppbar.Header>
);
};
export const SearchAppbar = ({
placeholder,
getSearchResults,
setSearchText,
searchText,
getNovels,
setLoading,
screen,
onFilter,
}) => {
const searchRef = useRef(null);
const navigation = useNavigation();
const theme = useSelector((state) => state.themeReducer.theme);
return (
<TouchableRipple
borderless
onPress={() => searchRef.current.focus()}
style={[
styles.searchAppbarContainer,
{ backgroundColor: theme.searchBarColor },
]}
>
<View
style={{
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
}}
>
<View style={{ flex: 1, flexDirection: "row" }}>
{screen === "Library" ? (
<IconButton
icon="magnify"
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
/>
) : (
<IconButton
icon="arrow-left"
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
onPress={() => navigation.goBack()}
/>
)}
<TextInput
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorSecondary,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
onChangeText={(text) => {
setSearchText(text);
if (screen === "Library") {
getSearchResults(text);
}
}}
onSubmitEditing={() => {
if (screen === "Extension") {
getSearchResults(searchText);
}
}}
defaultValue={searchText}
/>
</View>
{searchText !== "" && (
<IconButton
icon="close"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={() => {
setLoading?.(true);
getNovels();
setSearchText("");
}}
/>
)}
<IconButton
icon="filter-variant"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
disabled={typeof onFilter === "function" ? false : true}
/**
* TODO
*/
onPress={() => onFilter?.()}
/>
</View>
</TouchableRipple>
);
};
const styles = StyleSheet.create({
searchAppbarContainer: {
marginTop: Constants.statusBarHeight + 8,
height: 48,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
marginBottom: 12,
borderRadius: 8,
marginHorizontal: 12,
elevation: 2,
},
});
@@ -0,0 +1,23 @@
import React from "react";
import { View } from "react-native";
import { useSelector } from "react-redux";
const BottomSheetHandle = () => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<>
<View
style={{
backgroundColor: theme.textColorHintDark,
height: 5,
width: 30,
borderRadius: 50,
top: 10,
alignSelf: "center",
}}
/>
</>
);
};
export default BottomSheetHandle;
+52
View File
@@ -0,0 +1,52 @@
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { useSelector } from "react-redux";
const EmptyView = ({ icon, description }) => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<View style={styles.emptyViewContainer}>
<Text
style={[
styles.emptyViewIcon,
{
color: theme.textColorSecondary,
},
]}
>
{icon}
</Text>
<Text
style={[
styles.emptyViewText,
{
color: theme.textColorSecondary,
},
]}
>
{description}
</Text>
</View>
);
};
export default EmptyView;
const styles = StyleSheet.create({
emptyViewContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
emptyViewIcon: {
fontSize: 45,
fontWeight: "bold",
},
emptyViewText: {
fontWeight: "bold",
marginTop: 10,
textAlign: "center",
paddingHorizontal: 30,
},
});
+98 -33
View File
@@ -1,38 +1,55 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, ImageBackground } from "react-native";
import React from "react";
import { StyleSheet, View, Text, ImageBackground, Image } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { TouchableRipple } from "react-native-paper";
import { theme } from "../../theme/theme";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useSelector } from "react-redux";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const [mode, setMode] = useState();
AsyncStorage.getItem("@display_mode").then((value) => setMode(value));
const displayMode = useSelector(
(state) => state.settingsReducer.displayMode
);
return (
<View style={{ flex: 1 / 3 }}>
const theme = useSelector((state) => state.themeReducer.theme);
const itemsPerRow = useSelector(
(state) => state.settingsReducer.itemsPerRow
);
const height = {
1: 550,
2: 280,
3: 180,
4: 140,
5: 100,
6: 80,
7: 70,
8: 58,
9: 55,
10: 50,
};
return displayMode !== 2 ? (
<View style={{ flex: 1 / itemsPerRow }}>
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
style={styles.opac}
onPress={onPress}
>
<>
{item.novelCover ? (
{item.novelCover && (
<ImageBackground
source={{
uri: item.novelCover,
}}
style={styles.logo}
source={{ uri: item.novelCover }}
style={{ height: height[itemsPerRow] }}
imageStyle={[
{ borderRadius: 4 },
libraryStatus && { opacity: 0.4 },
libraryStatus && { opacity: 0.5 },
]}
progressiveRenderingEnabled={true}
>
{(!mode || mode === "compact") && (
{displayMode === 0 && (
<View style={styles.titleContainer}>
<LinearGradient
colors={[
@@ -43,7 +60,13 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
>
<Text
numberOfLines={2}
style={styles.title}
style={[
styles.title,
{
color:
"rgba(255,255,255,1)",
},
]}
>
{item.novelName}
</Text>
@@ -51,29 +74,60 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
</View>
)}
</ImageBackground>
) : (
<View>
<Text style={{ color: "white" }}>HAHAHAHAH</Text>
</View>
)}
{mode === "comfortable" && (
<Text numberOfLines={2} style={styles.title}>
{displayMode === 1 && (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{item.novelName}
</Text>
)}
</>
</TouchableRipple>
</View>
) : (
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.listView}
onPress={onPress}
>
<>
<Image
source={{ uri: item.novelCover }}
style={styles.extensionIcon}
/>
<Text
style={{
flex: 1,
color: theme.textColorPrimary,
marginLeft: 16,
fontSize: 15,
paddingRight: 8,
}}
numberOfLines={1}
>
{item.novelName}
</Text>
</>
</TouchableRipple>
);
};
export default NovelCover;
const styles = StyleSheet.create({
logo: {
height: 173,
borderRadius: 4,
},
// logo: {
// height: 180,
// },
titleContainer: {
flex: 1,
justifyContent: "flex-end",
@@ -82,18 +136,29 @@ const styles = StyleSheet.create({
title: {
fontFamily: "pt-sans-bold",
fontSize: 14,
color: "white",
padding: 8,
textShadowColor: "rgba(0, 0, 0, 0.75)",
textShadowOffset: { width: -1, height: 1 },
textShadowRadius: 10,
},
linearGradient: {
bottom: -1,
borderRadius: 4,
},
opac: {
paddingHorizontal: 3.5,
paddingVertical: 3.5,
paddingHorizontal: 4.5,
paddingVertical: 4,
borderRadius: 4,
},
extensionIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
listView: {
flex: 1,
paddingVertical: 8,
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
borderRadius: 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,
},
});
+12 -6
View File
@@ -1,17 +1,23 @@
import React, { useState } from "react";
import React from "react";
import { Checkbox } from "react-native-paper";
import { useSelector } from "react-redux";
export const DisplayCheckbox = ({ displayMode, onPress, value }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const label = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
return (
<Checkbox.Item
label={value.charAt(0).toUpperCase() + value.slice(1)}
labelStyle={{ color: theme.textColorPrimaryDark }}
label={label[value]}
labelStyle={{ color: theme.textColorPrimary }}
status={displayMode === value ? "checked" : "unchecked"}
mode="ios"
uncheckedColor={theme.textColorSecondaryDark}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccentDark}
onPress={onPress}
/>
@@ -24,10 +30,10 @@ export const ThemeCheckbox = ({ onPress, label, checked }) => {
return (
<Checkbox.Item
label={label}
labelStyle={{ color: theme.textColorPrimaryDark }}
labelStyle={{ color: theme.textColorPrimary }}
status={checked ? "checked" : "unchecked"}
mode="ios"
uncheckedColor={theme.textColorSecondaryDark}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccentDark}
onPress={onPress}
/>
+44 -35
View File
@@ -1,26 +1,28 @@
import React from "react";
import React, { useEffect } from "react";
import {
createStackNavigator,
TransitionPresets,
} from "@react-navigation/stack";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import { Provider } from "react-native-paper";
import { setStatusBarStyle } from "expo-status-bar";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import Library from "../screens/Library";
import History from "../screens/history/History";
import Browse from "../screens/Browse";
import Library from "../screens/Library/Library";
import History from "../screens/History/History";
import Browse from "../screens/Browse/Browse";
import More from "../screens/more/More";
import About from "../screens/more/About";
import Settings from "../screens/more/Settings";
import More from "../screens/More/More";
import About from "../screens/More/About";
import Settings from "../screens/More/Settings";
import NovelItem from "../screens/NovelItem";
import ChapterItem from "../screens/ChapterItem";
import NovelItem from "../screens/Novel/Novel";
import ChapterItem from "../screens/Chapter/Chapter";
// Extensions
import BoxNovel from "../screens/extensions/boxnovel/BoxNovel";
import ReadLightNovel from "../screens/extensions/readlightnovel/ReadLightNovel";
import BoxNovel from "../screens/Extensions/boxnovel/BoxNovel";
import ReadLightNovel from "../screens/Extensions/readlightnovel/ReadLightNovel";
const Stack = createStackNavigator();
@@ -61,10 +63,11 @@ import { useSelector } from "react-redux";
const BottomNavigator = () => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<Tab.Navigator
shifting={false}
barStyle={{ backgroundColor: theme.colorDarkPrimary }}
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccentDark}
>
<Tab.Screen
@@ -138,30 +141,36 @@ const BottomNavigator = () => {
};
const Router = () => {
const themeCode = useSelector((state) => state.themeReducer.themeCode);
useEffect(() => setStatusBarStyle(themeCode === 1 ? "dark" : "light"), []);
return (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Router" component={BottomNavigator} />
<Stack.Screen
name="ChapterItem"
component={ChapterItem}
options={{ ...TransitionPresets.SlideFromRightIOS }}
/>
<Stack.Screen
name="NovelItem"
component={NovelItem}
options={{
headerTitle: "",
headerShown: true,
headerTransparent: true,
headerTintColor: "white",
}}
/>
<Stack.Screen name="BoxNovelStack" component={BoxNovelStack} />
<Stack.Screen
name="ReadLightNovelStack"
component={ReadLightNovelStack}
/>
</Stack.Navigator>
<Provider>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Router" component={BottomNavigator} />
<Stack.Screen
name="ChapterItem"
component={ChapterItem}
options={{ ...TransitionPresets.SlideFromRightIOS }}
/>
<Stack.Screen
name="NovelItem"
component={NovelItem}
options={{
headerTitle: "",
headerShown: true,
headerTransparent: true,
headerTintColor: "white",
}}
/>
<Stack.Screen name="BoxNovelStack" component={BoxNovelStack} />
<Stack.Screen
name="ReadLightNovelStack"
component={ReadLightNovelStack}
/>
</Stack.Navigator>
</Provider>
);
};
+11
View File
@@ -0,0 +1,11 @@
import { GET_EXTENSIONS } from "./types";
import { fetchExtensionList } from "../../services/api";
export const getExtensions = () => async (dispatch) => {
const res = await fetchExtensionList();
dispatch({
type: GET_EXTENSIONS,
payload: res,
});
};
+38
View File
@@ -0,0 +1,38 @@
import {
UPDATE_NOVEL_HISTORY,
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
} from "./types";
import {
getHistoryFromDb,
insertIntoHistory,
deleteChapterHistory,
} from "../../services/db";
export const getHistory = () => async (dispatch) => {
dispatch({ type: LOAD_HISTORY });
const history = await getHistoryFromDb();
dispatch({ type: GET_HISTORY, payload: history });
};
export const updateNovelHistory = (chapterUrl, chapterName, novelUrl) => async (
dispatch
) => {
await insertIntoHistory(chapterUrl, chapterName, novelUrl);
dispatch({
type: UPDATE_NOVEL_HISTORY,
});
};
export const deleteChapterFromHistory = (novelUrl) => async (dispatch) => {
await deleteChapterHistory(novelUrl);
dispatch({
type: CLEAR_NOVEL_HISTORY,
payload: { novelUrl },
});
};
+12 -3
View File
@@ -1,11 +1,20 @@
import { GET_LIBRARY_NOVELS } from "./types";
import { getLibraryFromDb } from "../../services/db";
import { getLibraryFromDb, searchInLibrary } from "../../services/db";
export const getLibraryNovels = () => async (dispatch) => {
getLibraryFromDb().then((res) => console.log(res));
const res = await getLibraryFromDb();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res.data,
payload: res,
});
};
export const searchLibraryNovels = (searchText) => async (dispatch) => {
const res = await searchInLibrary(searchText);
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
+168
View File
@@ -0,0 +1,168 @@
import { ToastAndroid } from "react-native";
import {
LOADING_NOVEL,
GET_NOVEL,
GET_CHAPTERS,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
} from "./types";
import {
checkNovelInDb,
getChaptersFromDb,
getNovelInfoFromDb,
insertChaptersInDb,
insertNovelInfoInDb,
toggleFavourite,
downloadChapterFromSource,
deleteChapterFromDb,
chapterRead,
isChapterDownloaded,
getChapterFromDb,
} from "../../services/db";
import {
fetchChapterFromSource,
fetchChaptersFromSource,
fetchNovelFromSource,
} from "../../services/api";
export const setNovel = (novel) => async (dispatch) => {
dispatch({ type: SET_NOVEL, payload: novel });
};
export const getNovel = (inLibrary, extensionId, novelUrl) => async (
dispatch
) => {
dispatch({ type: LOADING_NOVEL });
if (inLibrary === 1) {
const chapters = await getChaptersFromDb(novelUrl, "", "");
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({ type: FETCHING_NOVEL });
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);
dispatch({
type: GET_NOVEL,
payload: { novel, chapters: novel.novelChapters },
});
insertNovelInfoInDb(novel);
insertChaptersInDb(novelUrl, novel.novelChapters);
}
}
};
export const sortAndFilterChapters = (novelUrl, filter, sort) => async (
dispatch
) => {
const chapters = await getChaptersFromDb(novelUrl, filter, sort);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
};
export const insertNovelInLibrary = (inLibrary, novelUrl) => async (
dispatch
) => {
toggleFavourite(inLibrary, novelUrl);
dispatch({
type: UPDATE_IN_LIBRARY,
payload: !inLibrary,
});
ToastAndroid.show(
!inLibrary ? "Added to library" : "Removed from library",
ToastAndroid.SHORT
);
};
export const getChapter = (extensionId, chapterUrl, novelUrl) => async (
dispatch
) => {
dispatch({ type: CHAPTER_LOADING });
const isDownloaded = await isChapterDownloaded(chapterUrl, novelUrl);
let chapter;
if (isDownloaded) {
chapter = await getChapterFromDb(chapterUrl, novelUrl);
} else {
chapter = await fetchChapterFromSource(
extensionId,
novelUrl,
chapterUrl
);
}
dispatch({ type: GET_CHAPTER, payload: chapter });
};
export const updateChapterRead = (chapterUrl, novelUrl) => async (dispatch) => {
await chapterRead(chapterUrl, novelUrl);
dispatch({ type: CHAPTER_READ, payload: { chapterUrl, novelUrl } });
};
export const downloadChapter = (
extensionId,
novelUrl,
chapterUrl,
chapterName
) => async (dispatch) => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: {
extensionId,
novelUrl,
chapterUrl,
},
});
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
) => async (dispatch) => {
await deleteChapterFromDb(novelUrl, chapterUrl);
dispatch({
type: CHAPTER_DELETED,
payload: { extensionId, novelUrl, chapterUrl },
});
ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
};
+35
View File
@@ -0,0 +1,35 @@
import {
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
} from "./types";
export const setDisplayMode = (val) => (dispatch) => {
dispatch({
type: SET_DISPLAY_MODE,
payload: val,
});
};
export const setItemsPerRow = (val) => (dispatch) => {
console.log("called" + val);
dispatch({
type: SET_ITEMS_PER_ROW,
payload: val,
});
};
export const updateReaderTheme = (val) => (dispatch) => {
dispatch({
type: UPDATE_READER_THEME,
payload: val,
});
};
export const updateReaderTextSize = (val) => (dispatch) => {
dispatch({
type: UPDATE_READER_TEXT_SIZE,
payload: val,
});
};
+9 -2
View File
@@ -1,8 +1,15 @@
import { SWITCH_THEME } from "./types";
export const switchTheme = (theme) => (dispatch) => {
/**
* 0 -> Amoled Dark Theme
* 1 -> Light Theme
* 2 -> Dark Theme
* 3 -> Midnight Dusk Theme
*/
export const switchTheme = (val) => (dispatch) => {
dispatch({
type: SWITCH_THEME,
payload: theme,
payload: val,
});
};
+44
View File
@@ -1,3 +1,47 @@
export const SWITCH_THEME = "SWITCH_THEME";
export const GET_LIBRARY_NOVELS = "GET_LIBRARY_NOVELS";
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 FETCHING_NOVEL = "FETCHING_NOVEL";
export const LOADING_CHAPTERS = "LOADING_CHAPTERS";
export const SET_NOVEL = "SET_NOVEL";
export const GET_NOVEL = "GET_NOVEL";
export const UPDATE_IN_LIBRARY = "UPDATE_IN_LIBRARY";
export const GET_CHAPTERS = "GET_CHAPTERS";
export const CHAPTER_DOWNLOADED = "CHAPTER_DOWNLOADED";
export const CHAPTER_DELETED = "CHAPTER_DELETED";
export const CHAPTER_DOWNLOADING = "CHAPTER_DOWNLOADING";
export const LOAD_HISTORY = "LOAD_HISTORY";
export const UPDATE_NOVEL_HISTORY = "UPDATE_NOVEL_HISTORY";
export const CLEAR_NOVEL_HISTORY = "CLEAR_NOVEL_HISTORY";
export const GET_HISTORY = "GET_HISTORY";
export const CHAPTER_READ = "CHAPTER_READ";
export const GET_CHAPTER = "GET_CHAPTER";
export const CHAPTER_LOADING = "CHAPTER_LOADING";
export const UPDATE_READER_THEME = "UPDATE_READER_THEME";
export const UPDATE_READER_TEXT_SIZE = "UPDATE_READER_TEXT_SIZE";
+19
View File
@@ -0,0 +1,19 @@
import { GET_EXTENSIONS } from "../actions/types";
const initialState = {
extensions: [],
loading: true,
};
const libraryReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_EXTENSIONS:
return { extensions: payload, loading: false };
default:
return state;
}
};
export default libraryReducer;
+41
View File
@@ -0,0 +1,41 @@
import {
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
UPDATE_NOVEL_HISTORY,
} from "../actions/types";
const initialState = {
history: [],
loading: true,
};
const historyReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case LOAD_HISTORY:
return { ...state, loading: true };
case GET_HISTORY:
return { ...state, history: payload, loading: false };
case UPDATE_NOVEL_HISTORY:
return state;
case CLEAR_NOVEL_HISTORY:
return {
...state,
history: state.history.filter(
(item) => item.novelUrl !== payload.novelUrl
),
};
// return {
// ...state,
// history: state.history.map((novel) =>
// novel.novelUrl === payload.novelUrl ? payload : novel
// ),
// };
default:
return state;
}
};
export default historyReducer;
+109
View File
@@ -0,0 +1,109 @@
import {
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
GET_CHAPTERS,
LOADING_NOVEL,
GET_NOVEL,
GET_CHAPTER,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
CHAPTER_READ,
CHAPTER_LOADING,
} from "../actions/types";
const initialState = {
novel: null,
chapters: [],
chapter: null,
downloading: [],
chapterLoading: true,
loading: true,
fetching: false,
};
const novelReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case LOADING_NOVEL:
return { ...state, loading: true };
case FETCHING_NOVEL:
return { ...state, loading: true, fetching: true };
case SET_NOVEL:
return { ...state, novel: payload };
case GET_NOVEL:
return {
...state,
novel: payload.novel,
chapters: payload.chapters,
loading: false,
fetching: false,
};
case GET_CHAPTERS:
return {
...state,
chapters: payload,
loading: false,
fetching: false,
};
case UPDATE_IN_LIBRARY:
return {
...state,
novel: { ...state.novel, libraryStatus: payload },
};
case CHAPTER_READ:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterUrl === payload.chapterUrl &&
chapter.novelUrl === payload.novelUrl
? { ...chapter, read: 1 }
: chapter
),
};
case CHAPTER_LOADING:
return {
...state,
chapterLoading: true,
};
case GET_CHAPTER:
return { ...state, chapter: payload, chapterLoading: false };
case CHAPTER_DOWNLOADING:
return {
...state,
downloading: [...state.downloading, payload],
};
case CHAPTER_DOWNLOADED:
return {
...state,
downloading: state.downloading.filter(
(chapter) =>
chapter.chapterUrl === payload.chapterUrl &&
chapter.novelUrl === payload.novelUrl
),
chapters: state.chapters.map((chapter) =>
chapter.chapterUrl === payload.chapterUrl &&
chapter.novelUrl === payload.novelUrl
? { ...chapter, downloaded: 1 }
: chapter
),
};
case CHAPTER_DELETED:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterUrl === payload.chapterUrl &&
chapter.novelUrl === payload.novelUrl
? { ...chapter, downloaded: 0 }
: chapter
),
};
default:
return state;
}
};
export default novelReducer;
+56
View File
@@ -0,0 +1,56 @@
import {
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
} from "../actions/types";
const initialState = {
displayMode: 0,
itemsPerRow: 3,
reader: {
theme: 1,
textSize: 16,
},
};
/**
* Display Mode
* 0 -> Compact
* 1 -> Comfortable
* 2 -> List
*/
/**
* Reader Settings
* theme
* 1 -> Dark
* 2 -> White
* 3 -> Sepia
*/
const settingsReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case SET_DISPLAY_MODE:
return {
...state,
displayMode: payload,
itemsPerRow: payload === 2 ? 1 : state.itemsPerRow,
};
case SET_ITEMS_PER_ROW:
return {
...state,
itemsPerRow: state.displayMode !== 2 ? payload : 1,
};
case UPDATE_READER_THEME:
return { ...state, reader: { ...state.reader, theme: payload } };
case UPDATE_READER_TEXT_SIZE:
return { ...state, reader: { ...state.reader, textSize: payload } };
default:
return state;
}
};
export default settingsReducer;
+16 -3
View File
@@ -1,7 +1,20 @@
import { amoledDarkTheme } from "../../theme/theme";
import {
amoledDarkTheme,
lightTheme,
darkTheme,
midnightDuskTheme,
} from "../../theme/theme";
const themes = {
0: amoledDarkTheme,
1: lightTheme,
2: darkTheme,
3: midnightDuskTheme,
};
const initialState = {
theme: amoledDarkTheme,
themeCode: 0,
theme: themes[0],
};
const themeReducer = (state = initialState, action) => {
@@ -9,7 +22,7 @@ const themeReducer = (state = initialState, action) => {
switch (type) {
case "SWITCH_THEME":
return { theme: payload };
return { ...state, themeCode: payload, theme: themes[payload] };
default:
return state;
}
+13 -1
View File
@@ -4,16 +4,28 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
import thunk from "redux-thunk";
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";
import historyReducer from "../reducers/history";
const persistConfig = {
key: "root",
storage: AsyncStorage,
blacklist: ["novelReducer"],
};
const persistedReducer = persistReducer(
persistConfig,
combineReducers({ themeReducer, libraryReducer })
combineReducers({
themeReducer,
settingsReducer,
libraryReducer,
extensionReducer,
novelReducer,
historyReducer,
})
);
export const store = createStore(persistedReducer, applyMiddleware(thunk));
-144
View File
@@ -1,144 +0,0 @@
import React, { useEffect, useState } from "react";
import {
StyleSheet,
View,
Text,
FlatList,
Image,
ActivityIndicator,
} from "react-native";
import { TouchableRipple, Button } from "react-native-paper";
import { CustomAppbar } from "../components/common/Appbar";
import { useSelector } from "react-redux";
const Browse = ({ navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const [loading, setLoading] = useState(true);
const [extensions, setExtensions] = useState();
useEffect(() => {
fetch(`https://lnreader-extensions.herokuapp.com/api/`)
.then((response) => response.json())
.then((json) => {
setExtensions(json);
setLoading(false);
});
}, []);
return (
<>
<CustomAppbar title="Browse" />
<View
style={[
styles.container,
{ backgroundColor: theme.colorDarkPrimaryDark },
]}
>
{!loading ? (
<FlatList
data={extensions}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={({ item }) => (
<TouchableRipple
style={styles.sourceCard}
onPress={() =>
navigation.navigate(
item.sourceName + "Stack"
)
}
rippleColor={theme.rippleColorDark}
>
<>
<Image
source={{
uri: item.sourceCover,
}}
style={{
width: 40,
height: 40,
borderRadius: 6,
}}
resizeMode="contain"
/>
<View
style={{
marginLeft: 15,
flex: 1,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
}}
>
<View>
<Text
style={{
color:
theme.textColorPrimaryDark,
fontSize: 14,
}}
>
{item.sourceName}
</Text>
<Text
style={{
color:
theme.textColorSecondaryDark,
fontSize: 12,
}}
>
{item.sourceLanguage}
</Text>
</View>
<View>
<Button
labelStyle={{
letterSpacing: 0,
}}
uppercase={false}
color={theme.colorAccentDark}
onPress={() =>
navigation.navigate(
item.sourceName +
"Stack"
)
}
>
Browse
</Button>
</View>
</View>
</>
</TouchableRipple>
)}
/>
) : (
<View style={{ padding: 20 }}>
<ActivityIndicator
size="small"
color={theme.colorAccentDark}
/>
</View>
)}
</View>
</>
);
};
export default Browse;
const styles = StyleSheet.create({
container: {
flex: 1,
},
sourceCard: {
paddingVertical: 10,
marginVertical: 5,
paddingHorizontal: 20,
borderRadius: 6,
flexDirection: "row",
alignItems: "center",
},
});
+56
View File
@@ -0,0 +1,56 @@
import React, { useEffect } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar } from "../../components/common/Appbar";
import { connect } from "react-redux";
import { getExtensions } from "../../redux/actions/extension";
import ExtensionCard from "./components/ExtensionCard";
const Browse = ({ theme, extensions, loading, getExtensions }) => {
useEffect(() => {
getExtensions();
}, []);
return (
<>
<Appbar title="Browse" />
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
data={extensions}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={({ item }) => <ExtensionCard item={item} />}
ListEmptyComponent={
loading && (
<ActivityIndicator
size="small"
color={theme.colorAccentDark}
style={{ marginTop: 16 }}
/>
)
}
/>
</View>
</>
);
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
extensions: state.extensionReducer.extensions,
loading: state.extensionReducer.loading,
});
export default connect(mapStateToProps, { getExtensions })(Browse);
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 4,
},
});
@@ -0,0 +1,92 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple, Button } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
const ExtensionCard = ({ item }) => {
const { sourceName, sourceCover, sourceLanguage } = item;
const navigation = useNavigation();
const theme = useSelector((state) => state.themeReducer.theme);
return (
<TouchableRipple
style={styles.extensionCard}
onPress={() => navigation.navigate(sourceName + "Stack")}
rippleColor={theme.rippleColor}
borderless
>
<>
<Image
source={{
uri: sourceCover,
}}
style={styles.extensionIcon}
resizeMode="contain"
/>
<View style={styles.extensionDetails}>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
{sourceName}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
</View>
<View>
<Button
labelStyle={{
letterSpacing: 0,
}}
uppercase={false}
color={theme.colorAccentDark}
onPress={() =>
navigation.navigate(sourceName + "Stack")
}
>
Browse
</Button>
</View>
</View>
</>
</TouchableRipple>
);
};
export default ExtensionCard;
const styles = StyleSheet.create({
extensionCard: {
flexDirection: "row",
alignItems: "center",
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
borderRadius: 4,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 8,
},
extensionDetails: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
});
+193
View File
@@ -0,0 +1,193 @@
import React, { useState, useEffect, useRef } from "react";
import { StyleSheet, View, Text, ActivityIndicator } from "react-native";
import { Appbar, Provider, Portal } from "react-native-paper";
import { CollapsibleHeaderScrollView } from "react-native-collapsible-header-views";
import BottomSheet from "./components/BottomSheet";
import { updateNovelHistory } from "../../redux/actions/history";
import { getChapter, updateChapterRead } from "../../redux/actions/novel";
import { connect } from "react-redux";
const ChapterItem = ({
route,
navigation,
theme,
updateNovelHistory,
updateChapterRead,
getChapter,
chapter,
loading,
reader,
}) => {
const { extensionId, chapterUrl, novelUrl, chapterName } = route.params;
let _panel = useRef(null);
useEffect(() => {
getChapter(extensionId, chapterUrl, novelUrl);
updateNovelHistory(chapterUrl, chapterName, novelUrl);
}, []);
const readerBackground = (val) => {
const backgroundColor = {
1: "#000000",
2: "#FFFFFF",
3: "#F4ECD8",
};
return backgroundColor[val];
};
const readerTextColor = (val) => {
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "#000000";
return textColor;
};
const readerLineHeight = (val) => {
const lineHeight = {
12: 20,
16: 25,
20: 28,
};
return lineHeight[val];
};
const isCloseToBottom = ({
layoutMeasurement,
contentOffset,
contentSize,
}) => {
const paddingToBottom = 40;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
};
return (
<Provider>
<CollapsibleHeaderScrollView
headerContainerBackgroundColor="rgba(0,0,0,0.4)"
CollapsibleHeaderComponent={
<Appbar.Header
style={{
backgroundColor: "transparent",
elevation: 0,
}}
>
<Appbar.BackAction
onPress={() => navigation.goBack()}
color="#FFFFFF"
size={26}
style={{ marginRight: 0 }}
/>
<Appbar.Content
title={loading ? "Chapter" : chapter.chapterName}
titleStyle={{ color: "#FFFFFF" }}
/>
{!loading && (
<>
<Appbar.Action
icon="chevron-left"
size={26}
disabled={!chapter.prevChapter}
onPress={() => {
navigation.push("ChapterItem", {
chapterUrl: chapter.prevChapter,
extensionId,
novelUrl,
chapterName: chapter.chapterName,
});
}}
color="#FFFFFF"
/>
<Appbar.Action
icon="chevron-right"
size={26}
disabled={!chapter.nextChapter}
onPress={() => {
navigation.push("ChapterItem", {
chapterUrl: chapter.nextChapter,
extensionId,
novelUrl,
chapterName: chapter.chapterName,
});
}}
color="#FFFFFF"
/>
<Appbar.Action
icon="dots-vertical"
size={26}
onPress={() =>
_panel.current.show({ velocity: -1.5 })
}
color="#FFFFFF"
/>
</>
)}
</Appbar.Header>
}
headerHeight={100}
contentContainerStyle={[
styles.container,
{ backgroundColor: readerBackground(reader.theme) },
]}
onScroll={({ nativeEvent }) => {
if (isCloseToBottom(nativeEvent)) {
updateChapterRead(chapterUrl, novelUrl);
}
}}
>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
size={50}
color={theme.colorAccentDark}
/>
</View>
) : (
<Text
style={[
{
paddingVertical: 15,
fontSize: reader.textSize,
color: readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize),
},
]}
>
{chapter.chapterText.trim()}
</Text>
)}
<Portal>
<BottomSheet bottomSheetRef={_panel} />
</Portal>
</CollapsibleHeaderScrollView>
</Provider>
);
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
chapter: state.novelReducer.chapter,
loading: state.novelReducer.chapterLoading,
reader: state.settingsReducer.reader,
});
export default connect(mapStateToProps, {
updateNovelHistory,
updateChapterRead,
getChapter,
})(ChapterItem);
const styles = StyleSheet.create({
container: {
flexGrow: 1,
paddingHorizontal: 10,
paddingVertical: 10,
},
});
@@ -5,19 +5,21 @@ import { ToggleButton } from "react-native-paper";
import Slider from "@react-native-community/slider";
import Bottomsheet from "rn-sliding-up-panel";
import { useSelector } from "react-redux";
import { connect } from "react-redux";
import { saveReaderTheme } from "../../services/asyncStorage";
import {
updateReaderTextSize,
updateReaderTheme,
} from "../../../redux/actions/settings";
import BottomSheetHandle from "../../../components/common/BottomSheetHandle";
const ChapterBottomSheet = ({
bottomSheetRef,
setSize,
size,
readerTheme,
setReaderTheme,
theme,
reader,
updateReaderTextSize,
updateReaderTheme,
}) => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<Bottomsheet
animatedValue={new Animated.Value(0)}
@@ -26,29 +28,12 @@ const ChapterBottomSheet = ({
snappingPoints={[0, 200]}
showBackdrop={false}
>
<View
style={[
styles.contentContainer,
{
backgroundColor: "rgba(0,0,0,0.4)",
},
]}
>
<View
style={{
backgroundColor: theme.textColorHintDark,
height: 5,
width: 30,
borderRadius: 50,
top: 10,
alignSelf: "center",
}}
/>
<View style={{ flex: 1, alignItems: "center", paddingTop: 30 }}>
<View style={styles.contentContainer}>
<BottomSheetHandle />
<View style={styles.readerSettingsContainer}>
<Text
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
fontWeight: "bold",
}}
>
@@ -59,52 +44,49 @@ const ChapterBottomSheet = ({
width: Dimensions.get("window").width,
height: 40,
}}
value={size}
value={reader.textSize}
minimumValue={12}
maximumValue={20}
step={4}
minimumTrackTintColor={theme.colorAccentDark}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccentDark}
onValueChange={(value) => setSize(value)}
onValueChange={(value) => updateReaderTextSize(value)}
/>
<Text
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
fontWeight: "bold",
}}
>
Reader Theme
</Text>
<ToggleButton.Row
onValueChange={(value) => {
setReaderTheme(value);
saveReaderTheme(value);
}}
value={readerTheme}
onValueChange={(value) => updateReaderTheme(value)}
value={reader.theme}
style={{ marginTop: 10 }}
>
<ToggleButton
icon="format-text"
color="white"
color="#FFFFFF"
value={1}
style={{
backgroundColor: "black",
backgroundColor: "#000000",
marginHorizontal: 10,
}}
/>
<ToggleButton
icon="format-text"
color="black"
color="#000000"
value={2}
style={{
backgroundColor: "white",
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
}}
/>
<ToggleButton
icon="format-text"
color="black"
color="#000000"
value={3}
style={{
backgroundColor: "#F4ECD8",
@@ -118,12 +100,22 @@ const ChapterBottomSheet = ({
);
};
export default ChapterBottomSheet;
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
reader: state.settingsReducer.reader,
});
export default connect(mapStateToProps, {
updateReaderTextSize,
updateReaderTheme,
})(ChapterBottomSheet);
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
// paddingTop: ,
// alignItems: "center",
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
backgroundColor: "rgba(0,0,0,0.4)",
},
readerSettingsContainer: { flex: 1, alignItems: "center", paddingTop: 30 },
});
-315
View File
@@ -1,315 +0,0 @@
import React, { useState, useEffect } from "react";
import { StyleSheet, View, Text, ActivityIndicator } from "react-native";
import { Appbar, Provider, Portal } from "react-native-paper";
import { theme } from "../theme/theme";
import { CollapsibleHeaderScrollView } from "react-native-collapsible-header-views";
import BottomSheet from "../components/chapter/BottomSheet";
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import { fetchChapterFromSource } from "../services/api";
import { getReaderTheme } from "../services/asyncStorage";
const ChapterItem = ({ route, navigation }) => {
const { extensionId, chapterUrl, novelUrl, chapterName } = route.params;
const [loading, setLoading] = useState(true);
const [chapter, setChapter] = useState();
const [size, setSize] = useState(16);
const [readerTheme, setReaderTheme] = useState(1);
getReaderTheme().then((value) => setReaderTheme(value));
const setHistory = () => {
db.transaction((tx) => {
tx.executeSql(
"INSERT INTO HistoryTable (chapterUrl, novelUrl, chapterName, lastRead) VALUES ( ?, ?, ?, (datetime('now','localtime')))",
[chapterUrl, novelUrl, chapterName],
(tx, res) =>
console.log(
"Inserted into history table: " + novelUrl + chapterUrl
),
(tx, error) => console.log(error)
);
tx.executeSql(
"UPDATE LibraryTable SET lastRead = ?, lastReadName = ?, unread = 0 WHERE novelUrl = ?",
[chapterUrl, chapterName, novelUrl],
(tx, res) =>
console.log("Set Last Read " + novelUrl + chapterUrl),
(tx, error) => console.log(error)
);
});
};
const updateHistory = () => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE HistoryTable SET lastRead = (datetime('now','localtime')), chapterName=?, chapterUrl=? WHERE novelUrl = ?",
[chapterName, chapterUrl, novelUrl],
(tx, res) =>
console.log("Updated into history table: " + novelUrl),
(tx, error) => console.log(error)
);
tx.executeSql(
"UPDATE LibraryTable SET lastRead = ?, lastReadName = ?, unread = 0 WHERE novelUrl = ?",
[chapterUrl, chapterName, novelUrl],
(tx, res) =>
console.log("Set Last read" + novelUrl + chapterUrl),
(tx, error) => console.log(error)
);
});
};
const setRead = () => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE ChapterTable SET `read` = 1 WHERE chapterUrl = ?",
[chapterUrl],
(tx, res) => console.log("Updated readStatus: " + novelUrl),
(tx, error) => console.log(error)
);
});
};
const checkIfInHistory = () => {
db.transaction((tx) => {
tx.executeSql(
"SELECT * FROM HistoryTable WHERE novelUrl=?",
[novelUrl],
(txObj, res) => {
if (res.rows.length === 0) {
console.log("Not In History");
setHistory();
} else {
console.log("In History");
updateHistory();
}
},
(txObj, error) => console.log("Error ", error)
);
});
};
// const updateLastRead = () => {
// db.transaction((tx) => {
// tx.executeSql(
// "UPDATE LibraryTable SET lastRead = ?, lastReadName = ?, unread = 0 WHERE novelUrl = ?",
// [chapter.nextChapter, chapterName, novelUrl],
// (tx, res) =>
// console.log("Set Last Read " + novelUrl + chapterUrl),
// (tx, error) => console.log(error)
// );
// });
// };
const checkIfDownloaded = () => {
db.transaction((tx) => {
tx.executeSql(
"SELECT * FROM DownloadsTable WHERE chapterUrl=? AND novelUrl=?",
[chapterUrl, novelUrl],
(txObj, res) => {
if (res.rows.length === 0) {
// console.log("Not Downloaded");
fetchChapterFromSource(
extensionId,
novelUrl,
chapterUrl
).then((res) => {
setChapter(res);
setLoading(false);
});
} else {
// console.log("Already Downloaded");
getChapterFromDB();
}
checkIfInHistory();
},
(txObj, error) => console.log("Error ", error)
);
});
};
const getChapterFromDB = () => {
db.transaction((tx) => {
tx.executeSql(
`SELECT * FROM DownloadsTable WHERE chapterUrl = ?`,
[chapterUrl],
(tx, results) => {
setChapter(results.rows.item(0));
setLoading(false);
},
(txObj, error) => console.log("Error ", error)
);
});
};
useEffect(() => {
checkIfDownloaded();
}, []);
const isCloseToBottom = ({
layoutMeasurement,
contentOffset,
contentSize,
}) => {
const paddingToBottom = 0;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
};
return (
<Provider>
<CollapsibleHeaderScrollView
headerContainerBackgroundColor={"rgba(0,0,0,0.4)"}
CollapsibleHeaderComponent={
<Appbar.Header
style={{
backgroundColor: "transparent",
elevation: 0,
}}
>
<Appbar.BackAction
onPress={() => {
// navigation.navigate("NovelItem", {
// novelUrl,
// extensionId,
// navigatingFrom: 0,
// });
navigation.goBack();
}}
color={"white"}
size={26}
style={{ marginRight: 0 }}
/>
<Appbar.Content
title={loading ? "Chapter" : chapter.chapterName}
titleStyle={{ color: theme.textColorPrimaryDark }}
/>
{!loading && (
<>
<Appbar.Action
icon="chevron-left"
size={26}
disabled={!chapter.prevChapter}
onPress={() => {
navigation.push("ChapterItem", {
chapterUrl: chapter.prevChapter,
extensionId,
novelUrl,
chapterName: chapter.chapterName,
});
}}
color={"white"}
/>
<Appbar.Action
icon="chevron-right"
size={26}
disabled={!chapter.nextChapter}
onPress={() => {
navigation.push("ChapterItem", {
chapterUrl: chapter.nextChapter,
extensionId,
novelUrl,
chapterName: chapter.chapterName,
});
}}
color={"white"}
/>
<Appbar.Action
icon="dots-vertical"
size={26}
onPress={() =>
_panel.show({ velocity: -1.5 })
}
color={"white"}
/>
</>
)}
</Appbar.Header>
}
headerHeight={100}
contentContainerStyle={[
styles.container,
readerTheme === 1 && {
backgroundColor: theme.colorDarkPrimary,
},
readerTheme === 2 && {
backgroundColor: "white",
},
readerTheme === 3 && {
backgroundColor: "#F4ECD8",
},
]}
onScroll={({ nativeEvent }) => {
// _panel.hide();
if (isCloseToBottom(nativeEvent)) {
// console.log("Scroll End Reached");
setRead();
}
}}
>
{/* <Button mode="contained" onPress={() => setHistory()}>
Set History
</Button> */}
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
size={50}
color={theme.colorAccentDark}
/>
</View>
) : (
<>
<Text
style={[
{
paddingVertical: 15,
fontSize: size,
},
readerTheme === 1
? { color: theme.textColorSecondaryDark }
: { color: "black" },
size === 16
? { lineHeight: 25 }
: size === 20
? { lineHeight: 28 }
: size === 12 && { lineHeight: 20 },
]}
>
{chapter.chapterText.trim()}
</Text>
</>
)}
<Portal>
<BottomSheet
bottomSheetRef={(c) => (_panel = c)}
setSize={setSize}
size={size}
setReaderTheme={setReaderTheme}
readerTheme={readerTheme}
/>
</Portal>
</CollapsibleHeaderScrollView>
</Provider>
);
};
export default ChapterItem;
const styles = StyleSheet.create({
container: {
flexGrow: 1,
paddingHorizontal: 10,
paddingVertical: 10,
},
});
+46 -75
View File
@@ -1,63 +1,55 @@
import React, { useState, useCallback } from "react";
import { useFocusEffect } from "@react-navigation/native";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar } from "../../components/common/Appbar";
import HistoryCard from "./components/HistoryCard";
import { connect } from "react-redux";
import EmptyView from "../../components/common/EmptyView";
import {
StyleSheet,
View,
Text,
FlatList,
ActivityIndicator,
} from "react-native";
import { CustomAppbar } from "../../components/common/Appbar";
import HistoryCard from "../../components/history/HistoryCard";
import { getHistoryFromDb, deleteChapterHistory } from "../../services/db";
import { useSelector } from "react-redux";
const History = ({ navigation }) => {
const [loading, setLoading] = useState(true);
const [history, setHistory] = useState();
const theme = useSelector((state) => state.themeReducer.theme);
const getHistory = async () => {
await getHistoryFromDb().then((res) => setHistory(res));
setLoading(false);
};
const deleteHistory = (novelId) => {
deleteChapterHistory(novelId);
getHistory();
};
getHistory,
deleteChapterFromHistory,
} from "../../redux/actions/history";
const History = ({
navigation,
theme,
history,
loading,
getHistory,
deleteChapterFromHistory,
}) => {
useFocusEffect(
useCallback(() => {
getHistory();
}, [])
);
const renderHistoryCard = ({ item }) => (
<HistoryCard
item={item}
deleteHistory={deleteChapterFromHistory}
navigation={navigation}
/>
);
return (
<>
<CustomAppbar title="History" />
<Appbar title="History" />
<View
style={[
styles.container,
{ backgroundColor: theme.colorDarkPrimaryDark },
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
contentContainerStyle={{ flex: 1 }}
contentContainerStyle={{ flexGrow: 1 }}
data={history}
keyExtractor={(item) => item.historyId.toString()}
renderItem={({ item }) => (
<HistoryCard
item={item}
deleteHistory={deleteHistory}
navigation={navigation}
/>
)}
keyExtractor={(item) => item.novelUrl.toString()}
renderItem={renderHistoryCard}
ListFooterComponent={
loading && (
<ActivityIndicator
@@ -68,34 +60,10 @@ const History = ({ navigation }) => {
}
ListEmptyComponent={
!loading && (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text
style={{
color: theme.textColorSecondaryDark,
fontSize: 45,
fontWeight: "bold",
}}
>
(˘・_・˘)
</Text>
<Text
style={{
color: theme.textColorSecondaryDark,
fontWeight: "bold",
marginTop: 10,
textAlign: "center",
paddingHorizontal: 30,
}}
>
Nothing read recently.
</Text>
</View>
<EmptyView
icon="(˘・_・˘)"
description="Nothing read recently"
/>
)
}
/>
@@ -104,17 +72,20 @@ const History = ({ navigation }) => {
);
};
export default History;
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
history: state.historyReducer.history,
loading: state.historyReducer.loading,
});
export default connect(mapStateToProps, {
getHistory,
deleteChapterFromHistory,
})(History);
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
},
historyCard: {
marginTop: 10,
borderRadius: 4,
flexDirection: "row",
alignItems: "center",
},
});
@@ -12,11 +12,8 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
return (
<TouchableRipple
style={[
styles.historyCard,
// { backgroundColor: theme.colorDarkPrimary },
]}
rippleColor={theme.rippleColorDark}
style={styles.historyCard}
rippleColor={theme.rippleColor}
borderless
onPress={() =>
navigation.navigate("NovelItem", {
@@ -24,31 +21,19 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
novelCover: item.novelCover,
extensionId: item.extensionId,
novelUrl: item.novelUrl,
navigatingFrom: 0,
})
}
>
<>
<Image
source={{ uri: item.novelCover }}
style={{
height: 80,
width: 57,
borderRadius: 4,
}}
style={styles.historyCover}
/>
<View
style={{
marginLeft: 15,
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
}}
>
<View style={styles.historyText}>
<View style={{ flex: 0.9 }}>
<Text
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
fontWeight: "bold",
fontSize: 16,
}}
@@ -58,7 +43,7 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
</Text>
<Text
style={{
color: theme.textColorSecondaryDark,
color: theme.textColorSecondary,
marginTop: 2,
}}
numberOfLines={1}
@@ -77,7 +62,7 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
<IconButton
icon="delete-outline"
size={22}
color={theme.textColorPrimaryDark}
color={theme.textColorPrimary}
style={{
marginRight: 0,
}}
@@ -86,7 +71,7 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
<IconButton
icon="play"
size={24}
color={theme.textColorPrimaryDark}
color={theme.textColorPrimary}
style={{ marginRight: 10 }}
onPress={() =>
navigation.navigate("ChapterItem", {
@@ -110,13 +95,20 @@ export default HistoryCard;
const styles = StyleSheet.create({
historyCard: {
// backgroundColor: theme.colorDarkPrimary,
// paddingVertical: 10,
// marginVertical: 5,
// paddingHorizontal: 20,
marginBottom: 15,
borderRadius: 4,
flexDirection: "row",
alignItems: "center",
},
historyCover: {
height: 80,
width: 57,
borderRadius: 4,
},
historyText: {
marginLeft: 15,
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
},
});
-220
View File
@@ -1,220 +0,0 @@
import React, { useState, useCallback } from "react";
import {
StyleSheet,
Text,
View,
TextInput,
RefreshControl,
ActivityIndicator,
ToastAndroid,
} from "react-native";
import { Appbar } from "react-native-paper";
import { FlatList } from "react-native-gesture-handler";
import { useFocusEffect } from "@react-navigation/native";
import NovelCover from "../components/common/NovelCover";
import { useSelector } from "react-redux";
import { getLibraryFromDb, searchInLibrary } from "../services/db";
import { getLibraryNovels } from "../redux/actions/library";
const LibraryScreen = ({ navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const [novels, setNovels] = useState();
const [searchBar, setSearchBar] = useState(false);
const [searchText, setSearchText] = useState("");
const getLibrary = async () => {
await getLibraryFromDb().then((res) => {
setNovels(res);
});
setLoading(false);
setRefreshing(false);
};
const searchNovel = async (searchText) => {
await searchInLibrary(searchText).then((res) => setNovels(res));
setRefreshing(false);
};
useFocusEffect(
useCallback(() => {
setSearchBar(false);
getLibrary();
}, [])
);
const onRefresh = () => {
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
setRefreshing(true);
getLibrary();
};
return (
<>
<Appbar.Header style={{ backgroundColor: theme.colorDarkPrimary }}>
{!searchBar ? (
<>
<Appbar.Content
title="Library"
titleStyle={{ color: theme.textColorPrimaryDark }}
/>
<Appbar.Action
icon="magnify"
onPress={() => setSearchBar(true)}
color={theme.textColorPrimaryDark}
/>
<Appbar.Action
color={theme.textColorPrimaryDark}
icon="filter-variant"
disabled
// onPress={() => _panel.show({ velocity: -1.5 })}
/>
</>
) : (
<>
<Appbar.BackAction
onPress={() => {
setSearchBar(false);
setSearchText("");
getLibrary();
}}
color={theme.textColorPrimaryDark}
size={24}
/>
<TextInput
placeholder="Search..."
defaultValue={searchText}
style={{
fontSize: 18,
flex: 1,
color: theme.textColorPrimaryDark,
marginLeft: 15,
}}
autoFocus
placeholderTextColor={theme.textColorHintDark}
blurOnSubmit={true}
onChangeText={(text) => {
setSearchText(text);
searchNovel(text);
}}
/>
{searchText !== "" && (
<Appbar.Action
icon="close"
onPress={() => {
setSearchText("");
// getLibrary();
}}
color={theme.textColorPrimaryDark}
/>
)}
</>
)}
</Appbar.Header>
<View
style={[
styles.container,
{ backgroundColor: theme.colorDarkPrimaryDark },
]}
>
<FlatList
contentContainerStyle={{ flex: 1 }}
numColumns={3}
data={novels}
showsVerticalScrollIndicator={false}
keyExtractor={(item) => item.novelUrl}
renderItem={({ item }) => (
<NovelCover
item={item}
onPress={() =>
navigation.navigate("NovelItem", {
...item,
navigatingFrom: 1,
})
}
/>
)}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["white"]}
progressBackgroundColor={theme.colorAccentDark}
/>
}
ListEmptyComponent={
!loading ? (
!searchBar ? (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text
style={{
color: theme.textColorSecondaryDark,
fontSize: 45,
fontWeight: "bold",
}}
>
Σ(ಠ_ಠ)
</Text>
<Text
style={{
color: theme.textColorSecondaryDark,
fontWeight: "bold",
marginTop: 10,
textAlign: "center",
paddingHorizontal: 30,
}}
>
Your library is empty. Add series to
your library from Browse.
</Text>
</View>
) : (
searchBar &&
searchText !== "" && (
<Text
style={{
color: theme.colorAccentDark,
fontWeight: "bold",
marginTop: 10,
textAlign: "center",
paddingHorizontal: 30,
}}
>
{`"${searchText}" not in library`}
</Text>
)
)
) : (
<ActivityIndicator
size="small"
color={theme.colorAccentDark}
/>
)
}
/>
</View>
</>
);
};
export default LibraryScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 4,
},
});
+152
View File
@@ -0,0 +1,152 @@
import React, { useState, useCallback } from "react";
import {
StyleSheet,
Text,
View,
RefreshControl,
ActivityIndicator,
ToastAndroid,
} from "react-native";
import { FlatList } from "react-native-gesture-handler";
import { useFocusEffect } from "@react-navigation/native";
import { connect } from "react-redux";
import NovelCover from "../../components/common/NovelCover";
import EmptyView from "../../components/common/EmptyView";
import {
getLibraryNovels,
searchLibraryNovels,
} from "../../redux/actions/library";
import { SearchAppbar } from "../../components/common/Appbar";
import { setNovel } from "../../redux/actions/novel";
const LibraryScreen = ({
navigation,
theme,
novels,
loading,
getLibraryNovels,
searchLibraryNovels,
setNovel,
itemsPerRow,
}) => {
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
useFocusEffect(
useCallback(() => {
setSearchText("");
getLibraryNovels();
}, [])
);
/**
* TODO: fix refreshing
*/
const onRefresh = () => {
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
setRefreshing(true);
getLibraryNovels();
setRefreshing(false);
};
const redirectToNovel = (item) => {
navigation.navigate("NovelItem", item);
setNovel(item);
};
return (
<>
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<SearchAppbar
screen="Library"
placeholder="Search Library"
getNovels={getLibraryNovels}
getSearchResults={searchLibraryNovels}
searchText={searchText}
setSearchText={setSearchText}
/>
{loading ? (
<ActivityIndicator
size="small"
color={theme.colorAccentDark}
/>
) : (
<FlatList
contentContainerStyle={{
flexGrow: 1,
paddingBottom: 8,
}}
numColumns={itemsPerRow}
key={itemsPerRow}
data={novels}
keyExtractor={(item) => item.novelUrl}
renderItem={({ item }) => (
<NovelCover
item={item}
onPress={() => redirectToNovel(item)}
/>
)}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["white"]}
progressBackgroundColor={theme.colorAccentDark}
/>
}
ListEmptyComponent={
searchText !== "" ? (
<Text
style={[
styles.emptySearch,
{ color: theme.colorAccentDark },
]}
>
{`"${searchText}" not in library`}
</Text>
) : (
<EmptyView
icon="Σ(ಠ_ಠ)"
description="Your library is empty. Add series to your library from Browse."
/>
)
}
/>
)}
</View>
</>
);
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
itemsPerRow: state.settingsReducer.itemsPerRow,
novels: state.libraryReducer.novels,
loading: state.libraryReducer.loading,
});
export default connect(mapStateToProps, {
getLibraryNovels,
searchLibraryNovels,
setNovel,
})(LibraryScreen);
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 4,
},
emptySearch: {
marginTop: 8,
textAlign: "center",
paddingHorizontal: 16,
},
});
+18 -21
View File
@@ -2,7 +2,7 @@ import React from "react";
import * as Linking from "expo-linking";
import { List, Divider } from "react-native-paper";
import { CustomAppbar } from "../../components/common/Appbar";
import { Appbar } from "../../components/common/Appbar";
import { useSelector } from "react-redux";
@@ -11,46 +11,43 @@ const AboutScreen = ({ navigation }) => {
return (
<>
<CustomAppbar
title="About"
onBackAction={() => navigation.goBack()}
/>
<Appbar title="About" onBackAction={() => navigation.goBack()} />
<List.Section
style={{
flex: 1,
marginTop: 0,
backgroundColor: theme.colorDarkPrimaryDark,
backgroundColor: theme.colorPrimaryDark,
marginBottom: 0,
}}
>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
title="Version"
descriptionStyle={{ color: theme.textColorSecondaryDark }}
description="Stable 1.0.17"
descriptionStyle={{ color: theme.textColorSecondary }}
description="Stable 1.0.9"
/>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
title="Build Time"
descriptionStyle={{ color: theme.textColorSecondaryDark }}
description="09-03-21 09:46 AM"
descriptionStyle={{ color: theme.textColorSecondary }}
description="21-03-21 11:10 AM"
/>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
title="What's new"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader/commits/master"
"https://github.com/rajarsheechatterjee/lnreader/commits/main"
)
}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
/>
<Divider />
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
descriptionStyle={{ color: theme.textColorSecondaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
descriptionStyle={{ color: theme.textColorSecondary }}
title="Github"
description="https://github.com/rajarsheechatterjee/lnreader"
onPress={() =>
@@ -58,11 +55,11 @@ const AboutScreen = ({ navigation }) => {
"https://github.com/rajarsheechatterjee/lnreader"
)
}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
/>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
descriptionStyle={{ color: theme.textColorSecondaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
descriptionStyle={{ color: theme.textColorSecondary }}
title="Extensions"
description="https://github.com/rajarsheechatterjee/lnreader-extensions"
descriptionNumberOfLines={1}
@@ -71,7 +68,7 @@ const AboutScreen = ({ navigation }) => {
"https://github.com/rajarsheechatterjee/lnreader-extensions"
)
}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
/>
</List.Section>
</>
+5 -5
View File
@@ -3,7 +3,7 @@ import React from "react";
import { StyleSheet } from "react-native";
import { List } from "react-native-paper";
import { CustomAppbar } from "../../components/common/Appbar";
import { Appbar } from "../../components/common/Appbar";
import { useSelector } from "react-redux";
@@ -12,17 +12,17 @@ const MoreScreen = ({ navigation }) => {
return (
<>
<CustomAppbar title="More" />
<Appbar title="More" />
<List.Section
style={{
flex: 1,
backgroundColor: theme.colorDarkPrimaryDark,
backgroundColor: theme.colorPrimaryDark,
marginTop: 0,
marginBottom: 0,
}}
>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
title="Settings"
left={() => (
<List.Icon
@@ -33,7 +33,7 @@ const MoreScreen = ({ navigation }) => {
onPress={() => navigation.navigate("Settings")}
/>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
title="About"
left={() => (
<List.Icon
+182 -114
View File
@@ -1,63 +1,80 @@
import React, { useState } from "react";
import { View, ToastAndroid } from "react-native";
import { View, Text } from "react-native";
import { setStatusBarStyle } from "expo-status-bar";
import { List } from "react-native-paper";
import { List, Modal, Portal } from "react-native-paper";
import InputSpinner from "react-native-input-spinner";
import { connect } from "react-redux";
import { switchTheme } from "../../redux/actions/theme";
import {
amoledDarkTheme,
darkTheme,
midnightDuskTheme,
lightTheme,
} from "../../theme/theme";
import { setDisplayMode, setItemsPerRow } from "../../redux/actions/settings";
import { CustomAppbar } from "../../components/common/Appbar";
import { Appbar } from "../../components/common/Appbar";
import {
DisplayCheckbox,
ThemeCheckbox,
} from "../../components/settings/Checkbox";
import {
setAppTheme,
getAppTheme,
getDisplayMode,
setDisplayMode,
} from "../../services/asyncStorage";
import {
deleteHistory,
deleteNovelsNotInLibrary,
deleteDatabase,
} from "../../services/db";
const SettingsScreen = ({ navigation, theme, switchTheme }) => {
const desciptionStyles = {
color: theme.textColorSecondaryDark,
const SettingsScreen = ({
navigation,
theme,
themeCode,
displayMode,
itemsPerRow,
switchTheme,
setDisplayMode,
setItemsPerRow,
}) => {
const display = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
const titleStyles = { color: theme.textColorPrimaryDark };
const themes = [
{ themeCode: 0, label: "AMOLED Dark Theme", statusBar: "light" },
{ themeCode: 1, label: "Light Theme", statusBar: "dark" },
{ themeCode: 2, label: "Dark Theme", statusBar: "light" },
{ themeCode: 3, label: "Midnight Dusk Theme", statusBar: "light" },
];
const [displayMode, setDisplay] = useState("compact");
const [applicationTheme, setApplicationTheme] = useState("amoledDarkTheme");
const desciptionStyles = {
color: theme.textColorSecondary,
};
getAppTheme().then((res) => setApplicationTheme(res));
const titleStyles = { color: theme.textColorPrimary };
getDisplayMode().then((res) => setDisplay(res));
/**
* Display Mode Modal
*/
const [displayModalVisible, setDisplayModalVisible] = useState(false);
const showDisplayModal = () => setDisplayModalVisible(true);
const hideDisplayModal = () => setDisplayModalVisible(false);
/**
* Change Theme Modal
*/
const [themeModalVisible, setthemeModalVisible] = useState(false);
const showthemeModal = () => setthemeModalVisible(true);
const hidethemeModal = () => setthemeModalVisible(false);
const [itemsModalVisible, setitemsModalVisible] = useState(false);
const showitemsModal = () => setitemsModalVisible(true);
const hideitemsModal = () => setitemsModalVisible(false);
return (
<>
<CustomAppbar
title="Settings"
onBackAction={() => navigation.goBack()}
/>
<View
style={{ flex: 1, backgroundColor: theme.colorDarkPrimaryDark }}
>
<Appbar title="Settings" onBackAction={() => navigation.goBack()} />
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<List.Section
style={{
flex: 1,
backgroundColor: theme.colorDarkPrimaryDark,
backgroundColor: theme.colorPrimaryDark,
}}
>
<List.Subheader
@@ -74,7 +91,7 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
descriptionStyle={desciptionStyles}
description="Delete history for novels not in your library"
onPress={() => deleteNovelsNotInLibrary()}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
/>
<List.Item
titleStyle={titleStyles}
@@ -82,7 +99,7 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
descriptionStyle={desciptionStyles}
description="Delete reading history for all novels"
onPress={() => deleteHistory()}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
/>
<List.Subheader
style={{
@@ -92,26 +109,95 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
>
Display
</List.Subheader>
<View>
<DisplayCheckbox
value="comfortable"
displayMode={displayMode}
onPress={() => {
setDisplay("comfortable");
setDisplayMode("comfortable");
<List.Item
titleStyle={titleStyles}
title="Display Mode"
descriptionStyle={desciptionStyles}
description={display[displayMode]}
onPress={showDisplayModal}
rippleColor={theme.rippleColor}
/>
<Portal>
<Modal
visible={displayModalVisible}
onDismiss={hideDisplayModal}
contentContainerStyle={{
backgroundColor: theme.colorPrimaryDark,
padding: 20,
margin: 20,
borderRadius: 6,
}}
/>
</View>
<View>
<DisplayCheckbox
value="compact"
displayMode={displayMode}
onPress={() => {
setDisplay("compact");
setDisplayMode("compact");
>
<DisplayCheckbox
value={1}
displayMode={displayMode}
onPress={() => setDisplayMode(1)}
/>
<DisplayCheckbox
value={0}
displayMode={displayMode}
onPress={() => setDisplayMode(0)}
/>
<DisplayCheckbox
value={2}
displayMode={displayMode}
onPress={() => setDisplayMode(2)}
/>
</Modal>
</Portal>
<List.Item
titleStyle={titleStyles}
title="Items per row in library"
descriptionStyle={desciptionStyles}
description={itemsPerRow}
onPress={showitemsModal}
rippleColor={theme.rippleColor}
/>
<Portal>
<Modal
visible={itemsModalVisible}
onDismiss={hideitemsModal}
contentContainerStyle={{
backgroundColor: theme.colorPrimaryDark,
padding: 20,
margin: 20,
borderRadius: 6,
}}
/>
</View>
>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 18,
marginBottom: 10,
}}
>
Items per row
</Text>
<InputSpinner
style={{
paddingHorizontal: 16,
paddingVertical: 16,
elevation: 0,
}}
max={10}
min={1}
skin="paper"
background={theme.colorPrimaryDark}
colorMax={theme.colorAccentDark}
colorLeft={theme.colorAccentDark}
colorRight={theme.colorAccentDark}
buttonTextColor={theme.textColorPrimary}
colorPress={"white"}
colorMin={theme.colorAccentDark}
value={itemsPerRow}
onChange={(num) => setItemsPerRow(num)}
inputStyle={{
color: theme.textColorPrimary,
}}
/>
</Modal>
</Portal>
{/* <List.Subheader
style={{
color: theme.colorAccentDark,
@@ -125,10 +211,9 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
title="Delete database"
descriptionStyle={desciptionStyles}
description="Delete entire database"
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
onPress={() => deleteDatabase()}
/> */}
<List.Subheader
style={{
color: theme.colorAccentDark,
@@ -137,66 +222,42 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
>
Theme
</List.Subheader>
<View>
<ThemeCheckbox
label="Light Theme"
checked={
applicationTheme === "lightTheme" ? true : false
}
onPress={() => {
switchTheme(lightTheme);
setApplicationTheme(`lightTheme`);
setAppTheme("lightTheme");
setStatusBarStyle("dark");
<List.Item
titleStyle={titleStyles}
title="Theme"
descriptionStyle={desciptionStyles}
description={themes[themeCode].label}
onPress={showthemeModal}
rippleColor={theme.rippleColor}
/>
<Portal>
<Modal
visible={themeModalVisible}
onDismiss={hidethemeModal}
contentContainerStyle={{
backgroundColor: theme.colorPrimaryDark,
padding: 20,
margin: 20,
borderRadius: 6,
}}
/>
</View>
<View>
<ThemeCheckbox
label="Dark Theme"
checked={
applicationTheme === "darkTheme" ? true : false
}
onPress={() => {
switchTheme(darkTheme);
setApplicationTheme("darkTheme");
setAppTheme("darkTheme");
setStatusBarStyle("light");
}}
/>
<View>
<ThemeCheckbox
label="Amoled Dark Theme"
checked={
applicationTheme === "amoledDarkTheme"
? true
: false
}
onPress={() => {
switchTheme(amoledDarkTheme);
setApplicationTheme(`amoledDarkTheme`);
setAppTheme("amoledDarkTheme");
setStatusBarStyle("light");
}}
/>
</View>
<View>
<ThemeCheckbox
label="Midnight Dusk Theme"
checked={
applicationTheme === "midnightDuskTheme"
? true
: false
}
onPress={() => {
switchTheme(midnightDuskTheme);
setApplicationTheme("midnightDuskTheme");
setAppTheme("midnightDuskTheme");
setStatusBarStyle("light");
}}
/>
</View>
</View>
>
{themes.map((item, index) => (
<ThemeCheckbox
key={index}
label={themes[item.themeCode].label}
checked={
themeCode === item.themeCode
? true
: false
}
onPress={() => {
switchTheme(item.themeCode);
setStatusBarStyle(item.statusBar);
}}
/>
))}
</Modal>
</Portal>
</List.Section>
</View>
</>
@@ -204,7 +265,14 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
};
const mapStateToProps = (state) => ({
themeCode: state.themeReducer.themeCode,
theme: state.themeReducer.theme,
displayMode: state.settingsReducer.displayMode,
itemsPerRow: state.settingsReducer.itemsPerRow,
});
export default connect(mapStateToProps, { switchTheme })(SettingsScreen);
export default connect(mapStateToProps, {
switchTheme,
setDisplayMode,
setItemsPerRow,
})(SettingsScreen);
+160
View File
@@ -0,0 +1,160 @@
import React, { useState, useEffect, useRef } from "react";
import {
StyleSheet,
View,
FlatList,
RefreshControl,
ToastAndroid,
} from "react-native";
import { Provider, Portal } from "react-native-paper";
import ChapterCard from "./components/ChapterCard";
import NovelInfoHeader from "./components/NovelHeader";
import { BottomSheet } from "./components/BottomSheet";
import { downloadOrDeleteChapter } from "../../services/db";
import { connect } from "react-redux";
import {
getNovel,
insertNovelInLibrary,
sortAndFilterChapters,
} from "../../redux/actions/novel";
const Novel = ({
route,
theme,
novel,
chapters,
loading,
getNovel,
fetching,
insertNovelInLibrary,
sortAndFilterChapters,
}) => {
const {
extensionId,
novelUrl,
novelName,
novelCover,
libraryStatus,
} = route.params;
// const [downloading, setDownloading] = useState({
// downloading: false,
// chapterUrl: "",
// });
let _panel = useRef(null); // Bottomsheet ref
/**
* Download chapter from source
*
* If downloaded then delete from db
*/
// const downloadChapter = (
// downloadStatus,
// extensionId,
// novelUrl,
// chapterUrl,
// chapterName
// ) => {
// setDownloading({ downloading: true, chapterUrl: chapterUrl });
// downloadOrDeleteChapter(
// downloadStatus,
// extensionId,
// novelUrl,
// chapterUrl
// ).then((res) => {
// setDownloading({ downloading: false });
// ToastAndroid.show(
// !downloadStatus
// ? `Downloaded ${chapterName}`
// : `Deleted ${chapterName}`,
// ToastAndroid.SHORT
// );
// });
// };
useEffect(() => {
getNovel(libraryStatus, extensionId, novelUrl);
}, []);
const renderChapterCard = ({ item }) => (
<ChapterCard
novelUrl={novelUrl}
extensionId={extensionId}
chapter={item}
// downloadChapter={downloadChapter}
// downloading={downloading}
/>
);
return (
<Provider>
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
data={!loading && chapters}
keyExtractor={(item) => item.chapterUrl}
removeClippedSubviews={true}
maxToRenderPerBatch={5}
windowSize={15}
initialNumToRender={7}
renderItem={renderChapterCard}
ListHeaderComponent={
<NovelInfoHeader
theme={theme}
item={{ novelName, novelCover }}
novel={novel}
noOfChapters={chapters.length}
insertNovelInLibrary={insertNovelInLibrary}
loading={loading}
bottomSheetRef={_panel}
/>
}
refreshControl={
<RefreshControl
refreshing={fetching}
// onRefresh={onRefresh}
colors={[theme.textColorPrimary]}
progressBackgroundColor={theme.colorPrimary}
/>
}
/>
<Portal>
<BottomSheet
novelUrl={novelUrl}
bottomSheetRef={_panel}
sortAndFilterChapters={sortAndFilterChapters}
/>
</Portal>
</View>
</Provider>
);
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
novel: state.novelReducer.novel,
chapters: state.novelReducer.chapters,
loading: state.novelReducer.loading,
fetching: state.novelReducer.fetching,
});
export default connect(mapStateToProps, {
getNovel,
insertNovelInLibrary,
sortAndFilterChapters,
})(Novel);
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
@@ -1,32 +1,29 @@
import React from "react";
import React, { useState } from "react";
import { StyleSheet, View, Text } from "react-native";
import { List, RadioButton, Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import { useSelector } from "react-redux";
const BottomSheetHandle = () => (
<View
style={{
backgroundColor: "rgba(255,255,255,0.25)",
height: 4,
width: 50,
borderRadius: 50,
top: 10,
alignSelf: "center",
}}
/>
);
export const BottomSheet = ({
bottomSheetRef,
sort,
filter,
sortChapters,
filterChapters,
sortAndFilterChapters,
novelUrl,
}) => {
const checkBoxStyle = { flexDirection: "row", alignItems: "center" };
const [chapterFlags, setChapterFlags] = useState({ sort: "", filter: "" });
const sortChapters = (sort) => {
setChapterFlags({ ...chapterFlags, sort: sort });
sortAndFilterChapters(novelUrl, chapterFlags.filter, sort);
};
const filterChapters = (filter) => {
setChapterFlags({ ...chapterFlags, filter: filter });
sortAndFilterChapters(novelUrl, filter, chapterFlags.sort);
};
const theme = useSelector((state) => state.themeReducer.theme);
return (
@@ -39,14 +36,14 @@ export const BottomSheet = ({
style={[
styles.contentContainer,
{
backgroundColor: theme.colorDarkPrimary,
backgroundColor: theme.colorPrimary,
},
]}
>
<BottomSheetHandle />
<View style={styles.bottomSheetHandle} />
<List.Subheader
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
fontSize: 20,
}}
>
@@ -55,8 +52,7 @@ export const BottomSheet = ({
<View>
<RadioButton.Group
onValueChange={(newValue) => sortChapters(newValue)}
value={sort}
c
value={chapterFlags.sort}
>
<View
style={{
@@ -77,7 +73,7 @@ export const BottomSheet = ({
/>
<Text
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
}}
>
Newest to oldest
@@ -96,7 +92,7 @@ export const BottomSheet = ({
/>
<Text
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
}}
>
Oldest to newest
@@ -108,7 +104,7 @@ export const BottomSheet = ({
<List.Subheader
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
fontSize: 20,
paddingVertical: 10,
}}
@@ -117,45 +113,51 @@ export const BottomSheet = ({
</List.Subheader>
<View style={checkBoxStyle}>
<Checkbox.Item
status={filter === "" ? "checked" : "unchecked"}
status={
chapterFlags.filter === "" ? "checked" : "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => filterChapters("")}
/>
<Text style={{ color: theme.textColorPrimaryDark }}>
<Text style={{ color: theme.textColorPrimary }}>
Show all
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND `read`=1" ? "checked" : "unchecked"
chapterFlags.filter === "AND `read`=1"
? "checked"
: "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => filterChapters("AND `read`=1")}
/>
<Text style={{ color: theme.textColorPrimaryDark }}>
<Text style={{ color: theme.textColorPrimary }}>
Show read chapters
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND `read`=0" ? "checked" : "unchecked"
chapterFlags.filter === "AND `read`=0"
? "checked"
: "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => filterChapters("AND `read`=0")}
/>
<Text style={{ color: theme.textColorPrimaryDark }}>
<Text style={{ color: theme.textColorPrimary }}>
Show unread chapters
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND downloaded=1"
chapterFlags.filter === "AND downloaded=1"
? "checked"
: "unchecked"
}
@@ -163,7 +165,7 @@ export const BottomSheet = ({
color={theme.colorAccentDark}
onPress={() => filterChapters("AND downloaded=1")}
/>
<Text style={{ color: theme.textColorPrimaryDark }}>
<Text style={{ color: theme.textColorPrimary }}>
Show downloaded chapters
</Text>
</View>
@@ -175,5 +177,15 @@ export const BottomSheet = ({
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
bottomSheetHandle: {
backgroundColor: "rgba(255,255,255,0.25)",
height: 4,
width: 50,
borderRadius: 50,
top: 10,
alignSelf: "center",
},
});
@@ -1,33 +1,32 @@
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 { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
import { deleteChapter, downloadChapter } from "../../../redux/actions/novel";
import { connect } from "react-redux";
const ChapterCard = ({
navigation,
novelUrl,
chapter,
downloadChapter,
extensionId,
theme,
downloading,
downloadChapter,
deleteChapter,
}) => {
const [downloaded, setDownloaded] = useState(chapter.downloaded);
const navigation = useNavigation();
const theme = useSelector((state) => state.themeReducer.theme);
const isDownloading = () =>
downloading.some(
(obj) =>
obj.chapterUrl === chapter.chapterUrl &&
obj.novelUrl === novelUrl
);
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,
@@ -36,14 +35,14 @@ const ChapterCard = ({
chapterName: chapter.chapterName,
})
}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
>
<>
<View>
<Text
style={[
{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
},
chapter.read === 1 && {
color: theme.textColorHintDark,
@@ -57,7 +56,7 @@ const ChapterCard = ({
<Text
style={[
{
color: theme.textColorSecondaryDark,
color: theme.textColorSecondary,
marginTop: 5,
fontSize: 12,
},
@@ -72,8 +71,7 @@ const ChapterCard = ({
)}
</View>
<View>
{downloading.downloading &&
downloading.chapterUrl === chapter.chapterUrl ? (
{isDownloading() && !chapter.downloaded ? (
<ActivityIndicator
color={theme.textColorHintDark}
size={25}
@@ -82,26 +80,33 @@ const ChapterCard = ({
) : (
<IconButton
icon={
downloaded
chapter.downloaded
? "check-circle"
: "arrow-down-circle-outline"
}
animated
color={
downloaded
? theme.textColorPrimaryDark
chapter.downloaded
? theme.textColorPrimary
: theme.textColorHintDark
}
size={25}
onPress={() => {
downloadChapter(
downloaded ? 1 : 0,
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName
);
setDownloaded(!downloaded);
if (!chapter.downloaded) {
downloadChapter(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName
);
} else {
deleteChapter(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName
);
}
}}
style={{ margin: 2 }}
/>
@@ -112,4 +117,21 @@ const ChapterCard = ({
);
};
export default ChapterCard;
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
downloading: state.novelReducer.downloading,
});
export default connect(mapStateToProps, { downloadChapter, deleteChapter })(
ChapterCard
);
const styles = StyleSheet.create({
chapterCardContainer: {
paddingHorizontal: 15,
paddingVertical: 7,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
});
@@ -15,21 +15,20 @@ import * as WebBrowser from "expo-web-browser";
import { LinearGradient } from "expo-linear-gradient";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { connect } from "react-redux";
const NovelInfoHeader = ({
item,
novel,
noOfChapters,
insertNovelInLib,
insertNovelInLibrary,
loading,
navigatingFrom,
libraryStatus,
bottomSheetRef,
theme,
}) => {
const navigation = useNavigation();
const [more, setMore] = useState(navigatingFrom === 2 ? true : false);
const [more, setMore] = useState(
!loading && novel.libraryStatus !== 1 ? true : false
);
const renderGenreChip = ({ item }) => (
<Text
@@ -38,7 +37,7 @@ const NovelInfoHeader = ({
{
color: theme.colorAccentDark,
borderColor: theme.colorAccentDark,
backgroundColor: theme.colorDarkPrimary,
backgroundColor: theme.colorPrimary,
},
]}
>
@@ -47,23 +46,19 @@ const NovelInfoHeader = ({
);
return (
<>
<View style={{ flexGrow: 1 }}>
<ImageBackground
source={{
uri: item.novelCover,
}}
source={{ uri: item.novelCover }}
style={styles.background}
>
<LinearGradient
colors={["rgba(0,0,0,0.2)", theme.colorDarkPrimaryDark]}
colors={["rgba(0,0,0,0.2)", theme.colorPrimaryDark]}
style={styles.linearGradient}
>
<View style={styles.detailsContainer}>
<View>
<Image
source={{
uri: item.novelCover,
}}
source={{ uri: item.novelCover }}
style={styles.logo}
/>
</View>
@@ -72,9 +67,7 @@ const NovelInfoHeader = ({
numberOfLines={2}
style={[
styles.name,
{
color: theme.textColorPrimaryDark,
},
{ color: theme.textColorPrimary },
]}
>
{item.novelName}
@@ -83,22 +76,19 @@ const NovelInfoHeader = ({
<>
<Text
style={{
color: theme.textColorSecondaryDark,
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
fontWeight: "bold",
}}
numberOfLines={2}
>
{novel["Author(s)"] &&
novel["Author(s)"].replace(
",",
", "
)}
{novel["Author(s)"].replace(",", ", ")}
</Text>
<Text
style={{
color: theme.textColorSecondaryDark,
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
}}
@@ -108,8 +98,7 @@ const NovelInfoHeader = ({
</Text>
<Text
style={{
color: theme.textColorSecondaryDark,
// marginVertical: 3,
color: theme.textColorSecondary,
fontSize: 14,
}}
numberOfLines={1}
@@ -124,19 +113,13 @@ const NovelInfoHeader = ({
</ImageBackground>
{!loading && (
<>
<View
style={{
flex: 1,
flexDirection: "row",
alignItems: "center",
}}
>
<View style={styles.buttonsContainer}>
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={
libraryStatus
novel.libraryStatus
? "heart"
: "heart-outline"
}
@@ -144,31 +127,24 @@ const NovelInfoHeader = ({
color={theme.colorAccentDark}
/>
)}
onPress={() => insertNovelInLib()}
onPress={() =>
insertNovelInLibrary(
novel.libraryStatus,
novel.novelUrl
)
}
style={[
{
backgroundColor: theme.colorDarkPrimaryDark,
marginRight: 2,
// borderColor: "rgba(255,255,255,0.121)",
// borderWidth: 1,
justifyContent: "center",
height: 30,
alignItems: "center",
marginLeft: 15,
paddingHorizontal: 3,
shadowOpacity: 0,
shadowOffset: 0,
},
// libraryStatus && {
// backgroundColor: "rgba(41,121,255,0.38)",
// },
{ backgroundColor: theme.colorPrimaryDark },
styles.toggleFavourite,
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
}}
>
{libraryStatus ? "In Library" : "Add to library"}
{novel.libraryStatus
? "In Library"
: "Add to library"}
</Chip>
<IconButton
@@ -192,28 +168,25 @@ const NovelInfoHeader = ({
size={21}
/>
</View>
{novel.novelSummary !== "" && (
<View
style={{
paddingHorizontal: 15,
marginBottom: 10,
// marginTop: 5,
paddingHorizontal: 16,
marginBottom: 8,
}}
>
<Text
style={{
color: theme.textColorPrimaryDark,
marginTop: 5,
paddingVertical: 5,
fontSize: 15,
fontWeight: "bold",
}}
style={[
{ color: theme.textColorPrimary },
styles.summaryHeader,
]}
>
About
</Text>
<Text
style={{
color: theme.textColorSecondaryDark,
color: theme.textColorSecondary,
lineHeight: 20,
}}
numberOfLines={more ? 100 : 2}
@@ -223,15 +196,13 @@ const NovelInfoHeader = ({
{novel.novelSummary}
</Text>
<Text
style={{
color: theme.colorAccentDark,
fontWeight: "bold",
position: "absolute",
bottom: 0,
right: 15,
backgroundColor: theme.colorDarkPrimaryDark,
paddingLeft: 5,
}}
style={[
{
color: theme.colorAccentDark,
backgroundColor: theme.colorPrimaryDark,
},
styles.moreBtn,
]}
onPress={() => setMore(!more)}
>
{more ? "Less " : "More "}
@@ -243,12 +214,9 @@ const NovelInfoHeader = ({
style={
novel.novelSummary === ""
? { marginTop: 20 }
: { marginTop: 10 }
: { marginTop: 12 }
}
contentContainerStyle={{
paddingHorizontal: 15,
paddingBottom: 15,
}}
contentContainerStyle={styles.genreContainer}
horizontal
data={novel["Genre(s)"] && novel["Genre(s)"].split(",")}
keyExtractor={(item) => item}
@@ -258,12 +226,10 @@ const NovelInfoHeader = ({
<Button
color="white"
style={{
backgroundColor: theme.colorAccentDark,
marginHorizontal: 15,
marginTop: 8,
marginBottom: 15,
}}
style={[
{ backgroundColor: theme.colorAccentDark },
styles.startButton,
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
onPress={() =>
@@ -275,73 +241,58 @@ const NovelInfoHeader = ({
})
}
>
{novel.unread && novel.unread === 1
{novel.unread === 1
? `Start reading ${novel.lastReadName}`
: `Continue reading ${novel.lastReadName}`}
</Button>
<TouchableRipple
style={{
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingRight: 12.5,
}}
style={styles.bottomsheet}
onPress={() =>
bottomSheetRef.current.show({ velocity: -1.5 })
}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
>
<>
<Text
style={{
color: theme.textColorPrimaryDark,
paddingHorizontal: 15,
paddingVertical: 5,
fontSize: 16,
fontWeight: "bold",
}}
style={[
{ color: theme.textColorPrimary },
styles.chapters,
]}
>
{noOfChapters + " Chapters "}
{noOfChapters + " Chapters"}
</Text>
<IconButton
icon="filter-variant"
color={theme.textColorPrimaryDark}
color={theme.textColorPrimary}
size={24}
/>
</>
</TouchableRipple>
</>
)}
</>
</View>
);
};
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
});
export default connect(mapStateToProps)(NovelInfoHeader);
export default NovelInfoHeader;
const styles = StyleSheet.create({
nameContainer: {
flex: 1,
marginHorizontal: 15,
paddingTop: 10,
marginHorizontal: 16,
paddingTop: 8,
},
background: {
height: 285,
// backgroundColor: theme.colorDarkPrimaryDark,
},
linearGradient: {
// flex: 1,
height: 286,
},
detailsContainer: {
flex: 1,
flexDirection: "row",
margin: 15,
margin: 16,
paddingTop: 90,
},
logo: {
@@ -364,4 +315,52 @@ const styles = StyleSheet.create({
fontWeight: "bold",
fontSize: 18,
},
buttonsContainer: {
flexDirection: "row",
alignItems: "center",
},
toggleFavourite: {
height: 32,
justifyContent: "center",
alignItems: "center",
marginLeft: 16,
marginRight: 4,
paddingHorizontal: 4,
borderWidth: 0,
elevation: 0,
},
chapters: {
paddingHorizontal: 16,
paddingVertical: 4,
fontSize: 16,
fontWeight: "bold",
},
bottomsheet: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingRight: 12,
},
summaryHeader: {
marginTop: 5,
paddingVertical: 5,
fontSize: 15,
fontWeight: "bold",
},
moreBtn: {
fontWeight: "bold",
position: "absolute",
bottom: 0,
right: 16,
paddingLeft: 6,
},
startButton: {
marginTop: 8,
marginBottom: 16,
marginHorizontal: 16,
},
genreContainer: {
paddingHorizontal: 15,
paddingBottom: 15,
},
});
-252
View File
@@ -1,252 +0,0 @@
import React, { useState, useCallback, useRef } from "react";
import { useFocusEffect } from "@react-navigation/native";
import {
StyleSheet,
View,
FlatList,
RefreshControl,
ToastAndroid,
} from "react-native";
import { Provider, Portal } from "react-native-paper";
import ChapterCard from "../components/novel/ChapterCard";
import NovelInfoHeader from "../components/novel/NovelHeader";
import { BottomSheet } from "../components/novel/BottomSheet";
import { fetchNovelFromSource, fetchChaptersFromSource } from "../services/api";
import {
insertNovelInfoInDb,
insertChaptersInDb,
getChaptersFromDb,
checkNovelInDb,
getNovelInfoFromDb,
toggleFavourite,
downloadOrDeleteChapter,
} from "../services/db";
import { useSelector } from "react-redux";
const NovelItem = ({ route, navigation }) => {
const item = route.params;
const theme = useSelector((state) => state.themeReducer.theme);
const {
extensionId,
novelUrl,
navigatingFrom,
novelName,
novelCover,
} = route.params;
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [novel, setNovel] = useState(item);
const [chapters, setChapters] = useState();
const [libraryStatus, setlibraryStatus] = useState(item.libraryStatus);
const [sort, setSort] = useState("");
const [filter, setFilter] = useState("");
const [downloading, setDownloading] = useState({
downloading: false,
chapterUrl: "",
});
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
*/
const insertNovelInLib = async () => {
toggleFavourite(libraryStatus, novelUrl).then((res) => {
setlibraryStatus(res);
ToastAndroid.show(
res ? "Added to library" : "Removed from library",
ToastAndroid.SHORT
);
});
};
/**
* Download chapter from source
*
* If downloaded then delete from db
*/
const downloadChapter = (
downloadStatus,
extensionId,
novelUrl,
chapterUrl,
chapterName
) => {
setDownloading({ downloading: true, chapterUrl: chapterUrl });
downloadOrDeleteChapter(
downloadStatus,
extensionId,
novelUrl,
chapterUrl
).then((res) => {
setDownloading({ downloading: false });
ToastAndroid.show(
!downloadStatus
? `Downloaded ${chapterName}`
: `Deleted ${chapterName}`,
ToastAndroid.SHORT
);
});
};
/**
* 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();
}, [sort, filter])
);
const onRefresh = async () => {
setRefreshing(true);
checkIfExistsInDb();
};
const renderChapterCard = ({ item }) => (
<ChapterCard
navigation={navigation}
novelUrl={novelUrl}
extensionId={extensionId}
chapter={item}
downloadChapter={downloadChapter}
downloading={downloading}
/>
);
return (
<Provider>
<View
style={[
styles.container,
{ backgroundColor: theme.colorDarkPrimaryDark },
]}
>
<FlatList
data={chapters}
extraData={[sort, filter]}
showsVerticalScrollIndicator={false}
keyExtractor={(item) => item.chapterUrl}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={15}
initialNumToRender={7}
renderItem={renderChapterCard}
ListHeaderComponent={() => (
<NovelInfoHeader
item={{
novelCover: novelCover,
novelName: novelName,
}}
novel={novel}
noOfChapters={!loading && chapters.length}
libraryStatus={libraryStatus}
insertNovelInLib={insertNovelInLib}
navigatingFrom={navigatingFrom}
loading={loading}
bottomSheetRef={_panel}
/>
)}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[theme.textColorPrimaryDark]}
progressBackgroundColor={theme.colorDarkPrimary}
/>
}
/>
<Portal>
<BottomSheet
bottomSheetRef={_panel}
sort={sort}
filter={filter}
sortChapters={setSort}
filterChapters={setFilter}
/>
</Portal>
</View>
</Provider>
);
};
export default NovelItem;
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
+34 -139
View File
@@ -1,70 +1,41 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar, Provider, Portal } from "react-native-paper";
import { Provider, Portal } from "react-native-paper";
import NovelCover from "../../../components/common/NovelCover";
import SearchBar from "../../../components/common/SearchBar";
import { SearchAppbar } from "../../../components/common/Appbar";
import { BottomSheet } from "./filters/BottomSheet";
import { useSelector } from "react-redux";
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const BoxNovel = ({ navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const itemsPerRow = useSelector(
(state) => state.settingsReducer.itemsPerRow
);
const library = useSelector((state) => state.libraryReducer.novels);
const AllNovels = ({ navigation }) => {
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [novels, setNovels] = useState([]);
const [libraryNovels, setlibraryNovels] = useState([]);
const [sort, setSort] = useState("rating");
const [pageNo, setPageNo] = useState(1);
const [searchBar, setSearchBar] = useState(false);
const [searchText, setSearchText] = useState("");
const [searched, setSearched] = useState(0);
const theme = useSelector((state) => state.themeReducer.theme);
let bottomSheetRef = useRef(null);
const getNovels = () => {
fetch(
`https://lnreader-extensions.herokuapp.com/api/1/novels/${pageNo}/?o=${sort}`
`https://lnreader-extensions.herokuapp.com/api/1/novels/1/?o=${sort}`
)
.then((response) => response.json())
.then((json) => {
setPageNo(pageNo + 1);
setNovels((novels) => novels.concat(json));
setNovels(json);
setLoading(false);
});
};
const getLibraryNovels = () => {
db.transaction((tx) => {
tx.executeSql(
"SELECT novelUrl FROM LibraryTable WHERE libraryStatus = 1 AND extensionId = 1",
null,
(tx, { rows: { _array } }) => {
setlibraryNovels(_array);
},
(tx, error) => console.log(error)
);
});
};
const onEndReached = ({
layoutMeasurement,
contentOffset,
contentSize,
}) => {
const paddingToBottom = 5;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
};
const getSearchResults = (searchText) => {
setLoading(true);
fetch(
@@ -78,92 +49,33 @@ const AllNovels = ({ navigation }) => {
};
const checkIFInLibrary = (id) => {
return libraryNovels.some((obj) => obj.novelUrl === id);
return library.some((obj) => obj.novelUrl === id);
};
useEffect(() => {
getLibraryNovels();
getNovels();
}, [sort]);
const sortNovels = () => {
setLoading(true);
if (searched) {
getSearchResults();
} else {
getNovels();
}
};
return (
<Provider>
<Appbar.Header style={{ backgroundColor: theme.colorDarkPrimary }}>
{!searchBar ? (
<>
<Appbar.BackAction
onPress={() => navigation.goBack()}
color={theme.textColorPrimaryDark}
/>
<Appbar.Content
title="Box Novel"
titleStyle={{ color: theme.textColorPrimaryDark }}
/>
<Appbar.Action
icon="magnify"
onPress={() => setSearchBar(true)}
color={theme.textColorPrimaryDark}
/>
<Appbar.Action
icon="filter-variant"
onPress={() => _panel.show({ velocity: -1.5 })}
color={theme.textColorPrimaryDark}
/>
</>
) : (
<>
<Appbar.BackAction
onPress={() => {
if (searched) {
setLoading(true);
setPageNo(1);
setNovels([]);
getNovels();
}
setSearchBar(false);
setSearchText("");
}}
color={theme.textColorPrimaryDark}
/>
<SearchBar
searchText={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={() => {
if (searchText !== "") {
setSort("rating");
getSearchResults(searchText);
setSearched(true);
}
}}
/>
{searchText !== "" && (
<Appbar.Action
icon="close"
onPress={() => {
setSearchText("");
}}
color={theme.textColorPrimaryDark}
/>
)}
</>
)}
</Appbar.Header>
<View
style={[
styles.container,
{ backgroundColor: theme.colorDarkPrimaryDark },
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<SearchAppbar
screen="Extension"
placeholder="Search BoxNovel"
searchText={searchText}
setSearchText={setSearchText}
getSearchResults={getSearchResults}
getNovels={getNovels}
setLoading={setLoading}
onFilter={() =>
bottomSheetRef.current.show({ velocity: -1.5 })
}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
@@ -174,27 +86,16 @@ const AllNovels = ({ navigation }) => {
) : (
<FlatList
contentContainerStyle={styles.list}
numColumns={3}
numColumns={itemsPerRow}
key={itemsPerRow}
data={novels}
showsVerticalScrollIndicator={false}
keyExtractor={(item) => item.novelUrl}
ListFooterComponent={() =>
loadingMore && (
<ActivityIndicator
color={theme.colorAccentDark}
size="small"
style={{ marginTop: 20, marginBottom: 80 }}
/>
)
}
renderItem={({ item }) => (
<NovelCover
item={item}
onPress={() => {
navigation.navigate("NovelItem", {
...item,
navigatingFrom: 2,
});
navigation.navigate("NovelItem", item);
}}
libraryStatus={
checkIFInLibrary(item.novelUrl)
@@ -203,33 +104,27 @@ const AllNovels = ({ navigation }) => {
}
/>
)}
onScroll={({ nativeEvent }) => {
if (onEndReached(nativeEvent)) {
setLoadingMore(true);
getNovels();
console.log("End Reached");
}
}}
/>
)}
</View>
<Portal>
<BottomSheet
bottomSheetRef={(c) => (_panel = c)}
bottomSheetRef={bottomSheetRef}
setSort={setSort}
sort={sort}
setLoading={setLoading}
/>
</Portal>
</Provider>
);
};
export default AllNovels;
export default BoxNovel;
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 3,
padding: 4,
},
contentContainer: {
@@ -5,14 +5,18 @@ import Bottomsheet from "rn-sliding-up-panel";
import { useSelector } from "react-redux";
export const BottomSheet = ({
bottomSheetRef,
setSort,
sort,
setRefreshing,
}) => {
export const BottomSheet = ({ bottomSheetRef, setSort, sort, setLoading }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const sorting = [
{ label: "Latest", sortFlag: "latest" },
{ label: "A-Z", sortFlag: "aplhabet" },
{ label: "Rating", sortFlag: "rating" },
{ label: "Trending", sortFlag: "trending" },
{ label: "Most Views", sortFlag: "views" },
{ label: "New", sortFlag: "new-manga" },
];
return (
<Bottomsheet
ref={bottomSheetRef}
@@ -22,20 +26,14 @@ export const BottomSheet = ({
<View
style={[
styles.contentContainer,
{
backgroundColor: theme.colorDarkPrimary,
},
{ backgroundColor: theme.colorPrimary },
]}
>
<View
style={{
backgroundColor: theme.textColorHintDark,
height: 5,
width: 30,
borderRadius: 50,
top: 10,
alignSelf: "center",
}}
style={[
styles.bottomSheetHandle,
{ backgroundColor: theme.textColorPrimary },
]}
/>
<List.Section
title="Sort"
@@ -43,135 +41,32 @@ export const BottomSheet = ({
color: theme.colorAccentDark,
fontWeight: "bold",
}}
style={{
color: theme.colorAccentDark,
// backgroundColor: theme.colorAccentDark,
}}
style={{ color: theme.colorAccentDark }}
expanded
>
<List.Item
right={(props) =>
sort === "latest" && (
<List.Icon
{...props}
size
icon="arrow-up"
color={theme.colorAccentDark}
/>
)
}
title="Latest"
titleStyle={{
fontSize: 15,
color: theme.textColorPrimaryDark,
}}
onPress={() => setSort("latest")}
/>
<List.Item
right={(props) =>
sort === "alphabet" && (
<List.Icon
{...props}
size
icon="arrow-up"
color={theme.colorAccentDark}
/>
)
}
title="A-Z"
titleStyle={{
fontSize: 15,
color: theme.textColorPrimaryDark,
}}
onPress={() => {
setSort("alphabet");
setRefreshing(true);
}}
/>
<List.Item
right={(props) =>
sort === "rating" && (
<List.Icon
{...props}
size
icon="arrow-up"
color={theme.colorAccentDark}
/>
)
}
title="Rating"
titleStyle={{
fontSize: 15,
color: theme.textColorPrimaryDark,
}}
onPress={() => {
setSort("rating");
setRefreshing(true);
}}
/>
<List.Item
right={(props) =>
sort === "trending" && (
<List.Icon
{...props}
size
icon="arrow-up"
color={theme.colorAccentDark}
/>
)
}
title="Trending"
titleStyle={{
fontSize: 15,
color: theme.textColorPrimaryDark,
}}
onPress={() => {
setSort("trending");
setRefreshing(true);
}}
/>
<List.Item
right={(props) =>
sort === "views" && (
<List.Icon
{...props}
size
icon="arrow-up"
color={theme.colorAccentDark}
/>
)
}
title="Most Views"
titleStyle={{
fontSize: 15,
color: theme.textColorPrimaryDark,
}}
onPress={() => {
setSort("views");
setRefreshing(true);
}}
/>
<List.Item
right={(props) =>
sort === "new-manga" && (
<List.Icon
{...props}
size
icon="arrow-up"
color={theme.colorAccentDark}
/>
)
}
title="New"
titleStyle={{
fontSize: 15,
color: theme.textColorPrimaryDark,
}}
onPress={() => {
setSort("new-manga");
setRefreshing(true);
}}
/>
{sorting.map((item, index) => (
<List.Item
key={index}
right={() =>
sort === item.sortFlag && (
<List.Icon
icon="arrow-up"
color={theme.colorAccentDark}
style={styles.sortArrow}
/>
)
}
title={item.label}
titleStyle={{
fontSize: 15,
color: theme.textColorPrimary,
}}
onPress={() => {
setLoading(true);
setSort(item.sortFlag);
}}
/>
))}
</List.Section>
</View>
</Bottomsheet>
@@ -181,5 +76,19 @@ export const BottomSheet = ({
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
bottomSheetHandle: {
opacity: 0.5,
height: 4,
width: 32,
borderRadius: 50,
top: 8,
alignSelf: "center",
},
sortArrow: {
marginVertical: 0,
paddingVertical: 0,
},
});
@@ -1,28 +1,26 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar, Provider } from "react-native-paper";
import { Provider } from "react-native-paper";
import NovelCover from "../../../components/common/NovelCover";
import SearchBar from "../../../components/common/SearchBar";
import { SearchAppbar } from "../../../components/common/Appbar";
import { useSelector } from "react-redux";
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import EmptyView from "../../../components/common/EmptyView";
const ReadLightNovel = ({ navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const library = useSelector((state) => state.libraryReducer.novels);
const itemsPerRow = useSelector(
(state) => state.settingsReducer.itemsPerRow
);
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState();
const [libraryNovels, setlibraryNovels] = useState([]);
const [searchBar, setSearchBar] = useState(false);
const [searchText, setSearchText] = useState("");
const [searched, setSearched] = useState(false);
const getNovels = () => {
fetch(`https://lnreader-extensions.herokuapp.com/api/2/novels/`)
.then((response) => response.json())
@@ -44,95 +42,32 @@ const ReadLightNovel = ({ navigation }) => {
});
};
const getLibraryNovels = () => {
db.transaction((tx) => {
tx.executeSql(
"SELECT novelUrl FROM LibraryTable WHERE libraryStatus = 1 AND extensionId = 2",
null,
(tx, { rows: { _array } }) => {
setlibraryNovels(_array);
},
(tx, error) => console.log(error)
);
});
};
const checkIFInLibrary = (id) => {
return libraryNovels.some((obj) => obj.novelUrl === id);
return library.some((obj) => obj.novelUrl === id);
};
useEffect(() => {
getNovels();
getLibraryNovels();
}, []);
return (
<Provider>
<Appbar.Header style={{ backgroundColor: theme.colorDarkPrimary }}>
{!searchBar ? (
<>
<Appbar.BackAction
color={theme.textColorPrimaryDark}
onPress={() => navigation.goBack()}
/>
<Appbar.Content
color={theme.textColorPrimaryDark}
title="ReadLightNovel"
titleStyle={{ color: theme.textColorPrimaryDark }}
/>
<Appbar.Action
color={theme.textColorPrimaryDark}
icon="magnify"
onPress={() => setSearchBar(true)}
/>
<Appbar.Action
icon="filter-variant"
disabled
color={theme.textColorPrimaryDark}
/>
</>
) : (
<>
<Appbar.BackAction
onPress={() => {
if (searched) {
setLoading(true);
getNovels();
}
setSearchBar(false);
setSearchText("");
}}
color={theme.textColorPrimaryDark}
/>
<SearchBar
searchText={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={() => {
if (searchText !== "") {
getSearchResults(searchText);
setSearched(true);
}
}}
/>
{searchText !== "" && (
<Appbar.Action
icon="close"
onPress={() => {
setSearchText("");
}}
color={theme.textColorPrimaryDark}
/>
)}
</>
)}
</Appbar.Header>
<View
style={[
styles.container,
{ backgroundColor: theme.colorDarkPrimaryDark },
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<SearchAppbar
screen="Extension"
placeholder="Search ReadLightNovel"
searchText={searchText}
setSearchText={setSearchText}
getSearchResults={getSearchResults}
getNovels={getNovels}
setLoading={setLoading}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
@@ -142,8 +77,9 @@ const ReadLightNovel = ({ navigation }) => {
</View>
) : (
<FlatList
contentContainerStyle={styles.list}
numColumns={3}
contentContainerStyle={{ flexGrow: 1 }}
numColumns={itemsPerRow}
key={itemsPerRow}
data={novels}
showsVerticalScrollIndicator={false}
keyExtractor={(item) => item.novelUrl}
@@ -151,10 +87,7 @@ const ReadLightNovel = ({ navigation }) => {
<NovelCover
item={item}
onPress={() =>
navigation.navigate("NovelItem", {
...item,
navigatingFrom: 2,
})
navigation.navigate("NovelItem", item)
}
libraryStatus={
checkIFInLibrary(item.novelUrl)
@@ -163,6 +96,15 @@ const ReadLightNovel = ({ navigation }) => {
}
/>
)}
ListEmptyComponent={
searchText !== "" &&
novels.length === 0 && (
<EmptyView
icon="(; ̄Д ̄)"
description="No results found"
/>
)
}
/>
)}
</View>
-23
View File
@@ -13,29 +13,6 @@ export const getReaderTheme = async () => {
return theme;
};
export const setAppTheme = async (val) => {
AsyncStorage.setItem("@application_theme", JSON.stringify(val));
};
export const getAppTheme = async () => {
let theme = await AsyncStorage.getItem("@application_theme").then(
(value) => {
if (value) return JSON.parse(value);
}
);
return theme;
};
export const getDisplayMode = async () => {
let displayMode = await AsyncStorage.getItem("@display_mode");
return displayMode;
};
export const setDisplayMode = async (val) => {
AsyncStorage.setItem("@display_mode", val);
};
export const setStatusBar = () => {
AsyncStorage.getItem("@application_theme").then((value) => {
if (JSON.parse(value) === "lightTheme") {
+15 -3
View File
@@ -20,9 +20,11 @@ export const fetchNovelFromSource = async (extensionId, novelUrl) => {
sourceUrl: res.sourceUrl,
source: res.sourceName,
unread: 1,
libraryStatus: 0,
lastRead: res.novelChapters[0].chapterUrl,
lastReadName: res.novelChapters[0].chapterName,
extensionId: res.extensionId,
novelChapters: res.novelChapters,
};
return novel;
@@ -53,12 +55,22 @@ export const fetchChapterFromSource = async (
chapterUrl
) => {
const url = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
console.log(
`https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`
);
let res = await fetch(url);
let chapter = await res.json();
return chapter;
};
/**
* Fetch list of extensions
*/
export const fetchExtensionList = async () => {
const url = `https://lnreader-extensions.herokuapp.com/api/`;
let res = await fetch(url);
let extensions = await res.json();
return extensions;
};
+135 -36
View File
@@ -28,7 +28,7 @@ export const createTables = async () => {
);
tx.executeSql(
"CREATE TABLE IF NOT EXISTS DownloadsTable (downloadId INTEGER PRIMARY KEY AUTOINCREMENT, chapterUrl VARCHAR(255) UNIQUE, novelUrl VARCHAR(255), chapterName VARCHAR(255), chapterText VARCHAR(255), nextChapter VARCHAR(255), prevChapter VARCHAR(255), FOREIGN KEY (novelUrl) REFERENCES LibraryTable(novelUrl) ON DELETE CASCADE)",
"CREATE TABLE IF NOT EXISTS DownloadsTable (downloadId INTEGER PRIMARY KEY AUTOINCREMENT, chapterUrl VARCHAR(255), novelUrl VARCHAR(255), chapterName VARCHAR(255), chapterText VARCHAR(255), nextChapter VARCHAR(255), prevChapter VARCHAR(255), FOREIGN KEY (novelUrl) REFERENCES LibraryTable(novelUrl) ON DELETE CASCADE, UNIQUE(chapterUrl, novelUrl))",
null,
(tx, results) => console.log("Downloads Table Created"),
(tx, error) => console.log(error)
@@ -71,7 +71,7 @@ export const createIndexes = async () => {
});
};
export const insertNovelInfoInDb = (novel) => {
export const insertNovelInfoInDb = async (novel) => {
db.transaction((tx) => {
tx.executeSql(
"INSERT INTO LibraryTable (novelUrl, novelName, novelCover, novelSummary, `Author(s)`, `Genre(s)`, Status, extensionId, lastRead, lastReadName, sourceUrl, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
@@ -89,15 +89,13 @@ export const insertNovelInfoInDb = (novel) => {
novel.sourceUrl,
novel.source,
],
(txObj, res) => {
console.log("Inserted Novel Info in Db");
},
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
export const insertChaptersInDb = (novelUrl, chapters) => {
export const insertChaptersInDb = async (novelUrl, chapters) => {
db.transaction((tx) => {
chapters.map((chapter) =>
tx.executeSql(
@@ -107,11 +105,12 @@ export const insertChaptersInDb = (novelUrl, chapters) => {
chapter.chapterName,
chapter.releaseDate,
novelUrl,
]
],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
)
);
});
console.log("Inserted Chapters in Db");
};
/**
@@ -180,18 +179,14 @@ export const checkNovelInDb = (novelUrl) => {
*/
export const toggleFavourite = async (libraryStatus, novelUrl) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
"UPDATE LibraryTable SET libraryStatus = ? WHERE novelUrl=?",
[!libraryStatus, novelUrl],
(tx, res) => {
resolve(!libraryStatus);
},
(txObj, error) => console.log("Error ", error)
);
})
);
db.transaction((tx) => {
tx.executeSql(
"UPDATE LibraryTable SET libraryStatus = ? WHERE novelUrl=?",
[!libraryStatus, novelUrl],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
/**
@@ -213,8 +208,8 @@ export const downloadOrDeleteChapter = async (
db.transaction((tx) => {
tx.executeSql(
`UPDATE ChapterTable SET downloaded = 1 WHERE chapterUrl = ?`,
[chapterUrl]
`UPDATE ChapterTable SET downloaded = 1 WHERE chapterUrl = ? AND novelUrl = ?`,
[chapterUrl, novelUrl]
);
tx.executeSql(
`INSERT INTO DownloadsTable (chapterUrl, novelUrl, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?, ?)`,
@@ -233,12 +228,12 @@ export const downloadOrDeleteChapter = async (
} else {
db.transaction((tx) => {
tx.executeSql(
`UPDATE ChapterTable SET downloaded = 0 WHERE chapterUrl = ?`,
[chapterUrl]
`UPDATE ChapterTable SET downloaded = 0 WHERE chapterUrl = ? AND novelUrl= ?`,
[chapterUrl, novelUrl]
);
tx.executeSql(
`DELETE FROM DownloadsTable WHERE chapterUrl = ?`,
[chapterUrl],
`DELETE FROM DownloadsTable WHERE chapterUrl = ? AND novelUrl = ?`,
[chapterUrl, novelUrl],
(tx, res) => console.log(`Chapter deleted`),
(txObj, error) => console.log("Error ", error)
);
@@ -254,7 +249,7 @@ export const getHistoryFromDb = async () => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
"SELECT HistoryTable.chapterUrl, HistoryTable.historyId, HistoryTable.chapterName, HistoryTable.lastRead, LibraryTable.novelName, LibraryTable.novelCover, LibraryTable.novelUrl, LibraryTable.extensionId, LibraryTable.libraryStatus FROM HistoryTable INNER JOIN LibraryTable ON HistoryTable.novelUrl = LibraryTable.novelUrl ORDER BY HistoryTable.lastRead DESC",
"SELECT HistoryTable.chapterUrl, HistoryTable.historyId, HistoryTable.chapterName, HistoryTable.lastRead, LibraryTable.novelName, LibraryTable.libraryStatus, LibraryTable.novelCover, LibraryTable.novelUrl, LibraryTable.extensionId, LibraryTable.libraryStatus FROM HistoryTable INNER JOIN LibraryTable ON HistoryTable.novelUrl = LibraryTable.novelUrl ORDER BY HistoryTable.lastRead DESC",
null,
(txObj, { rows: { _array } }) => {
resolve(_array);
@@ -265,14 +260,9 @@ export const getHistoryFromDb = async () => {
});
};
export const deleteChapterHistory = (novelId) => {
export const deleteChapterHistory = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
"DELETE FROM HistoryTable WHERE novelUrl = ?",
[novelId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
tx.executeSql("DELETE FROM HistoryTable WHERE novelUrl = ?", [novelId]);
});
};
@@ -316,14 +306,14 @@ export const searchInLibrary = async (searchText) => {
export const deleteHistory = () => {
db.transaction((tx) => {
tx.executeSql("DELETE FROM HistoryTable");
tx.executeSql("DELETE FROM HistoryTable; VACCUM;");
});
};
export const deleteNovelsNotInLibrary = () => {
db.transaction((tx) => {
tx.executeSql(
"DELETE FROM LibraryTable WHERE libraryStatus=0",
"DELETE FROM LibraryTable WHERE libraryStatus = 0",
null,
(txObj, res) => {
console.log("Deleted");
@@ -373,3 +363,112 @@ 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 = ? AND novelUrl = ?`,
[chapterUrl, novelUrl]
);
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 = async (novelUrl, chapterUrl) => {
const updateIsDownloadedQuery = `UPDATE ChapterTable SET downloaded = 0 WHERE chapterUrl = ? AND novelUrl = ?`;
const deleteChapterQuery = `DELETE FROM DownloadsTable WHERE chapterUrl = ? AND novelUrl = ?`;
db.transaction((tx) => {
tx.executeSql(
updateIsDownloadedQuery,
[chapterUrl, novelUrl],
(tx, res) => console.log(`Chapter deleted`),
(txObj, error) => console.log("Error ", error)
);
tx.executeSql(
deleteChapterQuery,
[chapterUrl, novelUrl],
(tx, res) => console.log(`Chapter deleted`),
(txObj, error) => console.log("Error ", error)
);
});
};
export const insertIntoHistory = async (chapterUrl, chapterName, novelUrl) => {
db.transaction((tx) => {
tx.executeSql(
"INSERT OR REPLACE INTO HistoryTable (chapterUrl, novelUrl, chapterName, lastRead) VALUES ( ?, ?, ?, (datetime('now','localtime')))",
[chapterUrl, novelUrl, chapterName]
);
tx.executeSql(
"UPDATE LibraryTable SET lastRead = ?, lastReadName = ?, unread = 0 WHERE novelUrl = ?",
[chapterUrl, chapterName, novelUrl]
);
});
};
export const chapterRead = async (chapterUrl, novelUrl) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE ChapterTable SET `read` = 1 WHERE chapterUrl = ? AND novelUrl = ?",
[chapterUrl, novelUrl],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
};
export const isChapterDownloaded = async (chapterUrl, novelUrl) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
"SELECT * FROM DownloadsTable WHERE chapterUrl=? AND novelUrl=?",
[chapterUrl, novelUrl],
(txObj, res) => {
if (res.rows.length !== 0) {
resolve(true);
} else {
resolve(false);
}
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
export const getChapterFromDb = async (chapterUrl, novelUrl) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
`SELECT * FROM DownloadsTable WHERE chapterUrl = ? AND novelUrl = ?`,
[chapterUrl, novelUrl],
(tx, results) => {
resolve(results.rows.item(0));
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
+29 -25
View File
@@ -2,63 +2,67 @@
* Application Colors
*/
export const theme = {
colorDarkPrimary: "#000000",
colorDarkPrimaryDark: "#000000",
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccentDark: "#3399FF",
textColorPrimaryDark: "#FFFFFF",
textColorSecondaryDark: "rgba(255,255,255,0.7)",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColorDark: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.2)",
};
/**
* Light Theme
*/
export const lightTheme = {
colorDarkPrimary: "#FFFFFF",
colorDarkPrimaryDark: "#FAFAFA",
colorPrimary: "#FFFFFF",
colorPrimaryDark: "#FAFAFA",
colorAccentDark: "#2979FF",
textColorPrimaryDark: "#000000",
textColorSecondaryDark: "rgba(0,0,0,0.54)",
textColorPrimary: "#000000",
textColorSecondary: "rgba(0,0,0,0.54)",
textColorHintDark: "rgba(0,0,0,0.38)",
rippleColorDark: "#C2C2C2",
rippleColor: "rgba(0,0,0,0.12)",
searchBarColor: "#FFFFFF",
};
/**
* Dark Theme
*/
export const darkTheme = {
colorDarkPrimary: "#242529",
colorDarkPrimaryDark: "#202125",
colorPrimary: "#242529",
colorPrimaryDark: "#202125",
colorAccentDark: "#3399FF",
textColorPrimaryDark: "#FFFFFF",
textColorSecondaryDark: "rgba(255,255,255,0.7)",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColorDark: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#303135",
};
/**
* Amoled Dark Theme
*/
export const amoledDarkTheme = {
colorDarkPrimary: "#000000",
colorDarkPrimaryDark: "#000000",
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccentDark: "#3399FF",
textColorPrimaryDark: "#FFFFFF",
textColorSecondaryDark: "rgba(255,255,255,0.7)",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColorDark: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#1F1F1F",
};
/**
* Midnight Dusk Theme
*/
export const midnightDuskTheme = {
colorDarkPrimary: "#201F27",
colorDarkPrimaryDark: "#16151D",
colorPrimary: "#201F27",
colorPrimaryDark: "#16151D",
colorAccentDark: "#F02475",
textColorPrimaryDark: "#FFFFFF",
textColorSecondaryDark: "rgba(255,255,255,0.7)",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColorDark: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#201F27",
};