Compare commits

...

43 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 4b5ee1667c Release v1.0.19 2021-05-31 22:54:49 +05:30
Rajarshee Chatterjee d5b3915ef1 Minor fixes 2021-05-31 21:20:17 +05:30
Rajarshee Chatterjee 7b41f10dd6 Add empty view component 2021-05-31 21:19:29 +05:30
Rajarshee Chatterjee b12eef2699 Global search from novel name 2021-05-31 21:18:26 +05:30
Rajarshee Chatterjee 7a8c8ded24 Add option to select text 2021-05-31 21:16:41 +05:30
Rajarshee Chatterjee b6e8b0057b Use tabs in chapter filter sheet 2021-05-31 17:08:48 +05:30
Rajarshee Chatterjee c06ce93581 Global search from library 2021-05-31 16:00:48 +05:30
Rajarshee Chatterjee c4c156eb1d Minor fixes 2021-05-31 15:38:19 +05:30
Rajarshee Chatterjee f1d86b798e UI fixes 2021-05-31 15:38:05 +05:30
Rajarshee Chatterjee 4f1aa7c2cc History fixes 2021-05-31 15:37:07 +05:30
Rajarshee Chatterjee f8199b4e4e Reorganize library filter sheet 2021-05-31 15:32:42 +05:30
Rajarshee Chatterjee 82e7111a40 Add searchbar in history screen (#24) 2021-05-31 09:13:15 +05:30
Rajarshee Chatterjee a15369967f Add custom accent color 2021-05-30 18:19:59 +05:30
Rajarshee Chatterjee 5a936bea02 Rename extensions to sources 2021-05-30 18:19:39 +05:30
Rajarshee Chatterjee 13b49dfd21 Minor UI changes 2021-05-30 18:19:14 +05:30
Rajarshee Chatterjee 689185bab6 Display images if links are present in chapterText 2021-05-30 18:18:25 +05:30
Rajarshee Chatterjee 917dea2261 Fix white border between screens 2021-05-29 18:59:53 +05:30
Rajarshee Chatterjee 6afcdcf96e Reorganize novel details 2021-05-29 17:15:30 +05:30
Rajarshee Chatterjee b74066111f Parse images in chapter and reorganize reader sheet 2021-05-29 12:04:24 +05:30
Rajarshee Chatterjee b42fb6319b Add hide chapter titles 2021-05-28 17:11:23 +05:30
Rajarshee Chatterjee cb05231335 Group updates and history by date 2021-05-28 10:30:07 +05:30
Rajarshee Chatterjee 7819c7dcdb Add selecting chapters logic 2021-05-27 10:37:43 +05:30
Rajarshee Chatterjee 9a7e1f7e53 Refactor Chapter 2021-05-25 10:32:21 +05:30
Rajarshee Chatterjee 2c2e814a0d Fix reader sheet buttons 2021-05-20 10:17:33 +05:30
Rajarshee Chatterjee f4fc3d096f Refactor NovelHeader 2021-05-17 09:50:01 +05:30
Rajarshee Chatterjee d230b64846 Refactor settings and minor fixes 2021-05-16 18:44:37 +05:30
Rajarshee Chatterjee a280fc95e1 Reset progress if marked read 2021-05-16 16:36:42 +05:30
Rajarshee Chatterjee 1f87a50762 Fix scroll to saved progress 2021-05-16 08:58:39 +05:30
Rajarshee Chatterjee 99945aa0d8 Release v1.0.18 2021-05-14 23:29:08 +05:30
Rajarshee Chatterjee fda59f825f Fix library filters and minor fixes 2021-05-14 21:00:55 +05:30
Rajarshee Chatterjee 67096eb1ff Add library sort/filter sheet and notification icon 2021-05-14 18:30:15 +05:30
Rajarshee Chatterjee 3a156f32a0 Save scroll percentage and add new theme 2021-05-14 12:03:32 +05:30
Rajarshee Chatterjee 555552a64d Update app icon and splash screen 2021-05-14 09:14:31 +05:30
Rajarshee Chatterjee 53fc800b90 Minor fixes 2021-05-14 08:56:46 +05:30
Rajarshee Chatterjee d1e10433e2 Add bottomsheet for chapter actions 2021-05-13 08:21:46 +05:30
Rajarshee Chatterjee cc53e309ad Fix last read chapter getting reset on opening another novel (fixes #19) 2021-05-12 14:20:48 +05:30
Rajarshee Chatterjee 2170b77607 Add swipe to mark chapter read/ bookmark logic 2021-05-12 10:00:46 +05:30
Rajarshee Chatterjee 632c2e47c7 Add scroll percentage in chapter screen (fixes #17) 2021-05-11 08:43:42 +05:30
Rajarshee Chatterjee 5e2c4d4e27 Add searchbar in updates screen 2021-05-10 09:15:09 +05:30
Rajarshee Chatterjee a21800f208 Minor fixes 2021-05-09 10:10:09 +05:30
Rajarshee Chatterjee 5f943fcc92 Merge branch 'main' 2021-05-09 10:00:59 +05:30
Rajarshee Chatterjee 8f254b4ef5 Add Migration (#15)
* Refactor

* Add migration

* Finish migration

* Add progress bar
2021-05-09 09:57:21 +05:30
Rajarshee Chatterjee 773913048c Refactor 2021-05-07 09:36:07 +05:30
101 changed files with 4934 additions and 2150 deletions
+3 -1
View File
@@ -12,7 +12,9 @@ import { fonts } from "./src/Theme/fonts";
const App = () => {
const [loaded] = useFonts(fonts);
useEffect(() => createDB(), []);
useEffect(() => {
createDB();
}, []);
if (!loaded) {
return null;
+11 -3
View File
@@ -3,8 +3,8 @@
"name": "LNReader",
"slug": "LNReader",
"scheme": "lnreader",
"version": "1.0.16",
"orientation": "portrait",
"version": "1.0.19",
"orientation": "default",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
@@ -15,6 +15,10 @@
"fallbackToCacheTimeout": 0,
"enabled": false
},
"notification": {
"icon": "./assets/notification.png",
"color": "#00adb5"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
@@ -26,7 +30,11 @@
},
"package": "com.rajarsheechatterjee.LNReader"
},
"androidStatusBar": {
"barStyle": "light-content",
"backgroundColor": "#00000000",
"translucent": true
},
"androidNavigationBar": {
"barStyle": "light-content",
"backgroundColor": "#000000"
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 266 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 28 KiB

+22 -29
View File
@@ -3819,14 +3819,6 @@
"uuid": "^3.4.0"
}
},
"expo-app-loading": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/expo-app-loading/-/expo-app-loading-1.0.3.tgz",
"integrity": "sha512-zLheUgBCQoyybJrGxw81MWfxvlnHb7zatjLbmkNosqMwqq3MOPC/P4HwF6qYsrVUFO6PIzb4yq3lFO3RkCCfhw==",
"requires": {
"expo-splash-screen": "~0.10.2"
}
},
"expo-application": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-3.1.2.tgz",
@@ -3914,15 +3906,6 @@
"url-parse": "^1.4.4"
}
},
"expo-media-library": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-12.0.2.tgz",
"integrity": "sha512-0gi4ny4IldYg3uWHjwP+P04VRe5Ea9BYflRILpQWBzcivppzEzTSYFQKUTQEXuZsWB9PA5uTQd2UaB+4q/dcJQ==",
"requires": {
"@expo/config-plugins": "^1.0.18",
"unimodules-permissions-interface": "~6.1.0"
}
},
"expo-notifications": {
"version": "0.11.6",
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.11.6.tgz",
@@ -3969,13 +3952,10 @@
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-9.1.2.tgz",
"integrity": "sha512-IFBQPimeZtdqtvmqQZEmiI0D4j9xE53tSZxLFrFPplNWzgDefk+dP+flof3duf9JgCQTsvOUU2h2/XwjD3vBTA=="
},
"expo-splash-screen": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.10.2.tgz",
"integrity": "sha512-ngelW7g5yFqk3LWbyLDDxi3LYXEfgGFCJddL/Q8S/C1pMUc5foW2j9i/q+akK8i5mjYtSx3+Bk/qbyX92QIF/w==",
"requires": {
"@expo/configure-splash-screen": "0.3.4"
}
"expo-speech": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/expo-speech/-/expo-speech-9.1.0.tgz",
"integrity": "sha512-5yIMVo1auAwClVnlJnCRyJqKS82IeQXzTCamP4iP4LmHDR5+9sae/D/mD5mX7KV3brWf3n0sEyFhuU7WqgMQvg=="
},
"expo-sqlite": {
"version": "9.1.0",
@@ -7434,6 +7414,14 @@
"resolved": "https://registry.npmjs.org/react-native-easing-gradient/-/react-native-easing-gradient-0.1.2.tgz",
"integrity": "sha512-oeSJ8nSrCxiCO5mLEVc59Evva4unjvH6AQ8vJFyJ7PIKRybIV5JC+OBU8VnF+V7oQ9dnDBpx2qT5Mh+oxd+/tw=="
},
"react-native-fit-image": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz",
"integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==",
"requires": {
"prop-types": "^15.5.10"
}
},
"react-native-gesture-handler": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz",
@@ -7478,6 +7466,11 @@
"redux-saga": "^1.0.2"
}
},
"react-native-pager-view": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-5.0.12.tgz",
"integrity": "sha512-QmHUnQeP2qcxDofEOnKRmoUue0RaT55lhNJDfcQ1/SNuxif4Q2UyvDfqeItm1+toaE5tVnXqoreZh82FqUqnvw=="
},
"react-native-paper": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-4.8.1.tgz",
@@ -7488,11 +7481,6 @@
"react-native-iphone-x-helper": "^1.3.1"
}
},
"react-native-picker-scrollview": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/react-native-picker-scrollview/-/react-native-picker-scrollview-1.0.1.tgz",
"integrity": "sha512-6N4IuAwLVDLoO3IUCW0QzWJrafXVi7WylofaPg4PNPfPRFOYAsGKMN5yEPw463tWtmbBxP6vZEIOfGl93d9Aeg=="
},
"react-native-reanimated": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-2.1.0.tgz",
@@ -7530,6 +7518,11 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.0.0.tgz",
"integrity": "sha512-35II5oxTaVp3OP8y0eLPOPpQkxG4fRKQ+dL1YSE1we5kCZFOU0l/Rn0T79HbyUu1LPwUZr6lZupPs0ULnRyMuQ=="
},
"react-native-tab-view": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-3.0.1.tgz",
"integrity": "sha512-L7qXtYz5Z+NdWyu38zaJrlHA/8H6rNFRZXe3ydym12UEEPPoefbVZ71b9YK2O6QP0u4pUlzMpMVM/Lsz2DJ4jg=="
},
"react-native-vector-icons": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-7.1.0.tgz",
+7 -6
View File
@@ -16,17 +16,19 @@
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "^41.0.1",
"expo-app-loading": "^1.0.1",
"expo-background-fetch": "~9.1.0",
"expo-document-picker": "^9.1.2",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-media-library": "~12.0.2",
"expo-notifications": "~0.11.6",
"expo-permissions": "^12.0.1",
"expo-sharing": "~9.1.2",
"expo-speech": "~9.1.0",
"expo-sqlite": "~9.1.0",
"expo-status-bar": "~1.0.4",
"expo-task-manager": "~9.1.0",
"expo-web-browser": "~9.1.0",
"moment": "^2.29.1",
"qs": "^6.10.1",
@@ -35,13 +37,14 @@
"react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
"react-native-collapsible-header-views": "^1.1.2",
"react-native-easing-gradient": "^0.1.2",
"react-native-fit-image": "^1.5.5",
"react-native-gesture-handler": "~1.10.2",
"react-native-offline": "^5.7.0",
"react-native-paper": "^4.5.0",
"react-native-picker-scrollview": "^1.0.1",
"react-native-reanimated": "~2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-tab-view": "^3.0.1",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "~0.13.12",
"react-native-webview": "11.2.3",
@@ -51,9 +54,7 @@
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4",
"expo-notifications": "~0.11.6",
"expo-background-fetch": "~9.1.0",
"expo-task-manager": "~9.1.0"
"react-native-pager-view": "5.0.12"
},
"devDependencies": {
"@babel/core": "~7.9.0"
+2 -1
View File
@@ -10,7 +10,7 @@ import Constants from "expo-constants";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
export const Appbar = ({ title, onBackAction, style }) => {
export const Appbar = ({ title, onBackAction, style, children }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
return (
@@ -24,6 +24,7 @@ export const Appbar = ({ title, onBackAction, style }) => {
title={title}
titleStyle={{ color: theme.textColorPrimary }}
/>
{children}
</MaterialAppbar.Header>
);
};
+12 -18
View File
@@ -1,23 +1,17 @@
import React from "react";
import { View } from "react-native";
import { useSelector } from "react-redux";
const BottomSheetHandle = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<>
<View
style={{
backgroundColor: theme.textColorHint,
height: 5,
width: 30,
borderRadius: 50,
top: 10,
alignSelf: "center",
}}
/>
</>
);
};
const BottomSheetHandle = ({ theme }) => (
<View
style={{
backgroundColor: theme.textColorHint,
height: 5,
width: 30,
borderRadius: 50,
top: 10,
alignSelf: "center",
}}
/>
);
export default BottomSheetHandle;
+21
View File
@@ -0,0 +1,21 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
const ScreenContainer = ({ children, theme }) => (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
{children}
</View>
);
const Row = ({ children, style }) => (
<View style={[styles.row, style]}>{children}</View>
);
export { ScreenContainer, Row };
const styles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
},
});
+5 -2
View File
@@ -2,7 +2,7 @@ import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { useSelector } from "react-redux";
const EmptyView = ({ icon, description }) => {
const EmptyView = ({ icon, description, style, children, iconStyle }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
return (
@@ -11,6 +11,8 @@ const EmptyView = ({ icon, description }) => {
style={[
styles.emptyViewIcon,
{ color: theme.textColorSecondary },
style,
iconStyle,
]}
>
{icon}
@@ -19,10 +21,12 @@ const EmptyView = ({ icon, description }) => {
style={[
styles.emptyViewText,
{ color: theme.textColorSecondary },
style,
]}
>
{description}
</Text>
{children}
</View>
);
};
@@ -37,7 +41,6 @@ const styles = StyleSheet.create({
},
emptyViewIcon: {
fontSize: 45,
fontWeight: "bold",
},
emptyViewText: {
fontWeight: "bold",
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
import { Text, View } from "react-native";
import { IconButton } from "react-native-paper";
import { Row } from "./Common";
import EmptyView from "./EmptyView";
const ErrorView = ({ theme, onRetry, error, openWebView }) => (
<View style={{ flex: 1, justifyContent: "center" }}>
<EmptyView
icon="(; ̄Д ̄)"
description={error}
style={{ color: theme.textColorSecondary }}
>
<IconButton
icon="reload"
size={25}
style={{ margin: 0, marginTop: 24 }}
color={theme.textColorSecondary}
onPress={onRetry}
/>
<Text style={{ color: theme.textColorPrimary }}>Retry</Text>
</EmptyView>
</View>
);
export default ErrorView;
+20 -6
View File
@@ -1,6 +1,8 @@
import React from "react";
import { StyleSheet } from "react-native";
import { List, Divider as PaperDivider } from "react-native-paper";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
const ListSection = ({ children }) => (
<List.Section style={styles.listSection}>{children}</List.Section>
@@ -14,10 +16,21 @@ const ListSubHeader = ({ children, theme }) => (
</List.Subheader>
);
const ListItem = ({ title, description, icon, onPress, theme, right }) => (
const ListItem = ({
title,
description,
icon,
onPress,
theme,
right,
iconColor,
titleStyle,
style,
}) => (
<List.Item
title={title}
titleStyle={{ color: theme.textColorPrimary }}
style={style}
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
description={description}
descriptionStyle={{ color: theme.textColorSecondary }}
descriptionNumberOfLines={1}
@@ -32,10 +45,11 @@ const ListItem = ({ title, description, icon, onPress, theme, right }) => (
}
right={() =>
right && (
<List.Icon
color="#47a84a"
icon={right}
style={{ marginVertical: 0 }}
<MaterialCommunityIcons
name={right}
color={iconColor || "#47a84a"}
size={23}
style={{ marginRight: 16, textAlignVertical: "center" }}
/>
)
}
+12 -3
View File
@@ -9,7 +9,8 @@ import ListView from "./ListView";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const theme = useTheme();
const { displayMode, itemsPerRow } = useSettings();
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
useSettings();
const height = {
1: 550,
@@ -53,6 +54,7 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
);
const unreadBadge = () =>
showUnreadBadges &&
item.chaptersUnread && (
<Text
style={[
@@ -61,6 +63,9 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
]}
>
{item.chaptersUnread}
@@ -68,6 +73,7 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
);
const downloadBadge = () =>
showDownloadBadges &&
item.chaptersDownloaded && (
<Text
style={[
@@ -76,6 +82,9 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
]}
>
{item.chaptersDownloaded}
@@ -83,7 +92,7 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
);
return displayMode !== 2 ? (
<View style={{ flex: 1 / itemsPerRow }}>
<View style={{ flex: 1 / novelsPerRow }}>
<TouchableRipple
borderless
centered
@@ -94,7 +103,7 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
<>
<ImageBackground
source={{ uri: item.novelCover }}
style={{ height: height[itemsPerRow] }}
style={{ height: height[novelsPerRow] }}
imageStyle={[
{ borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
+7 -11
View File
@@ -1,5 +1,6 @@
import React from "react";
import { RefreshControl, StyleSheet, FlatList } from "react-native";
import { StyleSheet, FlatList } from "react-native";
import { useSettings } from "../Hooks/reduxHooks";
const NovelList = ({
@@ -8,20 +9,15 @@ const NovelList = ({
refreshControl,
ListEmptyComponent,
}) => {
const { displayMode, itemsPerRow } = useSettings();
const getItemsPerRow = () => {
if (displayMode === 2) {
return 1;
} else {
return itemsPerRow;
}
};
const { displayMode, novelsPerRow } = useSettings();
const getNovelsPerRow = () => (displayMode === 2 ? 1 : novelsPerRow);
return (
<FlatList
contentContainerStyle={styles.flatListCont}
numColumns={getItemsPerRow()}
key={getItemsPerRow()}
numColumns={getNovelsPerRow()}
key={getNovelsPerRow()}
data={data}
keyExtractor={(item) => item.novelUrl}
renderItem={renderItem}
+6 -2
View File
@@ -8,7 +8,7 @@ export const RadioButtonGroup = ({ children, value, onValueChange }) => (
</MaterialRadioButton.Group>
);
export const RadioButton = ({ value, label, theme }) => (
export const RadioButton = ({ value, label, theme, labelStyle }) => (
<View style={styles.radioButtonContainer}>
<MaterialRadioButton
value={value}
@@ -16,7 +16,11 @@ export const RadioButton = ({ value, label, theme }) => (
uncheckedColor={theme.textColorSecondary}
/>
<Text
style={[styles.radioButtonLabel, { color: theme.textColorPrimary }]}
style={[
styles.radioButtonLabel,
{ color: theme.textColorPrimary },
labelStyle,
]}
>
{label}
</Text>
+21 -3
View File
@@ -4,7 +4,7 @@ import { TouchableRipple, IconButton } from "react-native-paper";
import Constants from "expo-constants";
import { useSettings } from "../Hooks/reduxHooks";
import { useDispatch } from "react-redux";
import { setDisplayMode } from "../redux/settings/settings.actions";
import { setAppSettings } from "../redux/settings/settings.actions";
export const Searchbar = ({
theme,
@@ -18,6 +18,9 @@ export const Searchbar = ({
clearSearchbar,
onChangeText,
onSubmitEditing,
migrate,
onPressMigrate,
filter,
}) => {
const searchRef = useRef(null);
const dispatch = useDispatch();
@@ -86,19 +89,34 @@ export const Searchbar = ({
style={{ marginRight: 0 }}
size={23}
onPress={() =>
dispatch(setDisplayMode(getDisplayMode()))
dispatch(
setAppSettings("displayMode", getDisplayMode())
)
}
/>
)}
{right && (
<IconButton
icon={right}
color={theme.textColorSecondary}
color={
!filter
? theme.textColorSecondary
: theme.filterColor
}
style={{ marginRight: 0 }}
size={23}
onPress={onPressRight}
/>
)}
{migrate && (
<IconButton
icon={migrate}
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={onPressMigrate}
/>
)}
</View>
</TouchableRipple>
);
-8
View File
@@ -1,8 +0,0 @@
import React from "react";
import { View, Text as RNText, Image, StyleSheet } from "react-native";
const Text = () => {};
export default UpdateCard;
const styles = StyleSheet.create({});
+87 -4
View File
@@ -40,6 +40,7 @@ const getChaptersQuery = (sort, filter) =>
}`;
export const getChapters = (novelId, sort, filter) => {
// console.log(getChaptersQuery(sort, filter));
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
@@ -56,7 +57,7 @@ export const getChapters = (novelId, sort, filter) => {
const getChapterQuery = `SELECT * FROM downloads WHERE downloadChapterId = ?`;
export const getChapter = async (chapterId) => {
export const getChapterFromDB = async (chapterId) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
@@ -71,6 +72,36 @@ export const getChapter = async (chapterId) => {
);
};
export const getNextChapterFromDB = async (novelId, chapterId) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
`SELECT * FROM chapters WHERE novelId = ? AND chapterId > ?`,
[novelId, chapterId],
(tx, results) => {
resolve(results.rows.item(0));
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
export const getPrevChapterFromDB = async (novelId, chapterId) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
`SELECT * FROM chapters WHERE novelId = ? AND chapterId < ?`,
[novelId, chapterId],
(tx, results) => {
resolve(results.rows.item(results.rows.length - 1));
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
const markChapterReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE chapterId = ?`;
export const markChapterRead = async (chapterId) => {
@@ -84,6 +115,19 @@ export const markChapterRead = async (chapterId) => {
});
};
const markChapterUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId = ?`;
export const markChapterUnread = async (chapterId) => {
db.transaction((tx) => {
tx.executeSql(
markChapterUnreadQuery,
[chapterId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
};
const isChapterDownloadedQuery = `SELECT * FROM downloads WHERE downloadChapterId=?`;
export const isChapterDownloaded = async (chapterId) => {
@@ -108,12 +152,12 @@ export const isChapterDownloaded = async (chapterId) => {
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?)`;
export const downloadChapter = async (
extensionId,
sourceId,
novelUrl,
chapterUrl,
chapterId
) => {
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}${chapterUrl}`;
const response = await fetch(downloadUrl);
const chapter = await response.json();
@@ -147,7 +191,7 @@ export const deleteChapter = async (chapterId) => {
tx.executeSql(
updateIsDownloadedQuery,
[chapterId],
(tx, res) => console.log(`Chaptermarked not downapdeod deleted`),
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
tx.executeSql(
@@ -179,3 +223,42 @@ export const getLastReadChapter = async (novelId) => {
});
});
};
const bookmarkChapterQuery = `UPDATE chapters SET bookmark = ? WHERE chapterId = ?`;
export const bookmarkChapter = async (bookmark, chapterId) => {
db.transaction((tx) => {
tx.executeSql(
bookmarkChapterQuery,
[!bookmark, chapterId],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
const markPreviuschaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE chapterId < ? AND novelId = ?`;
export const markPreviuschaptersRead = async (chapterId, novelId) => {
db.transaction((tx) => {
tx.executeSql(
markPreviuschaptersReadQuery,
[chapterId, novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
};
const markPreviousChaptersUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId < ? AND novelId = ?`;
export const markPreviousChaptersUnread = async (chapterId, novelId) => {
db.transaction((tx) => {
tx.executeSql(
markPreviousChaptersUnreadQuery,
[chapterId, novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
};
+12 -6
View File
@@ -8,7 +8,10 @@ const getHistoryQuery = `
ON history.historyChapterId = chapters.chapterId
JOIN novels
ON history.historyNovelId = novels.novelId
ORDER BY history.historyTimeRead DESC`;
GROUP BY novels.novelId
HAVING history.historyTimeRead = MAX(history.historyTimeRead)
ORDER BY history.historyTimeRead DESC
`;
export const getHistory = async () => {
return new Promise((resolve, reject) => {
@@ -41,15 +44,18 @@ export const insertHistory = async (novelId, chapterId) => {
});
};
export const deleteHistory = async (novelId) => {
export const deleteHistory = async (historyId) => {
db.transaction((tx) => {
tx.executeSql("DELETE FROM history WHERE historyNovelId = ?", [
novelId,
]);
tx.executeSql(
"DELETE FROM history WHERE historyId = ?",
[historyId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
export const deleteAllHistory = () => {
export const deleteAllHistory = async () => {
db.transaction((tx) => {
tx.executeSql("DELETE FROM history; VACCUM;");
});
+29 -27
View File
@@ -1,7 +1,7 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const getLibraryQuery = `
const getLibraryQuery = (sort, filter) => `
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
@@ -18,15 +18,15 @@ const getLibraryQuery = `
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1
GROUP BY novels.novelId
WHERE novels.followed = 1 ${filter ? "AND " + filter : ""}
${sort ? "ORDER BY " + sort : ""}
`;
export const getLibrary = () => {
export const getLibrary = (sort, filter) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getLibraryQuery,
getLibraryQuery(sort, filter),
null,
(txObj, { rows: { _array } }) => {
// console.log(_array);
@@ -38,32 +38,34 @@ export const getLibrary = () => {
);
};
// const searchLibraryQuery = ``;
const searchLibraryQuery = (searchText, sort, filter) =>
`
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
FROM chapters
WHERE chapters.read = 0
GROUP BY chapters.novelId
) AS C
ON novels.novelId = C.novelId
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
FROM chapters
WHERE chapters.downloaded = 1
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%' OR source LIKE '%${searchText}%' ${
filter ? "AND " + filter : ""
}
${sort ? "ORDER BY " + sort : ""} `;
export const searchLibrary = (searchText) => {
export const searchLibrary = (searchText, sort, filter) => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
`
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
FROM chapters
WHERE chapters.read = 0
GROUP BY chapters.novelId
) AS C
ON novels.novelId = C.novelId
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
FROM chapters
WHERE chapters.downloaded = 1
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%'
GROUP BY novels.novelId
`,
searchLibraryQuery(searchText, sort, filter),
null,
(txObj, { rows: { _array } }) => resolve(_array),
(txObj, error) => console.log("Error ", error)
+35 -1
View File
@@ -1,7 +1,7 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import { fetchChapters } from "../../Services/Source/source";
import { fetchChapters, fetchNovel } from "../../Services/Source/source";
import { insertChapters } from "./ChapterQueries";
const insertNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
@@ -123,3 +123,37 @@ export const restoreLibrary = async (novel) => {
)
);
};
const migrateNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
export const migrateNovel = async (sourceId, novelUrl) => {
const novel = await fetchNovel(sourceId, novelUrl);
db.transaction((tx) =>
tx.executeSql(
migrateNovelQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
1,
],
async (txObj, { insertId }) => {
const chapters = await fetchChapters(
novel.sourceId,
novel.novelUrl
);
await insertChapters(insertId, chapters);
},
(txObj, error) => console.log("Error ", error)
)
);
};
+3 -2
View File
@@ -2,13 +2,14 @@ import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const getUpdatesQuery = `
SELECT chapters.chapterId, chapters.chapterUrl, chapters.chapterName, novels.novelUrl, novels.novelId, novels.novelCover, novels.novelName, novels.sourceId, updates.updateTime, updates.updateId
SELECT chapters.chapterId, chapters.read, chapters.downloaded, chapters.chapterUrl, chapters.chapterName, novels.novelUrl, novels.novelId, novels.novelCover, novels.novelName, novels.sourceId, updates.updateTime, updates.updateId
FROM updates
JOIN chapters
ON updates.chapterId = chapters.chapterId
JOIN novels
ON updates.novelId = novels.novelId
ORDER BY updates.updateTime DESC`;
ORDER BY updates.updateTime DESC
`;
export const getUpdates = async () => {
return new Promise((resolve, reject) => {
+2 -2
View File
@@ -1,8 +1,8 @@
export const createHistoryTableQuery = `
CREATE TABLE IF NOT EXISTS history(
historyId INTEGER PRIMARY KEY AUTOINCREMENT,
historyChapterId INTEGER NOT NULL,
historyNovelId INTEGER NOT NULL UNIQUE,
historyChapterId INTEGER NOT NULL UNIQUE,
historyNovelId INTEGER NOT NULL,
historyTimeRead DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (historyChapterId) REFERENCES chapters(chapterId)
ON DELETE CASCADE
+1 -1
View File
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
};
const isNewVersion = (versionTag) => {
let currentVersion = "v1.0.17";
let currentVersion = "v1.0.19";
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, "");
+67 -20
View File
@@ -1,5 +1,5 @@
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { shallowEqual, useSelector } from "react-redux";
import { showChapterTitlesAction } from "../redux/novel/novel.actions";
const useTheme = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
@@ -14,11 +14,9 @@ const useReaderSettings = () => {
};
const useSettings = () => {
const { displayMode, itemsPerRow } = useSelector(
(state) => state.settingsReducer
);
const settings = useSelector((state) => state.settingsReducer);
return { displayMode, itemsPerRow };
return settings;
};
const useLibrary = () => {
@@ -33,44 +31,88 @@ const useNovel = () => {
return novel;
};
const useChapter = () => {
const chapter = useSelector((state) => state.chapterReducer);
return chapter;
};
const findNovel = (novelId) => {
let novel = useSelector((state) => state.preferenceReducer.novelSettings);
novel = novel.find((novel) => novel.novelId === novelId);
novel = novel[novelId];
return novel;
};
const usePreferences = (novelId) => {
let sort, filter;
let sort, filter, position, showChapterTitles;
const novel = findNovel(novelId);
if (novel) {
sort = novel.sort ?? "";
filter = novel.filter ?? "";
sort = novel.sort;
filter = novel.filter;
position = novel.position;
showChapterTitles = novel.showChapterTitles;
}
return { sort, filter };
return { sort, filter, position, showChapterTitles };
};
const useSavedSettings = () => {
const settings = useSelector(
(state) => state.preferenceReducer.novelSettings
);
return settings;
};
const useContinueReading = (chapters, novelId) => {
let chapter, chapterId, novel;
let lastReadChapter, chapterId, novel, position;
novel = findNovel(novelId);
if (novel) {
chapterId = novel.lastRead;
position = novel.position;
}
if (chapterId) {
lastReadChapter = chapters.find(
(obj) => obj.chapterId === chapterId && obj.read === 0
);
}
if (!lastReadChapter) {
lastReadChapter = chapters.find((obj) => obj.read === 0);
}
return { lastReadChapter, position };
};
const usePosition = (novelId, chapterId) => {
let novel, position;
novel = findNovel(novelId);
if (novel) {
chapterId = novel.lastRead;
if (novel.position) {
position = novel.position[chapterId] || null;
}
}
if (chapterId) {
chapter = chapters.find((obj) => obj.chapterId === chapterId);
}
return position;
};
if (!chapter) {
chapter = chapters.find((obj) => obj.read === 0);
}
const useTrackingStatus = () => {
const tracker = useSelector((state) => state.trackerReducer);
return chapter;
return tracker;
};
const useLibraryFilters = () => {
const filters = useSelector((state) => state.libraryReducer.filters);
return filters;
};
export {
@@ -80,5 +122,10 @@ export {
useSettings,
usePreferences,
useContinueReading,
useTrackingStatus,
useNovel,
useChapter,
useSavedSettings,
useLibraryFilters,
usePosition,
};
+76 -72
View File
@@ -1,7 +1,8 @@
import React from "react";
import { View } from "react-native";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { useTheme } from "../Hooks/reduxHooks";
import Library from "../Screens/Library/Library";
import Updates from "../Screens/Updates/Updates";
@@ -9,83 +10,86 @@ import History from "../Screens/History/History";
import Browse from "../Screens/Browse/Browse";
import More from "../Screens/More/More";
import { useTheme } from "../Hooks/reduxHooks";
const Tab = createMaterialBottomTabNavigator();
const BottomNavigator = () => {
const theme = useTheme();
return (
<Tab.Navigator
// shifting={false}
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccent}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="History"
component={History}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="history"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
/>
</Tab.Navigator>
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Tab.Navigator
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccent}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="History"
component={History}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="history"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
/>
</Tab.Navigator>
</View>
);
};
+16 -6
View File
@@ -9,6 +9,9 @@ import Novel from "../Screens/Novel/Novel";
import Chapter from "../Screens/Chapter/Chapter";
import Extension from "../Screens/Extension/Extension";
import GlobalSearch from "../Screens/Browse/GlobalSearch";
import Migration from "../Screens/Browse/Migration";
import SourceNovels from "../Screens/Browse/SourceNovels";
import MigrateNovel from "../Screens/Browse/MigrateNovel";
import MoreStack from "./More";
import { setStatusBarStyle } from "../Hooks/setStatusBarStyle";
@@ -30,8 +33,10 @@ const MainNavigator = () => {
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
return (
<NavigationContainer>
<View style={{ flex: 1, backgroundColor: theme.colorPrimary }}>
<NavigationContainer
theme={{ colors: { background: theme.colorPrimaryDark } }}
>
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen
@@ -42,10 +47,6 @@ const MainNavigator = () => {
name="Novel"
component={Novel}
options={{
headerTitle: "",
headerShown: true,
headerTransparent: true,
headerTintColor: "white",
...TransitionPresets.RevealFromBottomAndroid,
}}
/>
@@ -60,6 +61,15 @@ const MainNavigator = () => {
name="GlobalSearch"
component={GlobalSearch}
/>
<Stack.Screen name="Migration" component={Migration} />
<Stack.Screen
name="SourceNovels"
component={SourceNovels}
/>
<Stack.Screen
name="MigrateNovel"
component={MigrateNovel}
/>
</Stack.Navigator>
</View>
</NavigationContainer>
+42 -6
View File
@@ -1,24 +1,60 @@
import React from "react";
import { View } from "react-native";
import { useTheme } from "../Hooks/reduxHooks";
import { createStackNavigator } from "@react-navigation/stack";
import About from "../Screens/More/About";
import Settings from "../Screens/More/Settings";
import Trackers from "../Screens/More/Trackers";
import { View } from "react-native";
import { useTheme } from "../Hooks/reduxHooks";
import TrackerSettings from "../Screens/More/TrackerSettings";
import ReaderSettings from "../Screens/More/ReaderSettings";
import BackupSettings from "../Screens/More/BackupSettings";
import AdvancedSettings from "../Screens/More/AdvancedSettings";
import GeneralSettings from "../Screens/More/GeneralSettings";
const Stack = createStackNavigator();
const stackNavigatorConfig = { headerShown: false };
const SettingsStack = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen
name="GeneralSettings"
component={GeneralSettings}
/>
<Stack.Screen
name="ReaderSettings"
component={ReaderSettings}
/>
<Stack.Screen
name="TrackerSettings"
component={TrackerSettings}
/>
<Stack.Screen
name="BackupSettings"
component={BackupSettings}
/>
<Stack.Screen
name="AdvancedSettings"
component={AdvancedSettings}
/>
</Stack.Navigator>
</View>
);
};
const MoreStack = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimary }}>
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen name="Trackers" component={Trackers} />
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
</Stack.Navigator>
</View>
+3 -1
View File
@@ -74,13 +74,15 @@ const Browse = ({ navigation }) => {
<Searchbar
theme={theme}
placeholder="Search Source"
left={"magnify"}
left="magnify"
onPressLeft={clearSearchbar}
searchText={searchText}
onChangeText={onChangeText}
clearSearchbar={clearSearchbar}
right="book-search"
onPressRight={() => navigation.navigate("GlobalSearch")}
migrate="swap-vertical-variant"
onPressMigrate={() => navigation.navigate("Migration")}
/>
<FlatList
+44 -5
View File
@@ -14,14 +14,22 @@ import NovelCover from "../../Components/NovelCover";
import NovelList from "../../Components/NovelList";
import EmptyView from "../../Components/EmptyView";
import GlobalSearchNovelList from "./components/GlobalSearchNovelList";
import { ProgressBar } from "react-native-paper";
import { showToast } from "../../Hooks/showToast";
const GlobalSearch = ({ navigation }) => {
const GlobalSearch = ({ route, navigation }) => {
const theme = useTheme();
let novelName = "";
if (route.params) {
novelName = route.params.novelName;
}
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState("");
const [searchText, setSearchText] = useState(novelName);
const [searchResults, setSearchResults] = useState("");
const { sources, pinned } = useSelector((state) => state.sourceReducer);
const [progress, setProgress] = useState(0);
const library = useLibrary();
@@ -29,6 +37,10 @@ const GlobalSearch = ({ navigation }) => {
(source) => pinned.indexOf(source.sourceId) !== -1
);
useEffect(() => {
novelName && onSubmitEditing();
}, []);
const clearSearchbar = () => {
setSearchText("");
};
@@ -62,8 +74,26 @@ const GlobalSearch = ({ navigation }) => {
},
]);
setLoading(false);
});
}, 3000 * index)
})
.catch((error) => {
showToast(error.message);
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
sourceLanguage: source.sourceLanguage,
novels: [],
},
]);
setLoading(false);
})
.finally(() =>
setProgress(
(progress) => progress + 1 / pinnedSources.length
)
);
}, 1000 * index)
);
};
@@ -103,7 +133,16 @@ const GlobalSearch = ({ navigation }) => {
onSubmitEditing={onSubmitEditing}
clearSearchbar={clearSearchbar}
/>
{progress < 1 && progress > 0 && pinned && (
<ProgressBar
color={
progress > 0.9
? theme.colorPrimaryDark
: theme.colorAccent
}
progress={progress}
/>
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
+168
View File
@@ -0,0 +1,168 @@
import React, { useEffect, useState } from "react";
import {
StyleSheet,
View,
Text,
FlatList,
ActivityIndicator,
} from "react-native";
import { ProgressBar } from "react-native-paper";
import { useLibrary, useTheme } from "../../Hooks/reduxHooks";
import { useSelector } from "react-redux";
import EmptyView from "../../Components/EmptyView";
import MigrationNovelList from "./components/MigrationNovelList";
import { Appbar } from "../../Components/Appbar";
import { showToast } from "../../Hooks/showToast";
const GlobalSearch = ({ navigation, route }) => {
const { sourceId, novelName } = route.params;
const theme = useTheme();
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState("");
const { sources, pinned } = useSelector((state) => state.sourceReducer);
const library = useLibrary();
const pinnedSources = sources.filter(
(source) =>
pinned.indexOf(source.sourceId) !== -1 &&
source.sourceId !== sourceId
);
const getSearchResults = () => {
const getSearchUrl = (sourceId) => {
if (sourceId === 1) {
return `https://lnreader-extensions.vercel.app/api/1/search/?s=${novelName}&?o=rating`;
} else {
return `https://lnreader-extensions.vercel.app/api/${sourceId}/search/?s=${novelName}`;
}
};
pinnedSources.map((source, index) =>
setTimeout(() => {
setLoading(true);
fetch(getSearchUrl(source.sourceId))
.then((response) => response.json())
.then((json) => {
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
sourceLanguage: source.sourceLanguage,
novels: json,
},
]);
setLoading(false);
})
.catch((error) => {
showToast(error.message);
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
sourceLanguage: source.sourceLanguage,
novels: [],
},
]);
setLoading(false);
})
.finally(() =>
setProgress(
(progress) => progress + 1 / pinnedSources.length
)
);
}, 1000 * index)
);
};
useEffect(() => {
getSearchResults();
}, []);
const renderItem = ({ item }) => (
<>
<View style={{ padding: 8, paddingVertical: 16 }}>
<Text style={{ color: theme.textColorPrimary }}>
{item.sourceName}
</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 12 }}>
{item.sourceLanguage}
</Text>
</View>
<MigrationNovelList
data={item.novels}
theme={theme}
library={library}
navigation={navigation}
/>
</>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Appbar
title={novelName}
onBackAction={() => navigation.goBack()}
/>
{progress < 1 && pinned && (
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
data={searchResults}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in your pinned sources ${
pinned.length === 0
? "(No sources pinned)"
: ""
}`}
/>
)}
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: "center",
padding: 16,
}}
>
<ActivityIndicator
size="large"
color={theme.colorAccent}
/>
</View>
)
}
/>
</View>
);
};
export default GlobalSearch;
const styles = StyleSheet.create({
container: { flex: 1 },
});
+69
View File
@@ -0,0 +1,69 @@
import React from "react";
import { StyleSheet, View, FlatList, Text } from "react-native";
import { useLibrary, useTheme } from "../../Hooks/reduxHooks";
import { useSelector } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import MigrationSourceCard from "./components/MigrationSourceCard";
const GlobalSearch = ({ navigation }) => {
const theme = useTheme();
const { sources } = useSelector((state) => state.sourceReducer);
const library = useLibrary();
const novelsPerSource = (sourceId) =>
library.filter((novel) => novel.sourceId === sourceId).length;
const renderItem = ({ item }) => (
<MigrationSourceCard
item={item}
theme={theme}
noOfNovels={novelsPerSource(item.sourceId)}
buttonLabel="All"
onPress={() => navigation.navigate("SourceNovels", item.sourceId)}
/>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Appbar
title="Select Source"
onBackAction={() => navigation.goBack()}
/>
<FlatList
data={sources}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
ListHeaderComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 20,
paddingBottom: 10,
textTransform: "uppercase",
}}
>
Select a Source To Migrate From
</Text>
}
/>
</View>
);
};
export default GlobalSearch;
const styles = StyleSheet.create({
container: { flex: 1 },
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
});
+83
View File
@@ -0,0 +1,83 @@
import React from "react";
import { StyleSheet, View, FlatList, Text } from "react-native";
import { useLibrary, useTheme } from "../../Hooks/reduxHooks";
import EmptyView from "../../Components/EmptyView";
import { Appbar } from "../../Components/Appbar";
import ListView from "../../Components/ListView";
const SourceNovels = ({ navigation, route }) => {
const sourceId = route.params;
const theme = useTheme();
const library = useLibrary();
const sourceNovels = library.filter((novel) => novel.sourceId === sourceId);
const renderItem = ({ item }) => (
<ListView
item={item}
theme={theme}
onPress={() =>
navigation.navigate("MigrateNovel", {
sourceId: item.sourceId,
novelName: item.novelName,
})
}
/>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Appbar
title="Select Novel"
onBackAction={() => navigation.goBack()}
/>
<FlatList
data={sourceNovels}
keyExtractor={(item) => item.novelId.toString()}
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 20,
textAlign: "center",
}}
>
Your library does not have any novels from this source
</Text>
}
/>
{/* <Portal>
<MigrationDialog
migrationDialog={migrationDialog}
hideMigrationDialog={hideMigrationDIalog}
theme={theme}
selectedNovel={selectedNovel}
loading={loading}
migrationNovel={migrationNovel}
/>
<SourceToMigrateDialog
selectedNovel={selectedNovel}
sourceToMigrateDialog={sourceToMigrateDialog}
hideSourceToMigrateDialog={hideSourceToMigrateDialog}
sources={sources}
theme={theme}
showMigrationDialog={showMigrationDialog}
selectSourceToMigrate={selectSourceToMigrate}
/>
</Portal> */}
</View>
);
};
export default SourceNovels;
const styles = StyleSheet.create({
container: { flex: 1 },
});
@@ -2,7 +2,7 @@ import React from "react";
import { StyleSheet, View, Text, ImageBackground } from "react-native";
import { TouchableRipple } from "react-native-paper";
import { useSettings, useTheme } from "../../../Hooks/reduxHooks";
import { useTheme } from "../../../Hooks/reduxHooks";
const GlobalSearchNovelCover = ({ item, onPress, libraryStatus }) => {
const theme = useTheme();
@@ -0,0 +1,132 @@
import React, { useState } from "react";
import { StyleSheet, FlatList, Text, View } from "react-native";
import { Portal, Modal, Button } from "react-native-paper";
import { migrateNovel } from "../../../Database/queries/NovelQueries";
import { showToast } from "../../../Hooks/showToast";
import GlobalSearchNovelCover from "./GlobalSearchNovelCover";
const MigrationNovelList = ({ data, theme, library, navigation }) => {
const [selectedNovel, setSelectedNovel] = useState(false);
const [migrateNovelDialog, setMigrateNovelDialog] = useState(false);
const showMigrateNovelDialog = () => setMigrateNovelDialog(true);
const hideMigrateNovelDialog = () => setMigrateNovelDialog(false);
const checkIFInLibrary = (sourceId, novelUrl) => {
return library.some(
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
);
};
const renderItem = ({ item }) => (
<GlobalSearchNovelCover
item={item}
onPress={() =>
showModal(item.extensionId, item.novelUrl, item.novelName)
}
libraryStatus={checkIFInLibrary(item.extensionId, item.novelUrl)}
/>
);
const showModal = (sourceId, novelUrl, novelName) => {
if (checkIFInLibrary(sourceId, novelUrl)) {
showToast("Novel already in library");
} else {
setSelectedNovel({ sourceId, novelUrl, novelName });
showMigrateNovelDialog();
}
};
return (
<>
<FlatList
contentContainerStyle={styles.flatListCont}
horizontal={true}
data={data}
keyExtractor={(item) => item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 8,
paddingVertical: 4,
}}
>
No results found
</Text>
}
/>
<Portal>
<Modal
visible={migrateNovelDialog}
onDismiss={hideMigrateNovelDialog}
contentContainerStyle={{
padding: 20,
margin: 20,
borderRadius: 6,
backgroundColor: theme.colorPrimaryDark,
}}
>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 18,
}}
>
{`Migrate ${selectedNovel.novelName}?`}
</Text>
<View
style={{
flexDirection: "row",
justifyContent: "flex-end",
}}
>
<Button
style={{ marginTop: 30 }}
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
onPress={hideMigrateNovelDialog}
>
Cancel
</Button>
<Button
style={{ marginTop: 30 }}
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => {
migrateNovel(
selectedNovel.sourceId,
selectedNovel.novelUrl
);
hideMigrateNovelDialog();
showToast(
`${selectedNovel.novelName} migrated to new source.`
);
}}
>
Migrate
</Button>
</View>
</Modal>
</Portal>
</>
);
};
export default MigrationNovelList;
const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
@@ -0,0 +1,74 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple } from "react-native-paper";
const MigrationSourceCard = ({ item, theme, noOfNovels, onPress }) => {
const { sourceId, sourceName, sourceCover, sourceLanguage, status } = item;
return (
<TouchableRipple
style={styles.extensionCard}
onPress={onPress}
rippleColor={theme.rippleColor}
>
<>
<Image
source={{ uri: sourceCover }}
style={styles.extensionIcon}
resizeMode="contain"
/>
<View style={styles.extensionDetails}>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
{sourceName} {` (${noOfNovels || 0})`}
</Text>
<View style={{ flexDirection: "row" }}>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
</View>
</View>
</View>
</>
</TouchableRipple>
);
};
export default MigrationSourceCard;
const styles = StyleSheet.create({
extensionCard: {
flexDirection: "row",
alignItems: "center",
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 8,
},
extensionDetails: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
sourceStatus: {
color: "#C14033",
fontSize: 12,
marginLeft: 5,
},
});
+238 -178
View File
@@ -1,88 +1,115 @@
import React, { useState, useEffect, useRef } from "react";
import { StyleSheet, View, Text, ActivityIndicator } from "react-native";
import { Appbar, Provider, Portal } from "react-native-paper";
import { useDispatch } from "react-redux";
import Constants from "expo-constants";
import { IconButton, Portal } from "react-native-paper";
import { CollapsibleHeaderScrollView } from "react-native-collapsible-header-views";
import BottomSheet from "./components/BottomSheet";
import { getChapterFromDB } from "../../Database/queries/ChapterQueries";
import { fetchChapter } from "../../Services/Source/source";
import { showToast } from "../../Hooks/showToast";
import {
usePosition,
useReaderSettings,
useTheme,
useTrackingStatus,
} from "../../Hooks/reduxHooks";
import { updateChaptersRead } from "../../redux/tracker/tracker.actions";
import { insertHistoryAction } from "../../redux/history/history.actions";
import {
getChapterAction,
markChapterReadAction,
} from "../../redux/novel/novel.actions";
import { connect, useSelector, useDispatch } from "react-redux";
errorTextColor,
readerBackground,
readerLineHeight,
readerTextColor,
} from "./readerStyleController";
import { markChapterReadAction } from "../../redux/novel/novel.actions";
import { saveScrollPosition } from "../../redux/preferences/preference.actions";
import { parseChapterNumber } from "../../Services/updates";
import { updateChaptersRead } from "../../redux/tracker/tracker.actions";
const ChapterItem = ({
route,
navigation,
theme,
insertHistoryAction,
getChapterAction,
chapter,
loading,
reader,
}) => {
import ChapterAppbar from "./components/ChapterAppbar";
import ReaderSheet from "./components/ReaderSheet";
import EmptyView from "../../Components/EmptyView";
import FitImage from "react-native-fit-image";
const Chapter = ({ route, navigation }) => {
const {
sourceId,
chapterId,
extensionId,
chapterUrl,
novelUrl,
novelId,
novelUrl,
novelName,
chapterName,
} = route.params;
let scrollViewRef;
let readerSheetRef = useRef(null);
let _panel = useRef(null);
const theme = useTheme();
const dispatch = useDispatch();
const reader = useReaderSettings();
const { tracker, trackedNovels } = useTrackingStatus();
const position = usePosition(novelId, chapterId);
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
const tracker = useSelector((state) => state.trackerReducer.tracker);
const isTracked = trackedNovels.find((obj) => obj.novelId === novelId);
let isTracked = false;
const [chapter, setChapter] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState();
if (!loading) {
isTracked = trackedNovels.find((obj) => obj.novelId === novelId);
}
const [scrollPercentage, setScrollPercentage] = useState(0);
const [firstLayout, setFirstLayout] = useState(true);
const [selectText, setSelectText] = useState(false);
const [images, setImages] = useState([]);
const getChapter = async (chapterId) => {
try {
if (chapterId) {
const chapterDownloaded = await getChapterFromDB(chapterId);
if (chapterDownloaded) {
setChapter(chapterDownloaded);
parseImages(chapterDownloaded.chapterText);
} else {
const res = await fetchChapter(
sourceId,
novelUrl,
chapterUrl
);
parseImages(res.chapterText);
setChapter(res);
}
} else {
const res = await fetchChapter(sourceId, novelUrl, chapterUrl);
parseImages(res.chapterText);
setChapter(res);
}
setLoading(false);
} catch (error) {
setError(error.message);
setLoading(false);
showToast(error.message);
}
};
const parseImages = (chapterText) => {
let imageLinks = chapterText.match(/\[https.*?\]/g, "");
let chapterTextWithoutLinks;
if (imageLinks) {
imageLinks = imageLinks.map((link) => link.slice(1, -1));
setImages(imageLinks);
}
};
useEffect(() => {
getChapterAction(extensionId, chapterUrl, novelUrl, chapterId);
insertHistoryAction(novelId, chapterId);
getChapter(chapterId);
dispatch(insertHistoryAction(novelId, chapterId));
}, []);
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 chapterNumber = !loading && parseChapterNumber(chapter.chapterName);
const readerLineHeight = (val) => {
const lineHeight = {
12: 20,
14: 22,
16: 25,
18: 26,
20: 28,
};
return lineHeight[val];
};
const isCloseToBottom = ({
layoutMeasurement,
contentOffset,
@@ -95,99 +122,120 @@ const ChapterItem = ({
);
};
const updateTracker = () => {
const chapterNumber = parseChapterNumber(chapterName);
isTracked &&
chapterNumber &&
Number.isInteger(chapterNumber) &&
chapterNumber > isTracked.my_list_status.num_chapters_read &&
dispatch(
updateChaptersRead(
isTracked.id,
tracker.access_token,
chapterNumber
)
);
};
const onScroll = ({ nativeEvent }) => {
const offsetY = nativeEvent.contentOffset.y;
const position =
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height;
const percentage = Math.round(
(position / nativeEvent.contentSize.height) * 100
);
setScrollPercentage(percentage);
dispatch(saveScrollPosition(offsetY, percentage, chapterId, novelId));
if (isCloseToBottom(nativeEvent)) {
dispatch(markChapterReadAction(chapterId, novelId));
updateTracker();
}
};
const scrollToSavedProgress = () => {
if (position && firstLayout) {
position.percentage < 100 &&
scrollViewRef.getNode().scrollTo({
x: 0,
y: position.position,
animated: false,
});
setFirstLayout(false);
}
};
const readerStyles = [
{
paddingVertical: 16,
paddingBottom: 32,
fontSize: reader.textSize,
color: readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize, reader.lineHeight),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
},
];
return (
<Provider>
<>
<CollapsibleHeaderScrollView
headerContainerBackgroundColor="rgba(0,0,0,0.4)"
headerContainerBackgroundColor="rgba(0,0,0,0.5)"
ref={(ref) => (scrollViewRef = ref)}
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.replace("Chapter", {
chapterUrl: chapter.prevChapter,
chapterId: chapterId - 1,
extensionId,
novelUrl,
novelId,
chapterName: chapter.chapterName,
});
}}
color="#FFFFFF"
/>
<Appbar.Action
icon="chevron-right"
size={26}
disabled={!chapter.nextChapter}
onPress={() => {
navigation.replace("Chapter", {
chapterUrl: chapter.nextChapter,
extensionId,
novelUrl,
novelId,
chapterId: chapterId + 1,
chapterName: chapter.chapterName,
});
}}
color="#FFFFFF"
/>
<Appbar.Action
icon="dots-vertical"
size={26}
onPress={() =>
_panel.current.show({ velocity: -1.5 })
}
color="#FFFFFF"
/>
</>
)}
</Appbar.Header>
<ChapterAppbar
navigation={navigation}
sourceId={sourceId}
novelId={novelId}
novelUrl={novelUrl}
novelName={novelName}
chapterName={chapterName}
chapterId={chapterId}
chapter={chapter}
theme={theme}
readerSheetRef={readerSheetRef}
/>
}
headerHeight={100}
headerHeight={Constants.statusBarHeight + 56}
contentContainerStyle={[
styles.container,
styles.screenContainer,
{ backgroundColor: readerBackground(reader.theme) },
]}
onScroll={({ nativeEvent }) => {
if (isCloseToBottom(nativeEvent)) {
dispatch(markChapterReadAction(chapterId, novelId));
if (
isTracked &&
chapterNumber &&
chapterNumber >
isTracked.my_list_status.num_chapters_read
) {
dispatch(
updateChaptersRead(
isTracked.id,
tracker.access_token,
chapterNumber
)
);
}
}
}}
onScroll={onScroll}
showsVerticalScrollIndicator={false}
>
{loading ? (
{error ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<EmptyView
icon="(-_-;)・・・"
description={error}
style={{ color: errorTextColor(reader.theme) }}
>
<IconButton
icon="reload"
size={25}
style={{ margin: 0, marginTop: 16 }}
color={errorTextColor(reader.theme)}
onPress={() => {
getChapter(chapterId);
setLoading(true);
setError();
}}
/>
<Text
style={{ color: errorTextColor(reader.theme) }}
>
Retry
</Text>
</EmptyView>
</View>
) : loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
size={50}
@@ -195,47 +243,59 @@ const ChapterItem = ({
/>
</View>
) : (
<Text
style={[
{
paddingVertical: 15,
fontSize: reader.textSize,
color: readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
},
]}
>
{chapter.chapterText.trim()}
</Text>
<View style={{ flex: 1 }}>
{images.length > 0 &&
images.map((image, index) => (
<FitImage source={{ uri: image }} />
))}
<Text
style={readerStyles}
onLayout={scrollToSavedProgress}
selectable={selectText}
>
{chapter.chapterText
.trim()
.replace(/\[https.*?\]/g, "")}
</Text>
</View>
)}
<Portal>
<BottomSheet bottomSheetRef={_panel} />
<ReaderSheet
theme={theme}
reader={reader}
dispatch={dispatch}
bottomSheetRef={readerSheetRef}
selectText={selectText}
setSelectText={setSelectText}
/>
</Portal>
</CollapsibleHeaderScrollView>
</Provider>
<Text
style={[
styles.scrollPercentage,
{ color: readerTextColor(reader.theme) },
]}
>
{scrollPercentage + "%"}
</Text>
</>
);
};
const mapStateToProps = (state) => ({
theme: state.settingsReducer.theme,
chapter: state.novelReducer.chapter,
loading: state.novelReducer.chapterLoading,
reader: state.settingsReducer.reader,
});
export default connect(mapStateToProps, {
insertHistoryAction,
getChapterAction,
})(ChapterItem);
export default Chapter;
const styles = StyleSheet.create({
container: {
screenContainer: {
flexGrow: 1,
paddingVertical: 10,
},
scrollPercentage: {
position: "absolute",
bottom: 12,
alignSelf: "center",
fontFamily: "noto-sans",
fontWeight: "bold",
zIndex: 1,
},
});
@@ -1,276 +0,0 @@
import React from "react";
import { Animated, StyleSheet, View, Text, Dimensions } from "react-native";
import { ToggleButton, IconButton, Chip } from "react-native-paper";
import Slider from "@react-native-community/slider";
import Bottomsheet from "rn-sliding-up-panel";
import { fonts } from "../../../Services/utils/constants";
import { connect } from "react-redux";
import {
setReaderFont,
updateReaderPadding,
updateReaderTextAlign,
updateReaderTextSize,
updateReaderTheme,
} from "../../../redux/settings/settings.actions";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
const ChapterBottomSheet = ({
bottomSheetRef,
theme,
reader,
updateReaderTextSize,
updateReaderTheme,
updateReaderTextAlign,
updateReaderPadding,
setReaderFont,
}) => {
return (
<Bottomsheet
animatedValue={new Animated.Value(0)}
ref={bottomSheetRef}
draggableRange={{ top: 400, bottom: 0 }}
snappingPoints={[0, 400]}
showBackdrop={false}
>
<View style={styles.contentContainer}>
<BottomSheetHandle />
<View style={styles.readerSettingsContainer}>
<Text
style={{
color: "#FFFFFF",
fontWeight: "bold",
}}
>
Text Size
</Text>
<Slider
style={{
width: Dimensions.get("window").width,
height: 40,
}}
value={reader.textSize}
minimumValue={12}
maximumValue={20}
step={2}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccent}
onValueChange={(value) => updateReaderTextSize(value)}
/>
<View
style={{
flexDirection: "row",
justifyContent: "center",
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<Text
style={{
color: "#FFFFFF",
fontWeight: "bold",
}}
>
Reader Theme
</Text>
<ToggleButton.Row
onValueChange={(value) =>
updateReaderTheme(value)
}
value={reader.theme}
style={{ marginTop: 10 }}
>
<ToggleButton
icon="format-text"
color="#FFFFFF"
value={1}
style={{
backgroundColor: "#000000",
marginHorizontal: 10,
}}
/>
<ToggleButton
icon="format-text"
color="#000000"
value={2}
style={{
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
}}
/>
<ToggleButton
icon="format-text"
color="#000000"
value={3}
style={{
backgroundColor: "#F4ECD8",
marginHorizontal: 10,
}}
/>
</ToggleButton.Row>
</View>
<View
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<Text
style={{
color: "#FFFFFF",
fontWeight: "bold",
}}
>
Text Align
</Text>
<ToggleButton.Row
onValueChange={(value) =>
updateReaderTextAlign(value ?? "left")
}
value={reader.textAlign}
style={{ marginTop: 10 }}
>
<ToggleButton
icon="format-align-left"
color="#000000"
value="left"
style={{
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
}}
/>
<ToggleButton
icon="format-align-justify"
color="#000000"
value="justify"
style={{
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
}}
/>
<ToggleButton
icon="format-align-right"
color="#000000"
value="right"
style={{
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
}}
/>
</ToggleButton.Row>
</View>
</View>
<Text
style={{
color: "#FFFFFF",
fontWeight: "bold",
marginTop: 10,
}}
>
Padding
</Text>
<View
style={{
width: "100%",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
}}
>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
updateReaderPadding(reader.padding - 1)
}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 50,
}}
>
{`${reader.padding}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
updateReaderPadding(reader.padding + 1)
}
/>
</View>
<Text
style={{
color: "#FFFFFF",
fontWeight: "bold",
marginTop: 10,
}}
>
Font Style
</Text>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "center",
}}
>
{fonts.map((font, index) => (
<Chip
key={index}
onPress={() => setReaderFont(font.fontFamily)}
textStyle={{ fontFamily: font.fontFamily }}
style={{
borderWidth: 0,
marginHorizontal: 5,
marginVertical: 5,
}}
selected={
reader.fontFamily === font.fontFamily
? true
: false
}
>
{font.name}
</Chip>
))}
</View>
</View>
</View>
</Bottomsheet>
);
};
const mapStateToProps = (state) => ({
theme: state.settingsReducer.theme,
reader: state.settingsReducer.reader,
});
export default connect(mapStateToProps, {
updateReaderTextSize,
updateReaderTheme,
updateReaderTextAlign,
updateReaderPadding,
setReaderFont,
})(ChapterBottomSheet);
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
backgroundColor: "rgba(0,0,0,0.4)",
},
readerSettingsContainer: { flex: 1, alignItems: "center", paddingTop: 30 },
});
@@ -0,0 +1,95 @@
import React, { useEffect, useState } from "react";
import { Appbar } from "react-native-paper";
import {
getNextChapterFromDB,
getPrevChapterFromDB,
} from "../../../Database/queries/ChapterQueries";
const ChapterAppbar = ({
navigation,
sourceId,
novelId,
novelName,
novelUrl,
chapterId,
chapterName,
theme,
readerSheetRef,
}) => {
const [nextChapter, setNextChapter] = useState({});
const [prevChapter, setPrevChapter] = useState({});
const setPrevAndNextChap = async () => {
const nextChap = await getNextChapterFromDB(novelId, chapterId);
const prevChap = await getPrevChapterFromDB(novelId, chapterId);
setNextChapter(nextChap);
setPrevChapter(prevChap);
};
useEffect(() => {
setPrevAndNextChap();
}, []);
const navigateToPrevChapter = () =>
navigation.replace("Chapter", {
chapterUrl: prevChapter.chapterUrl,
chapterId: prevChapter.chapterId,
sourceId,
novelUrl,
novelId,
chapterName: prevChapter.chapterName,
novelName,
});
const navigateToNextChapter = () =>
navigation.replace("Chapter", {
chapterUrl: nextChapter.chapterUrl,
sourceId,
novelUrl,
novelId,
chapterId: nextChapter.chapterId,
chapterName: nextChapter.chapterName,
novelName,
});
return (
<Appbar.Header style={{ backgroundColor: "transparent", elevation: 0 }}>
<Appbar.BackAction
onPress={() => navigation.goBack()}
color="#FFFFFF"
size={26}
style={{ marginRight: 0 }}
/>
<Appbar.Content
title={novelName}
titleStyle={{ color: "#FFFFFF" }}
subtitle={chapterName}
subtitleStyle={{ color: "rgba(255, 255, 255, 0.7)" }}
/>
<Appbar.Action
icon="chevron-left"
size={26}
disabled={!prevChapter}
onPress={navigateToPrevChapter}
color="#FFFFFF"
/>
<Appbar.Action
icon="chevron-right"
size={26}
disabled={!nextChapter}
onPress={navigateToNextChapter}
color="#FFFFFF"
/>
<Appbar.Action
icon="dots-vertical"
size={26}
onPress={() => readerSheetRef.current.show()}
color="#FFFFFF"
/>
</Appbar.Header>
);
};
export default ChapterAppbar;
@@ -0,0 +1,403 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Dimensions } from "react-native";
import { ToggleButton, IconButton, Menu, Switch } from "react-native-paper";
import Slider from "@react-native-community/slider";
import Bottomsheet from "rn-sliding-up-panel";
import { fonts } from "../../../Services/utils/constants";
import { setReaderSettings } from "../../../redux/settings/settings.actions";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
import { Row } from "../../../Components/Common";
const ReaderSheet = ({
theme,
reader,
dispatch,
bottomSheetRef,
selectText,
setSelectText,
}) => {
const [fontMenu, setFontMenu] = useState(false);
const openFontMenu = () => setFontMenu(true);
const closeFontMenu = () => setFontMenu(false);
const ReaderSettingTitle = ({ title }) => (
<Text style={styles.title}>{title}</Text>
);
return (
<Bottomsheet
ref={bottomSheetRef}
draggableRange={{ top: 420, bottom: 0 }}
snappingPoints={[0, 420]}
showBackdrop={true}
backdropOpacity={0}
>
<View style={styles.contentContainer}>
<BottomSheetHandle theme={theme} />
<View style={styles.readerSettingsContainer}>
<ReaderSettingTitle title="Text Size" />
<Slider
style={{
width: Dimensions.get("window").width,
height: 40,
}}
value={reader.textSize}
minimumValue={12}
maximumValue={20}
step={2}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccent}
onValueChange={(value) =>
dispatch(setReaderSettings("textSize", value))
}
/>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
}}
>
<ReaderSettingTitle title="Reader Theme" />
<Row>
<ToggleButton.Row
onValueChange={(value) => {
dispatch(
setReaderSettings(
"theme",
value ?? reader.theme
)
);
}}
value={reader.theme}
>
<ToggleButton
icon={reader.theme === 1 && "check"}
color="#FFFFFF"
value={1}
style={{
backgroundColor: "#000000",
marginHorizontal: 10,
borderRadius: 50,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
}}
/>
<ToggleButton
icon={reader.theme === 4 && "check"}
color="#FFFFFF"
value={4}
style={{
backgroundColor: "#444444",
marginHorizontal: 10,
borderWidth: 0,
borderRadius: 50,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
}}
/>
<ToggleButton
icon={reader.theme === 2 && "check"}
color="#000000"
value={2}
style={{
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
borderRadius: 50,
}}
/>
<ToggleButton
icon={reader.theme === 3 && "check"}
color="#000000"
value={3}
style={{
backgroundColor: "#F4ECD8",
marginHorizontal: 10,
borderRadius: 50,
borderTopLeftRadius: 50,
borderBottomLeftRadius: 50,
}}
/>
</ToggleButton.Row>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Text Align" />
<Row>
<ToggleButton.Row
onValueChange={(value) =>
dispatch(
setReaderSettings(
"textAlign",
value ?? reader.textAlign
)
)
}
value={reader.textAlign}
>
<ToggleButton
icon="format-align-left"
color={
reader.textAlign === "left"
? theme.colorAccent
: "#FFFFFF"
}
value="left"
style={{
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
}}
/>
<ToggleButton
icon="format-align-justify"
color={
reader.textAlign === "justify"
? theme.colorAccent
: "#FFFFFF"
}
value="justify"
style={{
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderRadius: 4,
}}
/>
<ToggleButton
icon="format-align-right"
color={
reader.textAlign === "right"
? theme.colorAccent
: "#FFFFFF"
}
value="right"
style={{
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
}}
/>
</ToggleButton.Row>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Padding" />
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"padding",
reader.padding - 1
)
)
}
style={{ marginVertical: 0 }}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 24,
}}
>
{`${reader.padding}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"padding",
reader.padding + 1
)
)
}
style={{ marginVertical: 0 }}
/>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Line Height" />
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={
reader.lineHeight <= 1.3 ? true : false
}
onPress={() =>
dispatch(
setReaderSettings(
"lineHeight",
reader.lineHeight - 0.1
)
)
}
style={{ marginVertical: 0 }}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 24,
}}
>
{`${Math.round(reader.lineHeight * 10) / 10}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.lineHeight >= 2 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"lineHeight",
reader.lineHeight + 0.1
)
)
}
style={{ marginVertical: 0 }}
/>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Font Style" />
<Menu
visible={fontMenu}
onDismiss={closeFontMenu}
contentStyle={{ backgroundColor: theme.menuColor }}
anchor={
<Text
onPress={openFontMenu}
style={{
color: "white",
paddingHorizontal: 16,
fontSize: 16,
fontFamily: reader.fontFamily,
}}
>
{fonts.find(
(font) =>
font.fontFamily ===
reader.fontFamily
).name || "Default"}
</Text>
}
>
{fonts.map((font) => (
<Menu.Item
key={font.fontFamily}
onPress={() =>
dispatch(
setReaderSettings(
"fontFamily",
font.fontFamily
)
)
}
icon={
font.fontFamily === reader.fontFamily &&
"check"
}
title={font.name}
style={{ backgroundColor: theme.menuColor }}
titleStyle={{
color: theme.textColorPrimary,
}}
/>
))}
</Menu>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<View>
<ReaderSettingTitle title="Select Text " />
<Text
style={{
fontSize: 14,
color: "white",
}}
>
(Justify does not work with selectable text)
</Text>
</View>
<Switch
value={selectText}
onValueChange={setSelectText}
color={theme.colorAccent}
/>
</Row>
</View>
</View>
</Bottomsheet>
);
};
export default ReaderSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
backgroundColor: "rgba(0,0,0,0.5)",
},
readerSettingsContainer: {
flex: 1,
alignItems: "center",
paddingTop: 30,
},
title: {
color: "#FFFFFF",
fontWeight: "bold",
},
});
@@ -0,0 +1,31 @@
export const readerBackground = (val) => {
const backgroundColor = {
1: "#000000",
2: "#FFFFFF",
3: "#F4ECD8",
4: "#444444",
};
return backgroundColor[val] ?? "#FFFFFF";
};
export const readerTextColor = (val) => {
const textColor =
val === 1 ? "rgba(255,255,255,0.7)" : val === 4 ? "#FFFFFF" : "#000000";
return textColor;
};
export const readerLineHeight = (fontSize, lineHeightMultiplier) => {
let lineHeight = fontSize * lineHeightMultiplier;
lineHeight = Math.round(lineHeight);
return lineHeight;
};
export const errorTextColor = (val) => {
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.54)";
return textColor;
};
+35 -11
View File
@@ -8,6 +8,8 @@ import EmptyView from "../../Components/EmptyView";
import { Searchbar } from "../../Components/Searchbar";
import { useTheme, useLibrary } from "../../Hooks/reduxHooks";
import NovelList from "../../Components/NovelList";
import { showToast } from "../../Hooks/showToast";
import ErrorView from "../../Components/ErrorView";
const Extension = ({ navigation, route }) => {
const { sourceId, sourceName, sourceUrl } = route.params;
@@ -17,6 +19,7 @@ const Extension = ({ navigation, route }) => {
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState();
const [error, setError] = useState();
const [searchText, setSearchText] = useState("");
@@ -36,13 +39,21 @@ const Extension = ({ navigation, route }) => {
}
};
const getNovels = () => {
fetch(getSourceUrl())
.then((response) => response.json())
.then((json) => {
setNovels(json);
setLoading(false);
});
const getNovels = async () => {
try {
const url = getSourceUrl();
const res = await fetch(url);
const data = await res.json();
setNovels(data);
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
const clearSearchbar = () => {
@@ -58,6 +69,11 @@ const Extension = ({ navigation, route }) => {
.then((json) => {
setNovels(json);
setLoading(false);
})
.catch((error) => {
setNovels([]);
setLoading(false);
showToast(error.message);
});
};
@@ -86,10 +102,18 @@ const Extension = ({ navigation, route }) => {
/>
);
const listEmptyComponent = () =>
novels.length === 0 && (
<EmptyView icon="(; ̄Д ̄)" description="No results found" />
);
const listEmptyComponent = () => (
<ErrorView
error={error || "No results found"}
onRetry={() => {
getNovels();
setLoading(true);
setError();
}}
openWebView={() => WebBrowser.openBrowserAsync(sourceUrl)}
theme={theme}
/>
);
return (
<View
+146 -62
View File
@@ -1,87 +1,171 @@
import React, { useEffect } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import React, { useState, useEffect } from "react";
import { StyleSheet, FlatList, ActivityIndicator, Text } from "react-native";
import { Appbar } from "../../Components/Appbar";
import HistoryCard from "./components/HistoryCard";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import EmptyView from "../../Components/EmptyView";
import {
getHistoryAction,
deleteHistoryAction,
clearAllHistoryAction,
} from "../../redux/history/history.actions";
const History = ({
navigation,
theme,
history,
loading,
getHistoryAction,
deleteHistoryAction,
}) => {
import moment from "moment";
import { dateFormat } from "../../Services/utils/constants";
import { ScreenContainer } from "../../Components/Common";
import { Searchbar } from "../../Components/Searchbar";
import { useTheme } from "../../Hooks/reduxHooks";
import { Button, Dialog, Portal } from "react-native-paper";
const History = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { history, loading } = useSelector((state) => state.historyReducer);
const [searchText, setSearchText] = useState("");
const [searchResults, setSearchResults] = useState([]);
/**
* Confirm Clear History Dialog
*/
const [visible, setVisible] = useState(false);
const showDialog = () => setVisible(true);
const hideDialog = () => setVisible(false);
useEffect(() => {
getHistoryAction();
dispatch(getHistoryAction());
}, [getHistoryAction]);
const renderHistoryCard = ({ item }) => (
<HistoryCard
dispatch={dispatch}
item={item}
theme={theme}
deleteHistoryAction={deleteHistoryAction}
navigation={navigation}
/>
);
const clearSearchbar = () => setSearchText("");
const onChangeText = (text) => {
setSearchText(text);
let results = [];
text !== "" &&
history.map((item) => {
const date = item.date;
const novels = item.novels.filter((novel) =>
novel.novelName.toLowerCase().includes(text.toLowerCase())
);
if (novels.length > 0) {
results.push({
date,
novels,
});
}
});
setSearchResults(results);
};
return (
<>
<Appbar title="History" />
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
contentContainerStyle={{ flexGrow: 1, padding: 8 }}
data={history}
keyExtractor={(item) => item.novelUrl.toString()}
renderItem={renderHistoryCard}
ListFooterComponent={
loading && (
<ActivityIndicator
size="small"
color={theme.colorAccent}
/>
)
}
ListEmptyComponent={
!loading && (
<EmptyView
icon="(˘・_・˘)"
description="Nothing read recently"
/>
)
}
/>
</View>
</>
<ScreenContainer theme={theme}>
<Searchbar
placeholder="Search History"
searchText={searchText}
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
left="magnify"
theme={theme}
right="trash-can-outline"
onPressRight={showDialog}
/>
<FlatList
contentContainerStyle={{
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 16,
}}
data={searchText ? searchResults : history}
keyExtractor={(index) => index.toString()}
renderItem={({ item }) => (
<FlatList
keyExtractor={(item, index) => index.toString()}
data={item.novels}
renderItem={renderHistoryCard}
ListHeaderComponent={
<Text
style={{
textTransform: "uppercase",
paddingVertical: 8,
color: theme.textColorSecondary,
}}
>
{moment(item.date).calendar(null, dateFormat)}
</Text>
}
/>
)}
ListFooterComponent={
loading && (
<ActivityIndicator
size="small"
color={theme.colorAccent}
/>
)
}
ListEmptyComponent={
<EmptyView
icon="(˘・_・˘)"
description="Nothing read recently"
/>
}
/>
<Portal>
<Dialog
visible={visible}
onDismiss={hideDialog}
style={{
borderRadius: 6,
backgroundColor: theme.colorPrimary,
}}
>
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
color: theme.textColorPrimary,
}}
>
Are you sure? All history will be lost.
</Dialog.Title>
<Dialog.Actions>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={hideDialog}
>
Cancel
</Button>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => {
dispatch(clearAllHistoryAction());
hideDialog();
}}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</ScreenContainer>
);
};
const mapStateToProps = (state) => ({
theme: state.settingsReducer.theme,
history: state.historyReducer.history,
loading: state.historyReducer.loading,
});
export default History;
export default connect(mapStateToProps, {
getHistoryAction,
deleteHistoryAction,
})(History);
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
const styles = StyleSheet.create({});
+15 -9
View File
@@ -7,9 +7,10 @@ import moment from "moment";
import { useDispatch } from "react-redux";
import { setNovel } from "../../../redux/novel/novel.actions";
import { parseChapterNumber } from "../../../Services/utils/helpers";
import { deleteHistoryAction } from "../../../redux/history/history.actions";
const HistoryCard = ({ item, deleteHistoryAction, navigation, theme }) => {
const dispatch = useDispatch();
const HistoryCard = ({ item, navigation, theme, dispatch }) => {
return (
<TouchableRipple
style={styles.historyCard}
@@ -44,9 +45,11 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation, theme }) => {
}}
numberOfLines={1}
>
{`${item.chapterName} - ${moment(
item.historyTimeRead
).calendar()}`}
{`Ch. ${parseChapterNumber(
item.chapterName
)} - ${moment(item.historyTimeRead)
.format("h:mm a")
.toUpperCase()}`}
</Text>
</View>
<View
@@ -62,21 +65,24 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation, theme }) => {
style={{
marginRight: 0,
}}
onPress={() => deleteHistoryAction(item.novelId)}
onPress={() =>
dispatch(deleteHistoryAction(item.historyId))
}
/>
<IconButton
icon="play"
size={24}
color={theme.textColorPrimary}
style={{ marginRight: 10 }}
style={{ marginRight: 0 }}
onPress={() =>
navigation.navigate("Chapter", {
chapterId: item.historyChapterId,
chapterUrl: item.chapterUrl,
extensionId: item.sourceId,
sourceId: item.sourceId,
novelUrl: item.novelUrl,
novelId: item.novelId,
chapterName: item.chapterName,
novelName: item.novelName,
})
}
/>
@@ -91,7 +97,7 @@ export default HistoryCard;
const styles = StyleSheet.create({
historyCard: {
marginBottom: 15,
marginVertical: 8,
borderRadius: 4,
flexDirection: "row",
alignItems: "center",
+69 -23
View File
@@ -1,11 +1,13 @@
import React, { useState, useEffect } from "react";
import React, { useState, useCallback, useRef } from "react";
import {
StyleSheet,
Text,
View,
RefreshControl,
ActivityIndicator,
TouchableOpacity,
} from "react-native";
import { useFocusEffect } from "@react-navigation/native";
import { useSelector, useDispatch } from "react-redux";
import { Searchbar } from "../../Components/Searchbar";
@@ -18,21 +20,33 @@ import {
searchLibraryAction,
} from "../../redux/library/library.actions";
import { updateLibraryAction } from "../../redux/updates/updates.actions";
import { useTheme } from "../../Hooks/reduxHooks";
import { useSettings, useTheme } from "../../Hooks/reduxHooks";
import { setNovel } from "../../redux/novel/novel.actions";
import { Portal, TouchableRipple } from "react-native-paper";
import LibraryFilterSheet from "./components/LibraryFilterSheet";
const LibraryScreen = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { showNumberOfNovels } = useSettings();
const { loading, novels } = useSelector((state) => state.libraryReducer);
const libraryFilterSheetRef = useRef(null);
const {
loading,
novels,
filters: { sort, filter },
} = useSelector((state) => state.libraryReducer);
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
useEffect(() => {
dispatch(getLibraryAction());
}, [getLibraryAction]);
useFocusEffect(
useCallback(() => {
setSearchText("");
dispatch(getLibraryAction(sort, filter));
}, [getLibraryAction, sort, filter])
);
const onRefresh = () => {
setRefreshing(true);
@@ -41,13 +55,13 @@ const LibraryScreen = ({ navigation }) => {
};
const clearSearchbar = () => {
dispatch(getLibraryAction());
dispatch(getLibraryAction(sort, filter));
setSearchText("");
};
const onChangeText = (text) => {
setSearchText(text);
dispatch(searchLibraryAction(text));
dispatch(searchLibraryAction(text, sort, filter));
};
const renderItem = ({ item }) => (
@@ -69,12 +83,12 @@ const LibraryScreen = ({ navigation }) => {
/>
);
// if (!loading) {
// console.log(novels);
// }
const listEmptyComponent = () =>
searchText !== "" ? (
<Text style={[styles.emptySearch, { color: theme.colorAccent }]}>
{`"${searchText}" not in library`}
</Text>
) : (
searchText === "" && (
<EmptyView
icon="Σ(ಠ_ಠ)"
description="Your library is empty. Add series to your library from Browse."
@@ -90,22 +104,54 @@ const LibraryScreen = ({ navigation }) => {
]}
>
<Searchbar
placeholder="Search Library"
placeholder={`Search Library ${
showNumberOfNovels ? "(" + novels.length + ")" : ""
}`}
searchText={searchText}
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
left="magnify"
right="filter-variant"
filter={filter}
onPressRight={() => libraryFilterSheetRef.current.show()}
theme={theme}
/>
{loading ? (
<ActivityIndicator size="small" color={theme.colorAccent} />
) : (
<NovelList
data={novels}
renderItem={renderItem}
refreshControl={refreshControl()}
ListEmptyComponent={listEmptyComponent}
/>
<>
{searchText !== "" && (
<TouchableRipple
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: searchText,
})
}
borderless
rippleColor={theme.colorAccent}
style={styles.emptySearch}
>
<Text style={{ color: theme.colorAccent }}>
{`Search for "${searchText}" globally`}
</Text>
</TouchableRipple>
)}
<NovelList
data={novels}
renderItem={renderItem}
refreshControl={refreshControl()}
ListEmptyComponent={listEmptyComponent}
/>
<Portal>
<LibraryFilterSheet
bottomSheetRef={libraryFilterSheetRef}
theme={theme}
dispatch={dispatch}
sort={sort}
filter={filter}
/>
</Portal>
</>
)}
</View>
</>
@@ -119,8 +165,8 @@ const styles = StyleSheet.create({
flex: 1,
},
emptySearch: {
marginTop: 8,
textAlign: "center",
paddingHorizontal: 16,
alignItems: "center",
paddingVertical: 8,
borderRadius: 4,
},
});
@@ -0,0 +1,362 @@
import React, { useState } from "react";
import {
StyleSheet,
View,
Text,
Animated,
useWindowDimensions,
} from "react-native";
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
import { Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import { ListItem } from "../../../Components/List";
import { filterLibrary } from "../../../redux/library/library.actions";
import { useSettings } from "../../../Hooks/reduxHooks";
import { setAppSettings } from "../../../redux/settings/settings.actions";
const LibraryFilterSheet = ({
bottomSheetRef,
dispatch,
sort,
filter,
theme,
}) => {
const [animatedValue] = useState(new Animated.Value(0));
const FirstRoute = () => (
<View style={{ flex: 1 }}>
<Checkbox.Item
label="Downloaded"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersDownloaded > 0"
? "checked"
: "unchecked"
}
onPress={() =>
filter === "chaptersDownloaded > 0"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(sort, "chaptersDownloaded > 0")
)
}
/>
<Checkbox.Item
label="Unread"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={filter === "unread = 1" ? "checked" : "unchecked"}
onPress={() =>
filter === "unread = 1"
? dispatch(filterLibrary(sort, ""))
: dispatch(filterLibrary(sort, "unread = 1"))
}
/>
<Checkbox.Item
label="Completed"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersUnread IS NULL"
? "checked"
: "unchecked"
}
onPress={() =>
filter === "chaptersUnread IS NULL"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(sort, "chaptersUnread IS NULL")
)
}
/>
</View>
);
const SecondRoute = () => (
<View style={{ flex: 1 }}>
<ListItem
style={{ paddingVertical: 10 }}
title="Date Added"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.novelId ASC"
? "arrow-up"
: sort === "novels.novelId DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.novelId ASC"
? dispatch(filterLibrary("novels.novelId DESC", filter))
: dispatch(filterLibrary("novels.novelId ASC", filter))
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Alphabetically"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.novelName ASC"
? "arrow-up"
: sort === "novels.novelName DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.novelName ASC"
? dispatch(
filterLibrary("novels.novelName DESC", filter)
)
: dispatch(
filterLibrary("novels.novelName ASC", filter)
)
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Unread"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.unread ASC"
? "arrow-up"
: sort === "novels.unread DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.unread ASC"
? dispatch(filterLibrary("novels.unread DESC", filter))
: dispatch(filterLibrary("novels.unread ASC", filter))
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Downloaded"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "chaptersDownloaded ASC"
? "arrow-up"
: sort === "chaptersDownloaded DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "chaptersDownloaded ASC"
? dispatch(
filterLibrary("chaptersDownloaded DESC", filter)
)
: dispatch(
filterLibrary("chaptersDownloaded ASC", filter)
)
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Total Chapters"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "chaptersUnread ASC"
? "arrow-up"
: sort === "chaptersUnread DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "chaptersUnread ASC"
? dispatch(filterLibrary("chaptersUnread DESC", filter))
: dispatch(filterLibrary("chaptersUnread ASC", filter))
}
/>
</View>
);
const displayModes = [
{ displayMode: 0, label: "Compact Grid" },
{ displayMode: 1, label: "Comfortable Grid" },
{ displayMode: 2, label: "List" },
];
const {
displayMode,
showDownloadBadges,
showUnreadBadges,
showNumberOfNovels,
} = useSettings();
const renderCheckboxes = () => {
return displayModes.map((mode) => (
<Checkbox.Item
key={mode.displayMode}
label={mode.label}
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={
displayMode === mode.displayMode ? "checked" : "unchecked"
}
// mode="ios"
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(setAppSettings("displayMode", mode.displayMode))
}
/>
));
};
const ThirdRoute = () => (
<View style={{ flex: 1 }}>
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 16,
textTransform: "uppercase",
}}
>
Display Mode
</Text>
{renderCheckboxes()}
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 16,
textTransform: "uppercase",
}}
>
Badges
</Text>
<Checkbox.Item
label="Download Badges"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showDownloadBadges ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(
setAppSettings(
"showDownloadBadges",
!showDownloadBadges
)
)
}
/>
<Checkbox.Item
label="Unread Badges"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showUnreadBadges ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(
setAppSettings("showUnreadBadges", !showUnreadBadges)
)
}
/>
<Checkbox.Item
label="Show number of items"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showNumberOfNovels ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(
setAppSettings(
"showNumberOfNovels",
!showNumberOfNovels
)
)
}
/>
</View>
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
third: ThirdRoute,
});
const layout = useWindowDimensions();
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Filter" },
{ key: "second", title: "Sort" },
{ key: "third", title: "Display" },
]);
const renderTabBar = (props) => (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: theme.colorAccent }}
style={{ backgroundColor: theme.colorPrimary }}
renderLabel={({ route, focused, color }) => (
<Text style={{ color, margin: 8 }}>{route.title}</Text>
)}
inactiveColor={theme.textColorSecondary}
activeColor={theme.colorAccent}
pressColor={theme.rippleColor}
/>
);
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 470, bottom: 0 }}
snappingPoints={[0, 470]}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<TabView
navigationState={{ index, routes }}
renderTabBar={renderTabBar}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
style={{ borderTopRightRadius: 8, borderTopLeftRadius: 8 }}
/>
</View>
</Bottomsheet>
);
};
export default LibraryFilterSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
});
+5 -5
View File
@@ -17,12 +17,12 @@ const AboutScreen = ({ navigation }) => {
<ListSection>
<ListItem
title="Version"
description="Stable 1.0.17"
description="Stable 1.0.19"
theme={theme}
/>
<ListItem
title="Build Time"
description="06-05-21 21:00 PM"
description="31-05-21 21:30 PM"
theme={theme}
/>
<ListItem
@@ -54,11 +54,11 @@ const AboutScreen = ({ navigation }) => {
theme={theme}
/>
<ListItem
title="Extensions"
description="https://github.com/rajarsheechatterjee/lnreader-extensions"
title="Sources"
description="https://github.com/rajarsheechatterjee/lnreader-sources"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader-extensions"
"https://github.com/rajarsheechatterjee/lnreader-sources"
)
}
theme={theme}
+129
View File
@@ -0,0 +1,129 @@
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import { ScreenContainer } from "../../Components/Common";
import { ListItem, ListSection, ListSubHeader } from "../../Components/List";
import { useTheme } from "../../Hooks/reduxHooks";
import { clearAllHistoryAction } from "../../redux/history/history.actions";
import { deleteNovelCache } from "../../Database/queries/NovelQueries";
import { Button, Dialog, Portal } from "react-native-paper";
const AdvancedSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const [visible, setVisible] = useState(false);
const showDialog = () => setVisible(true);
const hideDialog = () => setVisible(false);
/**
* Confirm Clear Database Dialog
*/
const [clearDatabaseDialog, setClearDatabaseDialog] = useState(false);
const showClearDatabaseDialog = () => setClearDatabaseDialog(true);
const hideClearDatabaseDialog = () => setClearDatabaseDialog(false);
return (
<ScreenContainer theme={theme}>
<Appbar title="Advanced" onBackAction={() => navigation.goBack()} />
<ListSection>
<ListSubHeader theme={theme}>Data</ListSubHeader>
<ListItem
title="Clear database"
description="Delete history for novels not in your library"
onPress={showClearDatabaseDialog}
theme={theme}
/>
<ListItem
title="Clear history"
description="Delete reading history for all novels"
onPress={showDialog}
theme={theme}
/>
</ListSection>
<Portal>
<Dialog
visible={visible}
onDismiss={hideDialog}
style={{
borderRadius: 6,
backgroundColor: theme.colorPrimary,
}}
>
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
color: theme.textColorPrimary,
}}
>
Are you sure? All history will be lost.
</Dialog.Title>
<Dialog.Actions>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={hideDialog}
>
Cancel
</Button>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => {
dispatch(clearAllHistoryAction());
hideDialog();
}}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
<Dialog
visible={clearDatabaseDialog}
onDismiss={hideClearDatabaseDialog}
style={{
borderRadius: 6,
backgroundColor: theme.colorPrimary,
}}
>
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
lineHeight: 16 * 1.5,
color: theme.textColorPrimary,
}}
>
Are you sure? Read chapters and progress of non-library
novels will be lost.
</Dialog.Title>
<Dialog.Actions>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={hideClearDatabaseDialog}
>
Cancel
</Button>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => {
deleteNovelCache();
hideClearDatabaseDialog();
}}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</ScreenContainer>
);
};
export default AdvancedSettings;
+54
View File
@@ -0,0 +1,54 @@
import React from "react";
import { useDispatch } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import {
InfoItem,
ListItem,
ListSection,
ListSubHeader,
} from "../../Components/List";
import { ScreenContainer } from "../../Components/Common";
import { useTheme } from "../../Hooks/reduxHooks";
import { restoreLibraryAction } from "../../redux/library/library.actions";
import { createBackup } from "../../Services/backup";
const BackupSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
return (
<>
<ScreenContainer theme={theme}>
<Appbar
title="Backup"
onBackAction={() => navigation.goBack()}
/>
<ListSection>
<ListSubHeader theme={theme}>Backup</ListSubHeader>
<ListItem
title="Create backup"
description="Can be used to restore current library"
onPress={createBackup}
theme={theme}
/>
<ListItem
title="Restore backup"
description="Restore library from backup file"
onPress={() => dispatch(restoreLibraryAction())}
theme={theme}
/>
<InfoItem
title="Create backup may not work on devices with Android 9 or lower."
icon="information-outline"
description="Restore library from backup file"
theme={theme}
/>
</ListSection>
</ScreenContainer>
</>
);
};
export default BackupSettings;
+129
View File
@@ -0,0 +1,129 @@
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import {
Divider,
ListItem,
ListSection,
ListSubHeader,
} from "../../Components/List";
import { ScreenContainer } from "../../Components/Common";
import { useTheme } from "../../Hooks/reduxHooks";
import DisplayModeModal from "./components/DisplayModeModal";
import GridSizeModal from "./components/GridSizeModal";
import ThemeModal from "./components/ThemeModal";
import AccentColorModal from "./components/AccentColorModal";
const GenralSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { displayMode, novelsPerRow } = useSelector(
(state) => state.settingsReducer
);
const displayModeLabel = (displayMode) => {
const label = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
return label[displayMode] ?? "Compact Grid";
};
/**
* Display Mode Modal
*/
const [displayModalVisible, setDisplayModalVisible] = useState(false);
const showDisplayModal = () => setDisplayModalVisible(true);
const hideDisplayModal = () => setDisplayModalVisible(false);
/**
* Theme Modal
*/
const [themeModalVisible, setthemeModalVisible] = useState(false);
const showthemeModal = () => setthemeModalVisible(true);
const hidethemeModal = () => setthemeModalVisible(false);
/**
* Grid Size Modal
*/
const [gridSizeModalVisible, setGridSizeModalVisible] = useState(false);
const showGridSizeModal = () => setGridSizeModalVisible(true);
const hideGridSizeModal = () => setGridSizeModalVisible(false);
/**
* Accent Color Modal
*/
const [accentColorModal, setAccentColorModal] = useState(false);
const showAccentColorModal = () => setAccentColorModal(true);
const hideAccentColorModal = () => setAccentColorModal(false);
return (
<ScreenContainer theme={theme}>
<Appbar title="General" onBackAction={() => navigation.goBack()} />
<ListSection>
<ListSubHeader theme={theme}>Display</ListSubHeader>
<ListItem
title="Display Mode"
description={displayModeLabel(displayMode)}
onPress={showDisplayModal}
theme={theme}
/>
<ListItem
title="Items per row in library"
description={`${novelsPerRow} items per row`}
onPress={showGridSizeModal}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Theme</ListSubHeader>
<ListItem
title="Theme"
description={theme.name}
onPress={showthemeModal}
theme={theme}
/>
<ListItem
title="Accent Color"
description={theme.colorAccent}
onPress={showAccentColorModal}
theme={theme}
iconColor={theme.colorAccent}
right="circle"
/>
</ListSection>
<DisplayModeModal
displayMode={displayMode}
displayModalVisible={displayModalVisible}
hideDisplayModal={hideDisplayModal}
dispatch={dispatch}
theme={theme}
/>
<GridSizeModal
dispatch={dispatch}
novelsPerRow={novelsPerRow}
gridSizeModalVisible={gridSizeModalVisible}
hideGridSizeModal={hideGridSizeModal}
theme={theme}
/>
<ThemeModal
dispatch={dispatch}
themeModalVisible={themeModalVisible}
hidethemeModal={hidethemeModal}
theme={theme}
/>
<AccentColorModal
dispatch={dispatch}
accentColorModal={accentColorModal}
hideAccentColorModal={hideAccentColorModal}
theme={theme}
/>
</ScreenContainer>
);
};
export default GenralSettings;
+9 -17
View File
@@ -1,12 +1,13 @@
import React from "react";
import { StyleSheet, View, Image } from "react-native";
import { useSelector } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import { ListItem, ListSection } from "../../Components/List";
import { useTheme } from "../../Hooks/reduxHooks";
const MoreScreen = ({ navigation }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
const theme = useTheme();
return (
<>
@@ -15,8 +16,8 @@ const MoreScreen = ({ navigation }) => {
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
<View
style={{
paddingTop: 20,
paddingBottom: 30,
paddingTop: 10,
paddingBottom: 25,
alignItems: "center",
backgroundColor: theme.colorPrimary,
elevation: 4,
@@ -25,8 +26,8 @@ const MoreScreen = ({ navigation }) => {
<Image
source={require("../../../assets/logo.png")}
style={{
height: 60,
width: 60,
height: 80,
width: 80,
tintColor: theme.textColorPrimary,
}}
/>
@@ -38,21 +39,12 @@ const MoreScreen = ({ navigation }) => {
icon="cog-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "Settings",
})
}
theme={theme}
/>
<ListItem
title="Tracking"
icon="sync"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "Trackers",
screen: "SettingsStack",
})
}
theme={theme}
/>
<ListItem
title="About"
icon="information-outline"
+20
View File
@@ -0,0 +1,20 @@
import React from "react";
import { useDispatch } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import { ScreenContainer } from "../../Components/Common";
import { useTheme } from "../../Hooks/reduxHooks";
const ReaderSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
return (
<ScreenContainer theme={theme}>
<Appbar title="Reader" onBackAction={() => navigation.goBack()} />
</ScreenContainer>
);
};
export default ReaderSettings;
+48 -123
View File
@@ -2,9 +2,9 @@ import React, { useState } from "react";
import { View } from "react-native";
import { connect, useDispatch } from "react-redux";
import { switchTheme } from "../../redux/settings/settings.actions";
import { setAppTheme } from "../../redux/settings/settings.actions";
import {
setDisplayMode,
setAppSettings,
setItemsPerRow,
} from "../../redux/settings/settings.actions";
import { deleteNovelCache } from "../../Database/queries/NovelQueries";
@@ -15,53 +15,15 @@ import {
ListItem,
ListSection,
ListSubHeader,
InfoItem,
} from "../../Components/List";
import DisplayModeModal from "./components/DisplayModeModal";
import GridSizeModal from "./components/GridSizeModal";
import ThemeModal from "./components/ThemeModal";
import { createBackup, restoreBackup } from "../../Services/backup";
import { useTheme } from "../../Hooks/reduxHooks";
import { restoreLibraryAction } from "../../redux/library/library.actions";
import { clearAllHistoryAction } from "../../redux/history/history.actions";
const SettingsScreen = ({
navigation,
displayMode,
itemsPerRow,
switchTheme,
setDisplayMode,
setItemsPerRow,
}) => {
const displayModeLabel = (displayMode) => {
const label = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
return label[displayMode];
};
const SettingsScreen = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
// Display Mode Modal
const [displayModalVisible, setDisplayModalVisible] = useState(false);
const showDisplayModal = () => setDisplayModalVisible(true);
const hideDisplayModal = () => setDisplayModalVisible(false);
// Theme Modal
const [themeModalVisible, setthemeModalVisible] = useState(false);
const showthemeModal = () => setthemeModalVisible(true);
const hidethemeModal = () => setthemeModalVisible(false);
// Grid Size Modal
const [gridSizeModalVisible, setGridSizeModalVisible] = useState(false);
const showGridSizeModal = () => setGridSizeModalVisible(true);
const hideGridSizeModal = () => setGridSizeModalVisible(false);
return (
<>
<Appbar title="Settings" onBackAction={() => navigation.goBack()} />
@@ -69,84 +31,56 @@ const SettingsScreen = ({
style={{
flex: 1,
backgroundColor: theme.colorPrimaryDark,
paddingVertical: 8,
}}
>
<ListSection>
<ListSubHeader theme={theme}>Data</ListSubHeader>
<ListItem
title="Clear database"
description="Delete history for novels not in your library"
onPress={() => deleteNovelCache()}
theme={theme}
/>
<ListItem
title="Clear history"
description="Delete reading history for all novels"
onPress={() => dispatch(clearAllHistoryAction())}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Display</ListSubHeader>
<ListItem
title="Display Mode"
description={displayModeLabel(displayMode)}
onPress={showDisplayModal}
theme={theme}
/>
<ListItem
title="Items per row in library"
description={`${itemsPerRow} items per row`}
onPress={showGridSizeModal}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Theme</ListSubHeader>
<ListItem
title="Theme"
description={theme.name}
onPress={showthemeModal}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Backup</ListSubHeader>
<ListItem
title="Create backup"
description="Can be used to restore current library"
onPress={createBackup}
theme={theme}
/>
<ListItem
title="Restore backup"
description="Restore library from backup file"
onPress={() => dispatch(restoreLibraryAction())}
theme={theme}
/>
<InfoItem
title="Create backup may not work on devices with Android 9 or lower."
icon="information-outline"
description="Restore library from backup file"
theme={theme}
/>
</ListSection>
<DisplayModeModal
displayMode={displayMode}
displayModalVisible={displayModalVisible}
hideDisplayModal={hideDisplayModal}
setDisplayMode={setDisplayMode}
<ListItem
title="General"
icon="tune"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "GeneralSettings",
})
}
theme={theme}
/>
<GridSizeModal
itemsPerRow={itemsPerRow}
gridSizeModalVisible={gridSizeModalVisible}
hideGridSizeModal={hideGridSizeModal}
setItemsPerRow={setItemsPerRow}
{/* <ListItem
title="Reader"
icon="book-open-outline"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "ReaderSettings",
})
}
theme={theme}
/> */}
<ListItem
title="Tracking"
icon="sync"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "TrackerSettings",
})
}
theme={theme}
/>
<ThemeModal
themeModalVisible={themeModalVisible}
hidethemeModal={hidethemeModal}
switchTheme={switchTheme}
<ListItem
title="Backup"
icon="cloud-upload-outline"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "BackupSettings",
})
}
theme={theme}
/>
<ListItem
title="Advanced"
icon="code-tags"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "AdvancedSettings",
})
}
theme={theme}
/>
</View>
@@ -154,13 +88,4 @@ const SettingsScreen = ({
);
};
const mapStateToProps = (state) => ({
displayMode: state.settingsReducer.displayMode,
itemsPerRow: state.settingsReducer.itemsPerRow,
});
export default connect(mapStateToProps, {
switchTheme,
setDisplayMode,
setItemsPerRow,
})(SettingsScreen);
export default SettingsScreen;
@@ -0,0 +1,80 @@
import React, { useState } from "react";
import { StyleSheet, Text } from "react-native";
import { Modal, Portal, TextInput } from "react-native-paper";
import { setAccentColor } from "../../../redux/settings/settings.actions";
const AccentColorModal = ({
dispatch,
accentColorModal,
hideAccentColorModal,
theme,
}) => {
const [text, setText] = useState(theme.colorAccent);
const [error, setError] = useState();
return (
<Portal>
<Modal
visible={accentColorModal}
onDismiss={() => {
hideAccentColorModal();
setText();
setError();
}}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={[
styles.dialogTitle,
{ color: theme.textColorPrimary },
]}
>
Hex Color Code
</Text>
<TextInput
value={text}
defaultValue={theme.colorAccent}
onChangeText={(text) => setText(text)}
onSubmitEditing={() => {
if (text.match(/^#[0-9a-f]{3,6}$/i)) {
dispatch(setAccentColor(text));
hideAccentColorModal();
} else {
setError("Enter a valid hex color code");
}
}}
mode="outlined"
theme={{
colors: {
primary: theme.colorAccent,
placeholder: theme.textColorHint,
text: theme.textColorPrimary,
background: "transparent",
},
}}
underlineColor={theme.textColorHint}
dense
error={error}
/>
<Text style={{ color: "#FF0033", paddingTop: 8 }}>{error}</Text>
</Modal>
</Portal>
);
};
export default AccentColorModal;
const styles = StyleSheet.create({
containerStyle: {
margin: 30,
padding: 20,
borderRadius: 8,
},
dialogTitle: {
fontSize: 18,
marginBottom: 16,
},
});
@@ -1,13 +1,14 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Portal, Modal, Checkbox } from "react-native-paper";
import { setAppSettings } from "../../../redux/settings/settings.actions";
const DisplayModeModal = ({
displayMode,
displayModalVisible,
setDisplayMode,
hideDisplayModal,
theme,
dispatch,
displayMode,
hideDisplayModal,
displayModalVisible,
}) => {
const displayModes = [
{ displayMode: 0, label: "Compact Grid" },
@@ -27,7 +28,9 @@ const DisplayModeModal = ({
mode="ios"
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() => setDisplayMode(mode.displayMode)}
onPress={() =>
dispatch(setAppSettings("displayMode", mode.displayMode))
}
/>
));
};
+16 -15
View File
@@ -3,20 +3,21 @@ import { Text, StyleSheet } from "react-native";
import { Portal, Modal } from "react-native-paper";
import Slider from "@react-native-community/slider";
import { RadioButton, RadioButtonGroup } from "../../../Components/RadioButton";
import { setNovelsPerRow } from "../../../redux/settings/settings.actions";
const GridSizeModal = ({
itemsPerRow,
dispatch,
novelsPerRow,
gridSizeModalVisible,
hideGridSizeModal,
setItemsPerRow,
theme,
}) => {
const gridSizes = {
1: "XS",
2: "S",
5: "XS",
4: "S",
3: "M",
4: "L",
5: "XL",
2: "L",
1: "XL",
};
return (
@@ -43,22 +44,22 @@ const GridSizeModal = ({
{ color: theme.textColorSecondary },
]}
>
{`${itemsPerRow} per row`}
{`${novelsPerRow} per row`}
</Text>
<Slider
{/* <Slider
style={styles.slider}
value={itemsPerRow}
value={novelsPerRow}
minimumValue={1}
maximumValue={5}
step={1}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccent}
onValueChange={(value) => setItemsPerRow(value)}
/>
{/* <RadioButtonGroup
onValueChange={(value) => setItemsPerRow(value)}
value={itemsPerRow}
onValueChange={(value) => dispatch(setNovelsPerRow(value))}
/> */}
<RadioButtonGroup
onValueChange={(value) => dispatch(setNovelsPerRow(value))}
value={novelsPerRow}
>
{Object.keys(gridSizes).map((item) => (
<RadioButton
@@ -68,7 +69,7 @@ const GridSizeModal = ({
theme={theme}
/>
))}
</RadioButtonGroup> */}
</RadioButtonGroup>
</Modal>
</Portal>
);
+4 -7
View File
@@ -3,19 +3,16 @@ import { StyleSheet } from "react-native";
import { Portal, Modal, Checkbox } from "react-native-paper";
import { setStatusBarStyle } from "expo-status-bar";
import { setAppTheme } from "../../../redux/settings/settings.actions";
const ThemeModal = ({
themeModalVisible,
hidethemeModal,
switchTheme,
theme,
}) => {
const ThemeModal = ({ themeModalVisible, hidethemeModal, dispatch, theme }) => {
const themes = [
{ id: 0, name: "AMOLED Dark", statusBar: "light" },
{ id: 1, name: "Light", statusBar: "dark" },
{ id: 2, name: "Dark", statusBar: "light" },
{ id: 3, name: "Midnight Dusk", statusBar: "light" },
{ id: 4, name: "Lime", statusBar: "light" },
{ id: 5, name: "Iris Blue", statusBar: "light" },
];
const renderThemeCheckboxes = () =>
@@ -28,7 +25,7 @@ const ThemeModal = ({
mode="ios"
color={theme.colorAccent}
onPress={() => {
switchTheme(item.id);
dispatch(setAppTheme(item.id));
setStatusBarStyle(item.statusBar);
}}
/>
+290 -50
View File
@@ -1,52 +1,68 @@
import React, { useEffect, useRef, useState } from "react";
import { StyleSheet, View, FlatList, RefreshControl } from "react-native";
import { Provider, Portal } from "react-native-paper";
import {
StyleSheet,
View,
FlatList,
RefreshControl,
StatusBar,
Dimensions,
Share,
} from "react-native";
import { Provider, Portal, Appbar, IconButton, Menu } from "react-native-paper";
import { useDispatch } from "react-redux";
import {
bookmarkChapterAction,
deleteAllChaptersAction,
downloadAllChaptersAction,
getNovelAction,
markChapterReadAction,
markChaptersRead,
markChapterUnreadAction,
markPreviousChaptersReadAction,
sortAndFilterChapters,
updateNovelAction,
} from "../../redux/novel/novel.actions";
import {
useContinueReading,
useNovel,
usePreferences,
useTheme,
} from "../../Hooks/reduxHooks";
import { showToast } from "../../Hooks/showToast";
import ChapterItem from "./components/ChapterItem";
import NovelInfoHeader from "./components/NovelHeader";
import ChaptersSettingsSheet from "./components/ChaptersSettingsSheet";
import { useDispatch } from "react-redux";
import {
getNovelAction,
sortAndFilterChapters,
updateNovelAction,
} from "../../redux/novel/novel.actions";
import TrackSheet from "./components/Tracker/TrackSheet";
import { showToast } from "../../Hooks/showToast";
import ChapterActionsSheet from "./components/ChapterActionsSheet";
import { Row } from "../../Components/Common";
const Novel = ({ route, navigation }) => {
const item = route.params;
const {
sourceId,
novelUrl,
novelName,
novelCover,
followed,
novelId,
} = item;
const { sourceId, novelUrl, novelName, followed, novelId } = item;
const theme = useTheme();
const dispatch = useDispatch();
const { novel, chapters, loading, fetching } = useNovel();
const { novel, chapters, loading, updating, downloading } = useNovel();
const [selected, setSelected] = useState([]);
const [downloadMenu, showDownloadMenu] = useState(false);
let chaptersSettingsSheetRef = useRef(null);
let trackerSheetRef = useRef(null);
let chapterActionsSheetRef = useRef(null);
const { sort, filter } = usePreferences(
novelId || (!loading && novel.novelId)
);
const {
sort = "ORDER BY chapterId ASC",
filter = "",
showChapterTitles = false,
} = usePreferences(novel.novelId);
let lastRead = useContinueReading(
let { lastReadChapter, position } = useContinueReading(
chapters,
novelId || (!loading && novel.novelId)
novel.novelId
);
useEffect(() => {
@@ -56,43 +72,37 @@ const Novel = ({ route, navigation }) => {
}, [getNovelAction]);
const onRefresh = () => {
dispatch(updateNovelAction(sourceId, novelUrl, novelId));
dispatch(updateNovelAction(sourceId, novelUrl, novelId, sort, filter));
showToast(`Updated ${novelName}`);
};
const refreshControl = () => (
<RefreshControl
onRefresh={onRefresh}
refreshing={fetching}
refreshing={updating}
colors={[theme.textColorPrimary]}
progressBackgroundColor={theme.colorPrimary}
/>
);
const ListHeaderComponent = () => (
<NovelInfoHeader
item={item}
novel={novel}
theme={theme}
filter={filter}
loading={loading}
lastRead={lastRead}
dispatch={dispatch}
chapters={chapters}
navigation={navigation}
trackerSheetRef={trackerSheetRef}
chaptersSettingsSheetRef={chaptersSettingsSheetRef}
/>
);
const renderItem = ({ item }) => (
const renderItem = ({ item, index }) => (
<ChapterItem
theme={theme}
chapter={item}
index={index}
novelUrl={novelUrl}
dispatch={dispatch}
sourceId={sourceId}
navigation={navigation}
lastReadChapter={lastReadChapter}
position={position}
downloading={downloading}
selected={selected}
novelName={novelName}
selected={selected}
setSelected={setSelected}
showChapterTitles={showChapterTitles}
chapterActionsSheetRef={chapterActionsSheetRef}
/>
);
@@ -104,6 +114,203 @@ const Novel = ({ route, navigation }) => {
{ backgroundColor: theme.colorPrimaryDark },
]}
>
{selected.length > 0 ? (
<View
style={{
backgroundColor: theme.colorPrimary,
position: "absolute",
zIndex: 1,
width: Dimensions.get("window").width,
flexDirection: "row",
alignItems: "center",
paddingTop: StatusBar.currentHeight,
paddingBottom: 8,
}}
>
<Appbar.Action
icon="close"
color={theme.textColorPrimary}
onPress={() => {
setSelected([]);
}}
/>
<Appbar.Content
title={selected.length}
titleStyle={{ color: theme.textColorPrimary }}
/>
<Appbar.Action
icon="bookmark-outline"
color={theme.textColorPrimary}
onPress={() => {
dispatch(bookmarkChapterAction(selected));
setSelected([]);
}}
/>
{selected.some((obj) => obj.read === 0) && (
<Appbar.Action
icon="check"
color={theme.textColorPrimary}
onPress={() => {
dispatch(
markChaptersRead(
selected,
novel.novelId
)
);
setSelected([]);
}}
/>
)}
{selected.some((obj) => obj.read === 1) && (
<Appbar.Action
icon="check-outline"
color={theme.textColorPrimary}
onPress={() => {
dispatch(
markChapterUnreadAction(
selected,
novel.novelId
)
);
setSelected([]);
}}
/>
)}
{selected.length === 1 && (
<Appbar.Action
icon="playlist-check"
color={theme.textColorPrimary}
onPress={() => {
dispatch(
markPreviousChaptersReadAction(
selected[0].chapterId,
selected[0].novelId
)
);
setSelected([]);
}}
/>
)}
{selected.some((obj) => obj.downloaded === 1) && (
<Appbar.Action
icon="trash-can-outline"
color={theme.textColorPrimary}
onPress={() => {
dispatch(deleteAllChaptersAction(selected));
setSelected([]);
}}
/>
)}
{selected.some((obj) => obj.downloaded === 0) && (
<Appbar.Action
icon="download-outline"
color={theme.textColorPrimary}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
selected
)
);
setSelected([]);
}}
/>
)}
<Appbar.Action
icon="select-all"
color={theme.textColorPrimary}
onPress={() => {
setSelected(chapters);
}}
/>
</View>
) : (
<View
style={{
position: "absolute",
zIndex: 1,
height: StatusBar.currentHeight + 54,
width: "100%",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<IconButton
icon="arrow-left"
color="white"
size={24}
style={{ marginTop: StatusBar.currentHeight + 8 }}
onPress={() => navigation.goBack()}
/>
<Row>
<Menu
visible={downloadMenu}
onDismiss={() => showDownloadMenu(false)}
anchor={
<IconButton
icon="download"
color="white"
size={24}
style={{
marginTop:
StatusBar.currentHeight + 8,
}}
onPress={() => showDownloadMenu(true)}
/>
}
contentStyle={{
backgroundColor: theme.menuColor,
}}
>
<Menu.Item
title="Download all"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{
color: theme.textColorPrimary,
}}
onPress={() =>
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
)
)
}
/>
<Menu.Item
title="Delete downloads"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{
color: theme.textColorPrimary,
}}
onPress={() =>
dispatch(
deleteAllChaptersAction(chapters)
)
}
/>
</Menu>
<IconButton
icon="share-variant"
color="white"
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
marginRight: 16,
}}
onPress={() =>
Share.share({
message: novel.sourceUrl,
})
}
/>
</Row>
</View>
)}
<FlatList
data={!loading && chapters}
keyExtractor={(item) => item.chapterId.toString()}
@@ -112,7 +319,21 @@ const Novel = ({ route, navigation }) => {
windowSize={15}
initialNumToRender={7}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
ListHeaderComponent={
<NovelInfoHeader
item={item}
novel={novel}
theme={theme}
filter={filter}
loading={loading}
lastRead={lastReadChapter}
dispatch={dispatch}
chapters={chapters}
navigation={navigation}
trackerSheetRef={trackerSheetRef}
chaptersSettingsSheetRef={chaptersSettingsSheetRef}
/>
}
refreshControl={refreshControl()}
/>
{!loading && (
@@ -122,16 +343,29 @@ const Novel = ({ route, navigation }) => {
bottomSheetRef={chaptersSettingsSheetRef}
dispatch={dispatch}
sortAndFilterChapters={sortAndFilterChapters}
novelId={novelId || novel.novelId}
savedSort={sort}
savedFilter={filter}
novelId={novel.novelId}
sort={sort}
theme={theme}
filter={filter}
showChapterTitles={showChapterTitles}
/>
<TrackSheet
bottomSheetRef={trackerSheetRef}
novelId={novelId || novel.novelId}
novelName={novelName || novel.novelName}
novelId={novel.novelId}
novelName={novel.novelName}
theme={theme}
/>
{/* <ChapterActionsSheet
theme={theme}
selected={selected}
chapters={chapters}
dispatch={dispatch}
setSelected={setSelected}
bottomSheetRef={chapterActionsSheetRef}
markChapterReadAction={markChapterReadAction}
markChapterUnreadAction={markChapterUnreadAction}
bookmarkChapterAction={bookmarkChapterAction}
/> */}
</Portal>
)}
</View>
@@ -143,4 +377,10 @@ export default Novel;
const styles = StyleSheet.create({
container: { flex: 1 },
rowBack: {
alignItems: "center",
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
},
});
@@ -0,0 +1,176 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Animated } from "react-native";
import { List } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
import { ListItem } from "../../../Components/List";
import {
markPreviousChaptersReadAction,
markPreviousChaptersUnreadAction,
} from "../../../redux/novel/novel.actions";
const ChapterActionsSheet = ({
bottomSheetRef,
selected,
theme,
markChapterReadAction,
markChapterUnreadAction,
dispatch,
bookmarkChapterAction,
setSelected,
}) => {
const [animatedValue] = useState(new Animated.Value(0));
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 270, bottom: 0 }}
snappingPoints={[0, 270]}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={{
color: theme.textColorPrimary,
textAlign: "center",
paddingTop: 16,
paddingBottom: 8,
fontSize: 16,
}}
numberOfLines={1}
>
{selected?.chapterName}
</Text>
<List.Item
title="Mark previous chapters as read"
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon="eye-check"
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
dispatch(
markPreviousChaptersReadAction(
selected?.chapterId,
selected?.novelId
)
);
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
<List.Item
title="Mark previous chapters as unread"
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon="eye-off"
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
dispatch(
markPreviousChaptersUnreadAction(
selected?.chapterId,
selected?.novelId
)
);
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
<List.Item
title={
selected?.read
? "Mark chapter as unread"
: "Mark chapter as read"
}
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon={selected?.read ? "eye-off" : "eye"}
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
if (selected?.read) {
dispatch(
markChapterUnreadAction(selected?.chapterId)
);
setSelected((selected) => ({
...selected,
read: 0,
}));
} else {
dispatch(
markChapterReadAction(
selected?.chapterId,
selected?.novelId
)
);
setSelected((selected) => ({
...selected,
read: 1,
}));
}
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
<List.Item
title={
selected?.bookmark
? "Remove bookmark"
: "Bookmark chapter"
}
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon={
selected?.bookmark ? "bookmark-off" : "bookmark"
}
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
dispatch(
bookmarkChapterAction(
selected?.bookmark,
selected?.chapterId
)
);
setSelected((selected) => ({
...selected,
bookmark: !selected.bookmark,
}));
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
</View>
</Bottomsheet>
);
};
export default ChapterActionsSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
});
+154 -47
View File
@@ -1,27 +1,47 @@
import React, { useState } from "react";
import { View, Text, ActivityIndicator, StyleSheet } from "react-native";
import { TouchableRipple, IconButton, Menu } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import {
deleteChapterAction,
downloadChapterAction,
} from "../../../redux/novel/novel.actions";
import { parseChapterNumber } from "../../../Services/utils/helpers";
const ChapterItem = ({
novelUrl,
novelName,
chapter,
sourceId,
dispatch,
theme,
navigation,
downloading,
index,
position,
selected,
setSelected,
showChapterTitles,
chapterActionsSheetRef,
}) => {
const [visible, setVisible] = useState(false);
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
const navigateToChapter = () =>
navigation.navigate("Chapter", {
sourceId,
novelUrl,
novelId: chapter.novelId,
chapterId: chapter.chapterId,
chapterUrl: chapter.chapterUrl,
chapterName: chapter.chapterName,
novelName: novelName,
});
const displayDownloadButton = () => {
if (chapter.downloaded === 3) {
if (downloading.indexOf(chapter.chapterId) !== -1) {
return (
<ActivityIndicator
color={theme.textColorHint}
@@ -32,15 +52,15 @@ const ChapterItem = ({
} else if (chapter.downloaded === 1) {
return (
<Menu
visible={visible}
onDismiss={closeMenu}
visible={deleteChapterMenu}
onDismiss={hideDeleteChapterMenu}
anchor={
<IconButton
icon="check-circle"
animated
color={theme.textColorPrimary}
size={25}
onPress={openMenu}
onPress={showDeleteChapterMenu}
style={{ margin: 2 }}
/>
}
@@ -84,53 +104,140 @@ const ChapterItem = ({
}
};
const bookmarkButton = () => {
if (chapter.bookmark) {
return (
<IconButton
icon="bookmark"
color={theme.colorAccent}
size={18}
style={{ marginLeft: 2 }}
/>
);
}
};
const isSelected = (chapterId) => {
return selected.some((obj) => obj.chapterId === chapterId);
};
return (
<TouchableRipple
style={styles.chapterCardContainer}
onPress={() =>
navigation.navigate("Chapter", {
chapterId: chapter.chapterId,
chapterUrl: chapter.chapterUrl,
extensionId: sourceId,
novelUrl: novelUrl,
chapterName: chapter.chapterName,
novelId: chapter.novelId,
})
}
style={[
styles.chapterCardContainer,
isSelected(chapter.chapterId) && {
backgroundColor: theme.rippleColor,
},
]}
onPress={() => {
if (selected.length === 0) {
navigateToChapter();
} else {
if (isSelected(chapter.chapterId)) {
setSelected((selected) =>
selected.filter(
(item) => item.chapterId !== chapter.chapterId
)
);
} else {
setSelected((selected) => [...selected, chapter]);
}
}
}}
onLongPress={() => {
if (isSelected(chapter.chapterId)) {
setSelected((selected) =>
selected.filter(
(item) => item.chapterId !== chapter.chapterId
)
);
} else {
setSelected((selected) => [...selected, chapter]);
}
// chapterActionsSheetRef.current.show();
}}
rippleColor={theme.rippleColor}
>
<>
<View>
<Text
style={[
{
color: theme.textColorPrimary,
},
chapter.read === 1 && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
{chapter.chapterName.substring(0, 50)}
</Text>
{chapter.releaseDate && (
<Text
style={[
{
color: theme.textColorSecondary,
{bookmarkButton()}
<View>
<Text
style={[
{ color: theme.textColorPrimary },
chapter.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{showChapterTitles
? parseChapterNumber(chapter.chapterName)
? "Chapter " +
parseChapterNumber(
chapter.chapterName
)
: "Chapter " + index
: chapter.chapterName.substring(0, 50)}
</Text>
<View
style={{
flexDirection: "row",
marginTop: 5,
fontSize: 12,
},
chapter.read === 1 && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{chapter.releaseDate}
</Text>
)}
}}
>
{chapter.releaseDate && (
<Text
style={[
{
color: theme.textColorSecondary,
fontSize: 12,
},
chapter.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{chapter.releaseDate}
</Text>
)}
{position &&
!chapter.read &&
position[chapter.chapterId] &&
position[chapter.chapterId].percentage <
100 &&
position[chapter.chapterId].percentage >
0 && (
<Text
style={[
{
color: theme.textColorHint,
fontSize: 12,
marginLeft: 0,
},
chapter.releaseDate && {
marginLeft: 5,
},
]}
numberOfLines={1}
>
{chapter.releaseDate && "• "}
{"Progress " +
position[chapter.chapterId]
.percentage +
"%"}
</Text>
)}
</View>
</View>
</View>
</View>
<View>{displayDownloadButton()}</View>
</>
@@ -1,177 +1,202 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Animated } from "react-native";
import { List, RadioButton, Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import {
StyleSheet,
View,
Text,
Animated,
useWindowDimensions,
} from "react-native";
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
import { useSelector } from "react-redux";
import Bottomsheet from "rn-sliding-up-panel";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
import { showChapterTitlesAction } from "../../../redux/novel/novel.actions";
import { ListItem } from "../../../Components/List";
import { RadioButton, RadioButtonGroup } from "../../../Components/RadioButton";
import { Checkbox } from "react-native-paper";
const ChaptersSettingsSheet = ({
bottomSheetRef,
novelId,
sortAndFilterChapters,
dispatch,
savedSort,
savedFilter,
sort,
filter,
theme,
showChapterTitles,
}) => {
const [sort, setSort] = useState(savedSort || "ORDER BY chapterId ASC");
const [filter, setFilter] = useState(savedFilter || "");
const sortChapters = (val) => {
setSort(val);
const sortChapters = (val) =>
dispatch(sortAndFilterChapters(novelId, val, filter));
};
const filterChapters = (val) => {
setFilter(val);
const filterChapters = (val) =>
dispatch(sortAndFilterChapters(novelId, sort, val));
};
const checkBoxStyle = { flexDirection: "row", alignItems: "center" };
const theme = useSelector((state) => state.settingsReducer.theme);
const [animatedValue] = useState(new Animated.Value(0));
const FirstRoute = () => (
<View style={{ flex: 1 }}>
<Checkbox.Item
label="Downloaded"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter.match("AND downloaded=1") ? "checked" : "unchecked"
}
onPress={() =>
filter.match("AND downloaded=1")
? filterChapters(
filter.replace(" AND downloaded=1", "")
)
: filterChapters(filter + " AND downloaded=1")
}
/>
<Checkbox.Item
label="Unread"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter.match("AND `read`=0")
? "checked"
: filter.match("AND `read`=1")
? "indeterminate"
: "unchecked"
}
onPress={() => {
if (filter.match("AND `read`=0")) {
filterChapters(
filter.replace(" AND `read`=0", " AND `read`=1")
);
} else if (filter.match("AND `read`=1")) {
filterChapters(filter.replace(" AND `read`=1", ""));
} else {
filterChapters(filter + " AND `read`=0");
}
}}
/>
<Checkbox.Item
label="Bookmarked"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter.match("AND bookmark=1") ? "checked" : "unchecked"
}
onPress={() => {
filter.match("AND bookmark=1")
? filterChapters(filter.replace(" AND bookmark=1", ""))
: filterChapters(filter + " AND bookmark=1");
}}
/>
</View>
);
const SecondRoute = () => (
<View style={{ flex: 1 }}>
<ListItem
style={{ paddingVertical: 10 }}
title="By source"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "ORDER BY chapterId ASC"
? "arrow-up"
: "arrow-down"
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "ORDER BY chapterId ASC"
? sortChapters("ORDER BY chapterId DESC")
: sortChapters("ORDER BY chapterId ASC")
}
/>
</View>
);
const ThirdRoute = () => (
<View style={{ flex: 1, padding: 8 }}>
<RadioButtonGroup
onValueChange={(value) =>
dispatch(showChapterTitlesAction(novelId, value))
}
value={showChapterTitles}
>
<RadioButton
value={false}
label="Source title"
theme={theme}
labelStyle={{ fontSize: 14 }}
/>
<RadioButton
value={true}
label="Chapter number"
theme={theme}
labelStyle={{ fontSize: 14 }}
/>
</RadioButtonGroup>
</View>
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
third: ThirdRoute,
});
const layout = useWindowDimensions();
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Filter" },
{ key: "second", title: "Sort" },
{ key: "third", title: "Display" },
]);
const renderTabBar = (props) => (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: theme.colorAccent }}
style={{ backgroundColor: theme.colorPrimary }}
renderLabel={({ route, focused, color }) => (
<Text style={{ color, margin: 8 }}>{route.title}</Text>
)}
inactiveColor={theme.textColorSecondary}
activeColor={theme.colorAccent}
pressColor={theme.rippleColor}
/>
);
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 370, bottom: 0 }}
snappingPoints={[0, 370]}
draggableRange={{ top: 220, bottom: 0 }}
snappingPoints={[0, 220]}
>
<View
style={[
styles.contentContainer,
{
backgroundColor: theme.colorPrimary,
},
{ backgroundColor: theme.colorPrimary },
]}
>
<View style={styles.bottomSheetHandle} />
<List.Subheader
style={{
color: theme.textColorPrimary,
fontSize: 20,
}}
>
Sort
</List.Subheader>
<View>
<RadioButton.Group
onValueChange={(newValue) =>
sortChapters(newValue, filter)
}
value={sort}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
<RadioButton.Item
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
value="ORDER BY chapterId DESC"
/>
<Text
style={{
color: theme.textColorPrimary,
}}
>
Newest to oldest
</Text>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
<RadioButton.Item
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
value="ORDER BY chapterId ASC"
/>
<Text
style={{
color: theme.textColorPrimary,
}}
>
Oldest to newest
</Text>
</View>
</View>
</RadioButton.Group>
</View>
<List.Subheader
style={{
color: theme.textColorPrimary,
fontSize: 20,
paddingVertical: 10,
}}
>
Filter
</List.Subheader>
<View style={checkBoxStyle}>
<Checkbox.Item
status={filter === "" ? "checked" : "unchecked"}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show all
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND `read`=1" ? "checked" : "unchecked"
}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("AND `read`=1")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show read chapters
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND `read`=0" ? "checked" : "unchecked"
}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("AND `read`=0")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show unread chapters
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND downloaded=1"
? "checked"
: "unchecked"
}
uncheckedColor={theme.textColorHint}
color={theme.colorAccent}
onPress={() => filterChapters("AND downloaded=1")}
/>
<Text style={{ color: theme.textColorPrimary }}>
Show downloaded chapters
</Text>
</View>
<TabView
navigationState={{ index, routes }}
renderTabBar={renderTabBar}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
style={{ borderTopRightRadius: 8, borderTopLeftRadius: 8 }}
/>
</View>
</Bottomsheet>
);
@@ -185,12 +210,4 @@ const styles = StyleSheet.create({
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
bottomSheetHandle: {
backgroundColor: "rgba(255,255,255,0.25)",
height: 4,
width: 50,
borderRadius: 50,
top: 10,
alignSelf: "center",
},
});
@@ -1,53 +0,0 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Chip } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useDispatch } from "react-redux";
import { followNovelAction } from "../../../../redux/novel/novel.actions";
const FollowChip = ({ followed, novel, theme }) => {
const dispatch = useDispatch();
const getIconName = () => (followed ? "heart" : "heart-outline");
const getChipLabel = () => (followed ? "In Library" : "Add to library");
return (
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={getIconName()}
size={21}
color={theme.colorAccent}
/>
)}
onPress={() => dispatch(followNovelAction(novel))}
style={[
{ backgroundColor: theme.colorPrimaryDark },
styles.toggleFavourite,
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimary,
}}
>
{getChipLabel()}
</Chip>
);
};
export default FollowChip;
const styles = StyleSheet.create({
toggleFavourite: {
height: 32,
justifyContent: "center",
alignItems: "center",
marginLeft: 16,
paddingLeft: 4,
borderWidth: 0,
elevation: 0,
},
});
@@ -1,36 +0,0 @@
import React from "react";
import { ImageBackground, StyleSheet } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import easeGradient from "react-native-easing-gradient";
const NovelCoverImage = ({ children, source, theme }) => {
const { colors, locations } = easeGradient({
colorStops: {
0: {
color: "rgba(0,0,0,0.2)",
},
1: {
color: theme.colorPrimaryDark,
},
},
});
return (
<ImageBackground source={source} style={styles.background}>
<LinearGradient
colors={colors}
locations={locations}
style={styles.linearGradient}
>
{children}
</LinearGradient>
</ImageBackground>
);
};
export default NovelCoverImage;
const styles = StyleSheet.create({
background: { height: 285 },
linearGradient: { height: 286 },
});
@@ -0,0 +1,216 @@
import React from "react";
import {
FlatList,
Image,
ImageBackground,
StyleSheet,
Text,
View,
} from "react-native";
import { Chip, IconButton } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import easeGradient from "react-native-easing-gradient";
const NovelInfoContainer = ({ children }) => (
<View style={styles.novelInfoContainer}>{children}</View>
);
const CoverImage = ({ children, source, theme }) => {
const { colors, locations } = easeGradient({
colorStops: {
0: { color: "rgba(0,0,0,0.2)" },
1: { color: theme.colorPrimaryDark },
},
});
return (
<ImageBackground source={source} style={styles.coverImage}>
<LinearGradient
colors={colors}
locations={locations}
style={styles.linearGradient}
>
{children}
</LinearGradient>
</ImageBackground>
);
};
const NovelThumbnail = ({ source }) => (
<Image source={source} style={styles.novelThumbnail} />
);
const NovelTitle = ({ theme, children, onLongPress, onPress }) => (
<Text
onLongPress={onLongPress}
onPress={onPress}
numberOfLines={2}
style={[styles.novelTitle, { color: theme.textColorPrimary }]}
>
{children}
</Text>
);
const NovelAuthor = ({ theme, children }) => (
<Text
style={[styles.novelAuthor, { color: theme.textColorSecondary }]}
numberOfLines={2}
>
{children}
</Text>
);
const NovelInfo = ({ theme, children }) => (
<Text
style={[styles.novelInfo, { color: theme.textColorSecondary }]}
numberOfLines={1}
>
{children}
</Text>
);
const FollowButton = ({ theme, onPress, followed }) => (
<View style={{ alignItems: "center", flex: 1 }}>
<IconButton
icon={followed ? "heart" : "heart-outline"}
color={followed ? theme.colorAccent : theme.textColorSecondary}
size={24}
style={{ margin: 0 }}
onPress={onPress}
/>
<Text
style={{
fontSize: 12,
color: followed ? theme.colorAccent : theme.textColorSecondary,
}}
>
{followed ? "In Library" : "Add to library"}
</Text>
</View>
);
const TrackerButton = ({ theme, isTracked, onPress }) => (
<View style={{ alignItems: "center", flex: 1 }}>
<IconButton
icon={isTracked ? "check" : "sync"}
color={isTracked ? theme.colorAccent : theme.textColorSecondary}
size={24}
style={{ margin: 0 }}
onPress={onPress}
/>
<Text
style={{
fontSize: 12,
color: isTracked ? theme.colorAccent : theme.textColorSecondary,
}}
>
{isTracked ? "Tracked" : "Tracking"}
</Text>
</View>
);
const GenreChip = ({ children, theme }) => (
<Text
style={[
styles.genreChip,
{
color: theme.colorAccent,
borderColor: theme.colorAccent,
backgroundColor: theme.colorPrimary,
},
]}
>
{children}
</Text>
);
const NovelGenres = ({ theme, genre }) => {
const data = genre.split(",");
const renderItem = ({ item }) => (
<GenreChip theme={theme}>{item}</GenreChip>
);
return (
<FlatList
contentContainerStyle={styles.novelGenres}
horizontal
data={data}
keyExtractor={(item) => item}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
/>
);
};
export {
NovelInfoContainer,
CoverImage,
NovelThumbnail,
NovelTitle,
NovelAuthor,
NovelInfo,
FollowButton,
TrackerButton,
NovelGenres,
};
const styles = StyleSheet.create({
novelInfoContainer: {
flex: 1,
flexDirection: "row",
margin: 16,
paddingTop: 90,
},
coverImage: {
height: 285,
},
linearGradient: {
height: 286,
},
novelThumbnail: {
height: 160,
width: 110,
margin: 3.2,
borderRadius: 6,
},
novelTitle: {
fontWeight: "bold",
fontSize: 18,
},
novelAuthor: {
marginVertical: 3,
fontSize: 14,
fontWeight: "bold",
},
novelInfo: {
marginVertical: 3,
fontSize: 14,
},
followButton: {
height: 32,
justifyContent: "center",
alignItems: "center",
marginLeft: 16,
paddingLeft: 4,
borderWidth: 0,
elevation: 0,
},
novelGenres: {
// marginTop: 12,
paddingHorizontal: 16,
paddingBottom: 16,
},
genreChip: {
flex: 1,
justifyContent: "center",
paddingVertical: 2,
paddingHorizontal: 10,
marginHorizontal: 2,
fontSize: 13,
borderWidth: 1,
borderRadius: 50,
},
});
@@ -5,16 +5,8 @@ const NovelSummary = ({ summary, followed, theme }) => {
const [more, showMore] = useState(!followed);
return (
summary !== "" && (
summary && (
<View style={styles.summaryContainer}>
<Text
style={[
{ color: theme.textColorPrimary },
styles.summaryHeader,
]}
>
About
</Text>
<Text
style={{
color: theme.textColorSecondary,
@@ -48,14 +40,14 @@ export default NovelSummary;
const styles = StyleSheet.create({
summaryContainer: {
paddingHorizontal: 16,
marginBottom: 8,
},
summaryHeader: {
marginTop: 5,
paddingVertical: 5,
fontSize: 15,
fontWeight: "bold",
marginVertical: 16,
},
// summaryHeader: {
// marginTop: 5,
// paddingVertical: 5,
// fontSize: 15,
// fontWeight: "bold",
// },
showMoreButton: {
fontWeight: "bold",
position: "absolute",
@@ -1,7 +1,7 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Button } from "react-native-paper";
import { useContinueReading } from "../../../../Hooks/reduxHooks";
const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
return (
@@ -21,8 +21,9 @@ const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
chapterUrl: lastRead.chapterUrl,
novelUrl: novel.novelUrl,
novelId: lastRead.novelId,
extensionId: novel.sourceId,
sourceId: novel.sourceId,
chapterName: lastRead.chapterName,
novelName: novel.novelName,
});
}}
>
@@ -1,48 +0,0 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Chip } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
const TrackerChip = ({ isTracked, trackerSheetRef, theme }) => {
const getIconName = () => (isTracked ? "check" : "sync");
const getChipLabel = () => (isTracked ? "Tracked" : "Tracking");
return (
<Chip
mode="outlined"
icon={() => (
<MaterialCommunityIcons
name={getIconName(isTracked)}
size={21}
color={theme.colorAccent}
/>
)}
onPress={() => trackerSheetRef.current.show()}
style={[
styles.toggleFavourite,
{ backgroundColor: theme.colorPrimaryDark },
]}
textStyle={{
fontWeight: "bold",
color: theme.textColorPrimary,
}}
>
{getChipLabel()}
</Chip>
);
};
export default TrackerChip;
const styles = StyleSheet.create({
toggleFavourite: {
height: 32,
justifyContent: "center",
alignItems: "center",
paddingLeft: 4,
marginLeft: 0,
borderWidth: 0,
elevation: 0,
},
});
+105 -173
View File
@@ -1,20 +1,27 @@
import React, { useState } from "react";
import { View, Text, StyleSheet, Image, FlatList, Share } from "react-native";
import { TouchableRipple, IconButton, Button, Menu } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import React from "react";
import { View, Text, StyleSheet, Clipboard } from "react-native";
import * as WebBrowser from "expo-web-browser";
import { useSelector, useDispatch } from "react-redux";
import { TouchableRipple, IconButton } from "react-native-paper";
import { followNovelAction } from "../../../redux/novel/novel.actions";
import { useTrackingStatus } from "../../../Hooks/reduxHooks";
import {
downloadAllChaptersAction,
deleteAllChaptersAction,
} from "../../../redux/novel/novel.actions";
import { GenreChip } from "./Info/GenreChip";
import NovelCoverImage from "./Info/NovelCoverImage";
import FollowChip from "./Info/FollowChip";
import TrackerChip from "./Info/TrackerChip";
CoverImage,
NovelAuthor,
NovelInfo,
NovelInfoContainer,
NovelThumbnail,
NovelTitle,
FollowButton,
TrackerButton,
NovelGenres,
} from "./Info/NovelDetailsComponents";
import { Row } from "../../../Components/Common";
import NovelSummary from "./Info/NovelSummary";
import ReadButton from "./Info/ReadButton";
import { showToast } from "../../../Hooks/showToast";
const NovelInfoHeader = ({
item,
@@ -29,12 +36,7 @@ const NovelInfoHeader = ({
trackerSheetRef,
chaptersSettingsSheetRef,
}) => {
const [downloadMenu, showDownloadMenu] = useState(false);
const tracker = useSelector((state) => state.trackerReducer.tracker);
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
const { tracker, trackedNovels } = useTrackingStatus();
let isTracked = false;
@@ -42,153 +44,113 @@ const NovelInfoHeader = ({
isTracked = trackedNovels.find((obj) => obj.novelId === novel.novelId);
}
const getGenres = ({ item }) => <GenreChip theme={theme}>{item}</GenreChip>;
return (
<View style={{ flexGrow: 1 }}>
<NovelCoverImage source={{ uri: item.novelCover }} theme={theme}>
<View style={styles.detailsContainer}>
<View>
<Image
source={{ uri: item.novelCover }}
style={styles.logo}
/>
</View>
<View style={styles.nameContainer}>
<Text
numberOfLines={2}
style={[
styles.name,
{ color: theme.textColorPrimary },
]}
<View>
<CoverImage source={{ uri: item.novelCover }} theme={theme}>
<NovelInfoContainer>
<NovelThumbnail source={{ uri: item.novelCover }} />
<View style={styles.novelDetails}>
<NovelTitle
theme={theme}
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: item.novelName,
})
}
onLongPress={() => {
Clipboard.setString(item.novelName);
showToast(
"Copied to clipboard: " + item.novelName
);
}}
>
{item.novelName}
</Text>
<>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
fontWeight: "bold",
}}
numberOfLines={2}
>
{(item.author &&
item.author.replace(",", ", ")) ||
(!loading &&
novel.author.replace(",", ", "))}
</Text>
<Text
style={{
color: theme.textColorSecondary,
marginVertical: 3,
fontSize: 14,
}}
numberOfLines={1}
>
{item.status || (!loading && novel.status)}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 14,
}}
numberOfLines={1}
>
{item.source || (!loading && novel.source)}
</Text>
</>
</NovelTitle>
{!loading && (
<>
<NovelAuthor theme={theme}>
{novel.author}
</NovelAuthor>
<NovelInfo theme={theme}>
{novel.status}
</NovelInfo>
<NovelInfo theme={theme}>
{novel.source}
</NovelInfo>
</>
)}
</View>
</View>
</NovelCoverImage>
</NovelInfoContainer>
</CoverImage>
{!loading && (
<>
<View style={styles.buttonsContainer}>
<FollowChip
<Row
style={{
justifyContent: "space-around",
paddingHorizontal: 16,
flex: 1,
}}
>
<FollowButton
theme={theme}
followed={novel.followed}
novel={novel}
onPress={() => dispatch(followNovelAction(novel))}
/>
{tracker && (
<TrackerChip
<TrackerButton
theme={theme}
isTracked={isTracked}
trackerSheetRef={trackerSheetRef}
onPress={() => trackerSheetRef.current.show()}
/>
)}
<IconButton
onPress={() =>
WebBrowser.openBrowserAsync(novel.sourceUrl)
}
icon="earth"
color={theme.colorAccent}
size={21}
/>
<IconButton
onPress={() =>
Share.share({ message: novel.sourceUrl })
}
icon="share-variant"
color={theme.colorAccent}
size={21}
/>
<Menu
visible={downloadMenu}
onDismiss={() => showDownloadMenu(false)}
anchor={
<IconButton
icon="download"
color={theme.colorAccent}
size={21}
onPress={() => showDownloadMenu(true)}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
>
<Menu.Item
title="Download all"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{ color: theme.textColorPrimary }}
<View style={{ alignItems: "center", flex: 1 }}>
<IconButton
icon="swap-vertical-variant"
color={theme.textColorSecondary}
size={24}
style={{ margin: 0 }}
onPress={() =>
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
)
)
navigation.navigate("MigrateNovel", {
sourceId: novel.sourceId,
novelName: novel.novelName,
})
}
/>
<Menu.Item
title="Delete downloads"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{ color: theme.textColorPrimary }}
<Text
style={{
fontSize: 12,
color: theme.textColorSecondary,
}}
>
Migrate
</Text>
</View>
<View style={{ alignItems: "center", flex: 1 }}>
<IconButton
icon="earth"
color={theme.textColorSecondary}
size={24}
style={{ margin: 0 }}
onPress={() =>
dispatch(deleteAllChaptersAction(chapters))
WebBrowser.openBrowserAsync(novel.sourceUrl)
}
/>
</Menu>
</View>
<Text
style={{
fontSize: 12,
color: theme.textColorSecondary,
}}
>
WebView
</Text>
</View>
</Row>
<NovelSummary
summary={novel.novelSummary}
followed={novel.followed}
theme={theme}
/>
<FlatList
style={
novel.novelSummary === ""
? { marginTop: 20 }
: { marginTop: 12 }
}
contentContainerStyle={styles.genreContainer}
horizontal
data={novel.genre.split(",")}
keyExtractor={(item) => item}
renderItem={getGenres}
showsHorizontalScrollIndicator={false}
/>
<NovelGenres theme={theme} genre={novel.genre} />
<ReadButton
novel={novel}
chapters={chapters}
@@ -198,11 +160,7 @@ const NovelInfoHeader = ({
/>
<TouchableRipple
style={styles.bottomsheet}
onPress={() =>
chaptersSettingsSheetRef.current.show({
velocity: -1.5,
})
}
onPress={() => chaptersSettingsSheetRef.current.show()}
rippleColor={theme.rippleColor}
>
<>
@@ -234,32 +192,11 @@ const NovelInfoHeader = ({
export default NovelInfoHeader;
const styles = StyleSheet.create({
nameContainer: {
novelDetails: {
flex: 1,
marginHorizontal: 16,
paddingHorizontal: 16,
paddingTop: 8,
},
detailsContainer: {
flex: 1,
flexDirection: "row",
margin: 16,
paddingTop: 90,
},
logo: {
height: 160,
width: 110,
margin: 3.2,
borderRadius: 6,
},
name: {
fontWeight: "bold",
fontSize: 18,
},
buttonsContainer: {
flexDirection: "row",
alignItems: "center",
},
chapters: {
paddingHorizontal: 16,
paddingVertical: 4,
@@ -272,9 +209,4 @@ const styles = StyleSheet.create({
alignItems: "center",
paddingRight: 12,
},
genreContainer: {
paddingHorizontal: 15,
paddingBottom: 15,
},
});
+87 -24
View File
@@ -1,11 +1,14 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback } from "react";
import {
StyleSheet,
View,
FlatList,
ActivityIndicator,
RefreshControl,
Button,
Text,
} from "react-native";
import { useFocusEffect } from "@react-navigation/native";
import { useDispatch, useSelector } from "react-redux";
import EmptyView from "../../Components/EmptyView";
import {
@@ -15,17 +18,25 @@ import {
import { Appbar } from "./components/Appbar";
import UpdatesItem from "./components/UpdatesItem";
import { useTheme } from "../../Hooks/reduxHooks";
import { Searchbar } from "../../Components/Searchbar";
import moment from "moment";
import { dateFormat } from "../../Services/utils/constants";
const Updates = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { updates, loading } = useSelector((state) => state.updatesReducer);
const [searchText, setSearchText] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
dispatch(getUpdatesAction());
}, [getUpdatesAction, updates]);
useFocusEffect(
useCallback(() => {
dispatch(getUpdatesAction());
}, [getUpdatesAction])
);
const onRefresh = async () => {
setRefreshing(true);
@@ -37,10 +48,11 @@ const Updates = ({ navigation }) => {
navigation.navigate("Chapter", {
chapterId: item.chapterId,
chapterUrl: item.chapterUrl,
extensionId: item.sourceId,
sourceId: item.sourceId,
novelUrl: item.novelUrl,
chapterName: item.chapterName,
novelId: item.novelId,
novelName: item.novelName,
});
const renderItem = ({ item }) => (
@@ -64,26 +76,77 @@ const Updates = ({ navigation }) => {
/>
);
const clearSearchbar = () => {
setSearchText("");
};
const onChangeText = (text) => {
setSearchText(text);
let results = [];
text !== "" &&
updates.map((item) => {
const date = item.date;
const chapters = item.chapters.filter((chapter) =>
chapter.novelName.toLowerCase().includes(text.toLowerCase())
);
if (chapters.length > 0) {
results.push({
date,
chapters,
});
}
});
setSearchResults(results);
};
return (
<>
<Appbar theme={theme} dispatch={dispatch} />
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
contentContainerStyle={styles.flatList}
data={updates}
keyExtractor={(item) => item.updateId.toString()}
renderItem={renderItem}
ListFooterComponent={ListFooterComponent()}
ListEmptyComponent={ListEmptyComponent()}
refreshControl={refreshControl()}
/>
</View>
</>
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
placeholder="Search Updates"
searchText={searchText}
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
left="magnify"
theme={theme}
right="reload"
onPressRight={() => dispatch(updateLibraryAction())}
/>
<FlatList
contentContainerStyle={styles.flatList}
data={searchText ? searchResults : updates}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<FlatList
ListHeaderComponent={
<Text
style={{
paddingHorizontal: 16,
textTransform: "uppercase",
paddingVertical: 8,
color: theme.textColorSecondary,
}}
>
{moment(item.date).calendar(null, dateFormat)}
</Text>
}
data={item.chapters}
keyExtractor={(item) => item.updateId.toString()}
renderItem={renderItem}
/>
)}
ListFooterComponent={ListFooterComponent()}
ListEmptyComponent={ListEmptyComponent()}
refreshControl={refreshControl()}
/>
</View>
);
};
+18 -18
View File
@@ -1,7 +1,6 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple } from "react-native-paper";
import moment from "moment";
const UpdateCard = ({ item, theme, onPress }) => {
return (
@@ -18,26 +17,27 @@ const UpdateCard = ({ item, theme, onPress }) => {
/>
<View style={styles.chapterDetails}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
style={[
{ color: theme.textColorPrimary, fontSize: 14 },
item.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={{ flexDirection: "row" }}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 12,
}}
>
{`${item.chapterName} · ${moment(
item.updateTime
).calendar()}`}
</Text>
</View>
<Text
style={[
{ color: theme.textColorPrimary, fontSize: 12 },
item.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{item.chapterName}
</Text>
</View>
</>
</TouchableRipple>
@@ -51,7 +51,7 @@ const styles = StyleSheet.create({
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 12,
paddingHorizontal: 16,
},
updateIcon: {
width: 42,
+2
View File
@@ -1,3 +1,5 @@
import { showToast } from "../../Hooks/showToast";
export const fetchSources = async () => {
const url = `https://lnreader-extensions.vercel.app/api/`;
+35 -3
View File
@@ -5,10 +5,23 @@ import { getLibrary } from "../Database/queries/LibraryQueries";
import { restoreLibrary } from "../Database/queries/NovelQueries";
import { showToast } from "../Hooks/showToast";
import * as Notifications from "expo-notifications";
Notifications.setNotificationHandler({
handleNotification: async () => {
return {
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
});
export const createBackup = async () => {
const novels = await getLibrary();
const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
const permissions =
await StorageAccessFramework.requestDirectoryPermissionsAsync();
if (!permissions.granted) {
return;
@@ -35,10 +48,29 @@ export const createBackup = async () => {
export const restoreBackup = async () => {
const backup = await DocumentPicker.getDocumentAsync();
Notifications.scheduleNotificationAsync({
content: { title: "Restoring backup" },
trigger: null,
});
if (backup.uri) {
let novels = await StorageAccessFramework.readAsStringAsync(backup.uri);
novels = await JSON.parse(novels);
await novels.map((novel) => restoreLibrary(novel));
showToast("Restored backup");
novels.map((novel, index) => {
setTimeout(async () => {
if (index + 1 === novels.length) {
Notifications.scheduleNotificationAsync({
content: {
title: "Library Restored",
body: novels.length + " novels restored",
},
trigger: null,
});
}
await restoreLibrary(novel);
}, 1000 * index);
});
}
};
+16 -2
View File
@@ -113,13 +113,27 @@ export const updateNovelChapters = async (extensionId, novelUrl, novelId) => {
export const updateAllNovels = async () => {
const libraryNovels = await getLibrary();
Notifications.scheduleNotificationAsync({
identifier: "updatingNotif",
content: { title: "Updating library" },
trigger: null,
});
libraryNovels.map((novel, index) =>
setTimeout(async () => {
updateNovelChapters(novel.sourceId, novel.novelUrl, novel.novelId);
await updateNovelChapters(
novel.sourceId,
novel.novelUrl,
novel.novelId
);
console.log(novel.novelName + " Updated");
if (index + 1 === libraryNovels.length) {
Notifications.scheduleNotificationAsync({
content: { title: "Library Updated" },
content: {
title: "Library Updated",
body: libraryNovels.length + " novels updated",
},
trigger: null,
});
}
+9 -2
View File
@@ -1,4 +1,4 @@
const fonts = [
export const fonts = [
{ fontFamily: "", name: "Original" },
{ fontFamily: "arbutus-slab", name: "Arbutus Slab" },
{ fontFamily: "domine", name: "Domine" },
@@ -8,4 +8,11 @@ const fonts = [
{ fontFamily: "pt-serif", name: "PT Serif" },
];
export { fonts };
export const dateFormat = {
sameDay: "[Today]",
nextDay: "[Tomorrow]",
nextWeek: "dddd",
lastDay: "[Yesterday]",
lastWeek: "[Last] dddd",
sameElse: "DD/MM/YYYY",
};
+15
View File
@@ -0,0 +1,15 @@
export const parseChapterNumber = (chapterName) => {
chapterName = chapterName.toLowerCase();
chapterName = chapterName.replace(/volume (\d+)/, "");
const basic = chapterName.match(/[ch]i (\d+)/);
const occurrence = chapterName.match(/\d+/);
if (basic) {
return basic[0];
} else if (occurrence) {
return occurrence[0];
} else {
return 0;
}
};
+1
View File
@@ -8,6 +8,7 @@ export const fonts = {
domine: require("../../assets/fonts/Domine-VariableFont_wght.ttf"),
lato: require("../../assets/fonts/Lato-Regular.ttf"),
"noto-sans": require("../../assets/fonts/NotoSans-Regular.ttf"),
"noto-sans-bold": require("../../assets/fonts/NotoSans-Bold.ttf"),
"open-sans": require("../../assets/fonts/OpenSans-Regular.ttf"),
"pt-serif": require("../../assets/fonts/PTSerif-Regular.ttf"),
};
+34 -11
View File
@@ -8,11 +8,11 @@ export const theme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.06)",
};
/**
* Light Theme
* Light
*/
export const lightTheme = {
id: 1,
@@ -23,18 +23,18 @@ export const lightTheme = {
textColorPrimary: "#000000",
textColorSecondary: "rgba(0,0,0,0.54)",
textColorHint: "rgba(0,0,0,0.38)",
rippleColor: "rgba(0,0,0,0.12)",
rippleColor: "rgba(0,0,0,0.06)",
searchBarColor: "#FFFFFF",
menuColor: "#FFFFFF",
dividerColor: "rgba(0,0,0,0.1)",
filterColor: "#FFC107",
colorDisabled: "rgba(220,220,220,255)",
colorButtonText: "#000000",
colorButtonText: "#FFFFFF",
statusBar: "dark",
};
/**
* Dark Theme
* Dark
*/
export const darkTheme = {
id: 2,
@@ -45,7 +45,7 @@ export const darkTheme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#303135",
menuColor: "#242529",
filterColor: "#FFEB3B",
@@ -56,7 +56,7 @@ export const darkTheme = {
};
/**
* Amoled Dark Theme
* Amoled Dark
*/
export const amoledDarkTheme = {
id: 0,
@@ -67,7 +67,7 @@ export const amoledDarkTheme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.12)",
searchBarColor: "#1F1F1F",
menuColor: "#242529",
filterColor: "#FFEB3B",
@@ -78,7 +78,7 @@ export const amoledDarkTheme = {
};
/**
* Midnight Dusk Theme
* Midnight Dusk
*/
export const midnightDuskTheme = {
id: 3,
@@ -89,7 +89,7 @@ export const midnightDuskTheme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#201F27",
menuColor: "#201F27",
filterColor: "#FFEB3B",
@@ -99,6 +99,10 @@ export const midnightDuskTheme = {
statusBar: "light",
};
/**
* Lime
*/
export const limeTheme = {
id: 4,
name: "Lime",
@@ -108,7 +112,7 @@ export const limeTheme = {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#201F27",
menuColor: "#201F27",
filterColor: "#FFEB3B",
@@ -117,3 +121,22 @@ export const limeTheme = {
colorButtonText: "#16151D",
statusBar: "light",
};
export const irisBlueTheme = {
id: 5,
name: "Iris Blue",
colorPrimary: "#393e46",
colorPrimaryDark: "#222831",
colorAccent: "#00adb5",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#393e46",
menuColor: "#393e46",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "#393e46",
colorButtonText: "#16151D",
statusBar: "light",
};
-94
View File
@@ -1,94 +0,0 @@
import { ToastAndroid } from "react-native";
import {
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
} from "./chapter.types";
import { fetchChapter } from "../../Services/Source/source";
import {
markChapterRead,
isChapterDownloaded,
getChapter,
downloadChapter,
deleteChapter,
} from "../../Database/queries/ChapterQueries";
import { SET_LAST_READ } from "../preferences/preference.types";
export const getChapterAction = (
extensionId,
chapterUrl,
novelUrl,
novelId
) => async (dispatch) => {
dispatch({ type: CHAPTER_LOADING });
let chapter = await getChapter(novelId);
if (!chapter) {
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
}
console.log(isDownloaded);
dispatch({ type: GET_CHAPTER, payload: chapter });
};
// export const markChapterReadAction = (chapterId, novelId) => async (
// dispatch
// ) => {
// await markChapterRead(chapterId);
// console.log("DIspatchedbsjs");
// dispatch({
// type: CHAPTER_READ,
// payload: { chapterId },
// });
// dispatch({
// type: SET_LAST_READ,
// payload: { novelId, chapterId: chapterId + 1 },
// });
// console.log("Set Last Read Diaptched", chapterId);
// };
export const downloadChapterAction = (
extensionId,
novelUrl,
chapterUrl,
chapterName,
chapterId
) => async (dispatch) => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: {
chapterId,
},
});
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
payload: { chapterId },
});
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
export const deleteChapterAction = (chapterId, chapterName) => async (
dispatch
) => {
await deleteChapter(chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: { chapterId },
});
ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
};
-65
View File
@@ -1,65 +0,0 @@
import {
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
} from "./chapter.types";
const initialState = {
chapter: null,
downloading: [],
loading: true,
};
const chapterReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case CHAPTER_READ:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...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: payload.chapterId,
};
case CHAPTER_DOWNLOADED:
return {
...state,
downloading: null,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...chapter, downloaded: 1 }
: chapter
),
};
case CHAPTER_DELETED:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...chapter, downloaded: 0 }
: chapter
),
};
default:
return state;
}
};
export default chapterReducer;
-6
View File
@@ -1,6 +0,0 @@
export const CHAPTER_DOWNLOADED = "CHAPTER_DOWNLOADED";
export const CHAPTER_DELETED = "CHAPTER_DELETED";
export const CHAPTER_DOWNLOADING = "CHAPTER_DOWNLOADING";
export const CHAPTER_READ = "CHAPTER_READ";
export const CHAPTER_LOADING = "CHAPTER_LOADING";
export const GET_CHAPTER = "GET_CHAPTER";
+39 -18
View File
@@ -1,5 +1,4 @@
import {
UPDATE_NOVEL_HISTORY,
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
@@ -12,43 +11,65 @@ import {
deleteAllHistory,
} from "../../Database/queries/HistoryQueries";
import { SET_LAST_READ } from "../preferences/preference.types";
import { showToast } from "../../Hooks/showToast";
export const getHistoryAction = () => async (dispatch) => {
dispatch({ type: LOAD_HISTORY });
const history = await getHistory();
dispatch({ type: GET_HISTORY, payload: history });
const groups = history.reduce((groups, update) => {
var dateParts = update.historyTimeRead.split("-");
var jsDate = new Date(
dateParts[0],
dateParts[1] - 1,
dateParts[2].substr(0, 2)
);
const date = jsDate.toISOString();
if (!groups[date]) {
groups[date] = [];
}
groups[date].push(update);
return groups;
}, {});
const groupedHistory = Object.keys(groups).map((date) => {
return {
date,
novels: groups[date],
};
});
// console.log(groupedHistory);
dispatch({ type: GET_HISTORY, payload: groupedHistory });
};
export const insertHistoryAction = (novelId, chapterId) => async (dispatch) => {
await insertHistory(novelId, chapterId);
dispatch({
type: UPDATE_NOVEL_HISTORY,
});
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId },
});
const history = await getHistory();
dispatch({ type: GET_HISTORY, payload: history });
dispatch(getHistoryAction());
};
export const deleteHistoryAction = (novelId) => async (dispatch) => {
await deleteHistory(novelId);
dispatch({
type: CLEAR_NOVEL_HISTORY,
payload: { novelId },
});
export const deleteHistoryAction = (historyId) => async (dispatch) => {
await deleteHistory(historyId);
console.log("Delete History Action Called");
dispatch(getHistoryAction());
};
export const clearAllHistoryAction = () => async (dispatch) => {
deleteAllHistory();
try {
await deleteAllHistory();
dispatch({ type: CLEAR_HISTORY });
showToast("History deleted");
dispatch({ type: CLEAR_HISTORY });
} catch (error) {
showToast(error.message);
}
};
+8 -14
View File
@@ -3,7 +3,6 @@ import {
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
UPDATE_NOVEL_HISTORY,
} from "./history.types";
const initialState = {
@@ -19,20 +18,15 @@ const historyReducer = (state = initialState, action) => {
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.novelId !== payload.novelId
),
};
// case CLEAR_NOVEL_HISTORY:
// return {
// ...state,
// history: state.history.filter(
// (item) => item.novelId !== payload.novelId
// ),
// };
case CLEAR_HISTORY:
return {
...state,
history: [],
};
return { ...state, history: [] };
default:
return state;
}
+30 -11
View File
@@ -1,6 +1,8 @@
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
SET_LIBRARY_LOADING,
SORT_FILTER_LIBRARY,
} from "./library.types";
import {
getLibrary,
@@ -8,8 +10,9 @@ import {
} from "../../Database/queries/LibraryQueries";
import { restoreBackup } from "../../Services/backup";
export const getLibraryAction = () => async (dispatch) => {
const res = await getLibrary();
export const getLibraryAction = (sort, filter) => async (dispatch) => {
// console.log("1.", sort, filter);
const res = await getLibrary(sort, filter);
dispatch({
type: GET_LIBRARY_NOVELS,
@@ -17,22 +20,38 @@ export const getLibraryAction = () => async (dispatch) => {
});
};
export const searchLibraryAction = (searchText) => async (dispatch) => {
const res = await searchLibrary(searchText);
export const searchLibraryAction =
(searchText, sort, filter) => async (dispatch) => {
const res = await searchLibrary(searchText, sort, filter);
dispatch({
type: GET_LIBRARY_SEARCH_RESULTS,
payload: res,
});
};
export const filterLibrary = (sort, filter) => async (dispatch) => {
dispatch({ type: SORT_FILTER_LIBRARY, payload: { sort, filter } });
const res = await getLibrary(sort, filter);
dispatch({
type: GET_LIBRARY_SEARCH_RESULTS,
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
export const restoreLibraryAction = () => async (dispatch) => {
await restoreBackup();
dispatch({ type: SET_LIBRARY_LOADING });
const res = await getLibrary();
const res = await restoreBackup();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
if (res) {
const novels = await getLibrary();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: novels,
});
}
};
+8 -7
View File
@@ -1,16 +1,14 @@
import {
FOLLOW_NOVEL,
UNFOLLOW_NOVEL,
UPDATE_IN_LIBRARY,
} from "../novel/novel.types";
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
SET_LIBRARY_LOADING,
SORT_FILTER_LIBRARY,
} from "./library.types";
const initialState = {
novels: [],
searchResults: [],
filters: { sort: "novels.novelId ASC", filter: "" },
loading: true,
};
@@ -18,14 +16,17 @@ const libraryReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case SET_LIBRARY_LOADING:
return { ...state, loading: true };
case GET_LIBRARY_NOVELS:
return { ...state, novels: payload, loading: false };
case GET_LIBRARY_SEARCH_RESULTS:
return { ...state, novels: payload, loading: false };
case UPDATE_IN_LIBRARY:
case SORT_FILTER_LIBRARY:
// console.log("2.", payload.sort, payload.filter);
return {
...state,
novels: [...state.novels, { novelUrl: payload.novelUrl }],
filters: { sort: payload.sort, filter: payload.filter },
};
default:
return state;
+3
View File
@@ -1,2 +1,5 @@
export const GET_LIBRARY_NOVELS = "GET_LIBRARY_NOVELS";
export const GET_LIBRARY_SEARCH_RESULTS = "GET_LIBRARY_SEARCH_RESULTS";
export const SET_LIBRARY_LOADING = "SET_LIBRARY_LOADING";
export const SORT_FILTER_LIBRARY = "SORT_FILTER_LIBRARY";
+253 -151
View File
@@ -4,21 +4,24 @@ import {
LOADING_NOVEL,
GET_NOVEL,
GET_CHAPTERS,
NOVEL_ERROR,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
UPDATE_NOVEL,
SET_NOVEL_SETTINGS,
CHAPTER_UNREAD,
BOOKMARK_CHAPTER,
MARK_PREVIOUS_CHAPTERS_READ,
MARK_PREVIOUS_CHAPTERS_UNREAD,
} from "./novel.types";
import { updateNovel } from "../../Services/updates";
import { fetchChapter, fetchNovel } from "../../Services/Source/source";
import { fetchNovel } from "../../Services/Source/source";
import {
followNovel,
insertNovel,
@@ -28,81 +31,117 @@ import {
getChapters,
insertChapters,
markChapterRead,
isChapterDownloaded,
getChapter,
downloadChapter,
deleteChapter,
markChapterUnread,
bookmarkChapter,
markPreviuschaptersRead,
markPreviousChaptersUnread,
getNextChapterFromDB,
} from "../../Database/queries/ChapterQueries";
import { deleteNovelUpdates } from "../../Database/queries/UpdateQueries";
import {
SAVE_SCROLL_POSITION,
SET_CHAPTER_LIST_PREF,
SET_LAST_READ,
} from "../preferences/preference.types";
import { GET_LIBRARY_NOVELS } from "../library/library.types";
import { getLibrary } from "../../Database/queries/LibraryQueries";
import { showToast } from "../../Hooks/showToast";
import { store } from "../store";
export const setNovel = (novel) => async (dispatch) => {
dispatch({ type: SET_NOVEL, payload: novel });
};
export const getNovelAction = (
followed,
sourceId,
novelUrl,
novelId,
sort,
filter
) => async (dispatch) => {
dispatch({ type: LOADING_NOVEL });
export const getNovelAction =
(followed, sourceId, novelUrl, novelId, sort, filter) =>
async (dispatch) => {
try {
dispatch({ type: LOADING_NOVEL });
if (followed === 1) {
/**
* If novel is followed directly get the chapters from db.
*/
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({ type: FETCHING_NOVEL });
/**
* Check if novel is cached.
*/
const novel = await getNovel(sourceId, novelUrl);
if (novel) {
/**
* Get chapters from db.
*/
novel.chapters = await getChapters(
novel.novelId,
sort,
filter
);
dispatch({
type: GET_NOVEL,
payload: novel,
});
} else {
/**
* Fetch novel from source.
*/
const fetchedNovel = await fetchNovel(sourceId, novelUrl);
/**
* Insert novel in db.
*/
const fetchedNovelId = await insertNovel(fetchedNovel);
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
/**
* Get novel from db.
*/
const novel = await getNovel(sourceId, novelUrl);
novel.chapters = await getChapters(novel.novelId);
dispatch({
type: GET_NOVEL,
payload: novel,
});
}
}
} catch (error) {
showToast(error.message);
// dispatch({ type: NOVEL_ERROR });
}
};
export const sortAndFilterChapters =
(novelId, sort, filter) => async (dispatch) => {
dispatch({ type: FETCHING_NOVEL });
if (followed === 1) {
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({ type: FETCHING_NOVEL });
const novel = await getNovel(sourceId, novelUrl);
if (novel) {
novel.chapters = await getChapters(novel.novelId, sort, filter);
dispatch({
type: GET_NOVEL,
payload: novel,
});
} else {
const fetchedNovel = await fetchNovel(sourceId, novelUrl);
const fetchedNovelId = await insertNovel(fetchedNovel);
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
const novel = await getNovel(sourceId, novelUrl);
novel.chapters = await getChapters(novel.novelId);
dispatch({
type: GET_NOVEL,
payload: novel,
});
}
}
};
export const sortAndFilterChapters = (novelId, sort, filter) => async (
dispatch
) => {
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: SET_CHAPTER_LIST_PREF,
payload: { novelId, sort, filter },
});
};
export const showChapterTitlesAction = (novelId, value) => async (dispatch) => {
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
dispatch({
type: SET_CHAPTER_LIST_PREF,
payload: { novelId, sort, filter },
type: SET_NOVEL_SETTINGS,
payload: { novelId, value: value },
});
};
@@ -125,107 +164,171 @@ export const followNovelAction = (novel) => async (dispatch) => {
payload: res,
});
ToastAndroid.show(
!novel.followed ? "Added to library" : "Removed from library",
ToastAndroid.SHORT
);
showToast(!novel.followed ? "Added to library" : "Removed from library");
};
export const getChapterAction = (
extensionId,
chapterUrl,
novelUrl,
novelId
) => async (dispatch) => {
dispatch({ type: CHAPTER_LOADING });
export const bookmarkChapterAction = (chapters) => async (dispatch) => {
chapters.map((chapter) => {
bookmarkChapter(chapter.bookmark, chapter.chapterId);
const isDownloaded = await isChapterDownloaded(novelId);
let chapter;
dispatch({
type: BOOKMARK_CHAPTER,
payload: {
bookmark: chapter.bookmark,
chapterId: chapter.chapterId,
},
});
});
};
if (isDownloaded) {
chapter = await getChapter(novelId);
} else {
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
export const markChapterReadAction =
(chapterId, novelId) => async (dispatch) => {
await markChapterRead(chapterId);
dispatch({
type: CHAPTER_READ,
payload: { chapterId },
});
const nextChapter = await getNextChapterFromDB(novelId, chapterId);
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId: nextChapter.chapterId },
});
/**
* Reset progress on marked read
*/
dispatch({
type: SAVE_SCROLL_POSITION,
payload: {
position: 0,
percentage: 0,
chapterId,
novelId,
},
});
};
export const markPreviousChaptersReadAction =
(chapterId, novelId) => async (dispatch) => {
await markPreviuschaptersRead(chapterId, novelId);
dispatch({
type: MARK_PREVIOUS_CHAPTERS_READ,
payload: chapterId,
});
showToast("Marked previous chapters read");
};
export const markChaptersRead = (chapters, novelId) => async (dispatch) => {
try {
const { sort = "ORDER BY chapterId ASC", filter = "" } =
store.getState().preferenceReducer.novelSettings[novelId];
chapters.map((chapter) => markChapterRead(chapter.chapterId));
const chaps = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chaps,
});
} catch (error) {
showToast(error.message);
}
dispatch({ type: GET_CHAPTER, payload: chapter });
};
export const markChapterReadAction = (chapterId, novelId) => async (
dispatch
) => {
await markChapterRead(chapterId);
dispatch({ type: CHAPTER_READ, payload: { chapterId } });
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId: chapterId + 1 },
});
};
export const markPreviousChaptersUnreadAction =
(chapterId, novelId) => async (dispatch) => {
await markPreviousChaptersUnread(chapterId, novelId);
export const downloadChapterAction = (
extensionId,
novelUrl,
chapterUrl,
chapterName,
chapterId
) => async (dispatch) => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapterId,
});
dispatch({
type: MARK_PREVIOUS_CHAPTERS_UNREAD,
payload: chapterId,
});
};
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
export const markChapterUnreadAction =
(chapters, novelId) => async (dispatch) => {
const { sort = "ORDER BY chapterId ASC", filter = "" } =
store.getState().preferenceReducer.novelSettings[novelId];
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapterId,
});
chapters.map((chapter) => markChapterUnread(chapter.chapterId));
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
const chaps = await getChapters(novelId, sort, filter);
export const downloadAllChaptersAction = (
extensionId,
novelUrl,
chapters
) => async (dispatch) => {
await chapters.map((chapter, index) => {
setTimeout(async () => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapter.chapterId,
dispatch({
type: GET_CHAPTERS,
payload: chaps,
});
};
export const downloadChapterAction =
(extensionId, novelUrl, chapterUrl, chapterName, chapterId) =>
async (dispatch) => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapterId,
});
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapterId,
});
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
export const downloadAllChaptersAction =
(extensionId, novelUrl, chapters) => async (dispatch) => {
try {
await chapters.map((chapter, index) => {
setTimeout(async () => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapter.chapterId,
});
if (!chapter.downloaded) {
await downloadChapter(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterId
);
}
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapter.chapterId,
});
if (index + 1 === chapters.length) {
showToast(`Download completed `);
}
}, 1000 * index);
});
} catch (error) {
showToast(error.message);
}
};
if (!chapter.downloaded) {
await downloadChapter(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterId
);
}
export const deleteChapterAction =
(chapterId, chapterName) => async (dispatch) => {
await deleteChapter(chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapter.chapterId,
});
}, 1000 * index);
});
dispatch({
type: CHAPTER_DELETED,
payload: chapterId,
});
ToastAndroid.show(`All chapters downloaded`, ToastAndroid.SHORT);
};
export const deleteChapterAction = (chapterId, chapterName) => async (
dispatch
) => {
await deleteChapter(chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: chapterId,
});
ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
};
showToast(`Deleted ${chapterName}`);
};
export const deleteAllChaptersAction = (chapters) => async (dispatch) => {
await chapters.map((chapter) => {
@@ -237,21 +340,20 @@ export const deleteAllChaptersAction = (chapters) => async (dispatch) => {
});
});
ToastAndroid.show(`Deleted all chapters`, ToastAndroid.SHORT);
showToast("Chapters deleted");
};
export const updateNovelAction = (sourceId, novelUrl, novelId) => async (
dispatch
) => {
dispatch({ type: FETCHING_NOVEL });
export const updateNovelAction =
(sourceId, novelUrl, novelId, sort, filter) => async (dispatch) => {
dispatch({ type: FETCHING_NOVEL });
await updateNovel(sourceId, novelUrl, novelId);
await updateNovel(sourceId, novelUrl, novelId);
let novel = await getNovel(sourceId, novelUrl);
let chapters = await getChapters(novel.novelId);
let novel = await getNovel(sourceId, novelUrl);
let chapters = await getChapters(novel.novelId, sort, filter);
dispatch({
type: UPDATE_NOVEL,
payload: { novel, chapters },
});
};
dispatch({
type: UPDATE_NOVEL,
payload: { novel, chapters },
});
};
+59 -21
View File
@@ -5,24 +5,25 @@ import {
GET_CHAPTERS,
LOADING_NOVEL,
GET_NOVEL,
GET_CHAPTER,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
NOVEL_ERROR,
CHAPTER_READ,
CHAPTER_LOADING,
UPDATE_NOVEL,
UPDATE_LAST_READ,
CHAPTER_UNREAD,
BOOKMARK_CHAPTER,
MARK_PREVIOUS_CHAPTERS_READ,
MARK_PREVIOUS_CHAPTERS_UNREAD,
} from "./novel.types";
const initialState = {
novel: null,
novel: {},
chapters: [],
chapter: null,
downloading: [],
chapterLoading: true,
loading: true,
fetching: false,
updating: false,
downloading: [],
};
const novelReducer = (state = initialState, action) => {
@@ -32,7 +33,7 @@ const novelReducer = (state = initialState, action) => {
case LOADING_NOVEL:
return { ...state, loading: true };
case FETCHING_NOVEL:
return { ...state, fetching: true };
return { ...state, updating: true };
case SET_NOVEL:
return { ...state, novel: payload };
case GET_NOVEL:
@@ -41,14 +42,14 @@ const novelReducer = (state = initialState, action) => {
novel: payload,
chapters: payload.chapters,
loading: false,
fetching: false,
updating: false,
};
case GET_CHAPTERS:
return {
...state,
chapters: payload,
loading: false,
fetching: false,
updating: false,
};
case UPDATE_IN_LIBRARY:
return {
@@ -61,7 +62,7 @@ const novelReducer = (state = initialState, action) => {
novel: payload.novel,
chapters: payload.chapters,
loading: false,
fetching: false,
updating: false,
};
case CHAPTER_READ:
return {
@@ -72,22 +73,29 @@ const novelReducer = (state = initialState, action) => {
: chapter
),
};
case CHAPTER_LOADING:
return {
...state,
chapterLoading: true,
};
case GET_CHAPTER:
return { ...state, chapter: payload, chapterLoading: false };
case CHAPTER_DOWNLOADING:
case CHAPTER_UNREAD:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload
? { ...chapter, downloaded: 3 }
chapter.chapterId === payload.chapterId
? { ...chapter, read: 0 }
: chapter
),
};
case BOOKMARK_CHAPTER:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...chapter, bookmark: !payload.bookmark }
: chapter
),
};
case CHAPTER_DOWNLOADING:
return {
...state,
downloading: [...state.downloading, payload],
};
case CHAPTER_DOWNLOADED:
return {
...state,
@@ -96,6 +104,9 @@ const novelReducer = (state = initialState, action) => {
? { ...chapter, downloaded: 1 }
: chapter
),
downloading: state.downloading.filter(
(chapterId) => chapterId !== payload
),
};
case CHAPTER_DELETED:
return {
@@ -111,6 +122,33 @@ const novelReducer = (state = initialState, action) => {
...state,
novel: { ...state.novel, lastRead: payload },
};
case MARK_PREVIOUS_CHAPTERS_READ:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId < payload
? { ...chapter, read: 1 }
: chapter
),
};
case MARK_PREVIOUS_CHAPTERS_UNREAD:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId < payload
? { ...chapter, read: 0 }
: chapter
),
};
case NOVEL_ERROR:
return {
...state,
novel: {},
chapters: [],
loading: false,
updating: false,
};
default:
return state;
}
+6 -4
View File
@@ -12,10 +12,12 @@ export const CHAPTER_DOWNLOADED = "CHAPTER_DOWNLOADED";
export const CHAPTER_DELETED = "CHAPTER_DELETED";
export const CHAPTER_DOWNLOADING = "CHAPTER_DOWNLOADING";
export const CHAPTER_READ = "CHAPTER_READ";
export const CHAPTER_LOADING = "CHAPTER_LOADING";
export const CHAPTER_UNREAD = "CHAPTER_UNREAD";
export const GET_CHAPTER = "GET_CHAPTER";
export const FOLLOW_NOVEL = "FOLLOW_NOVEL";
export const UNFOLLOW_NOVEL = "UNFOLLOW_NOVEL";
export const NOVEL_ERROR = "NOVEL_ERROR";
export const BOOKMARK_CHAPTER = "BOOKMARK_CHAPTER";
export const SET_NOVEL_SETTINGS = "SET_NOVEL_SETTINGS";
export const MARK_PREVIOUS_CHAPTERS_READ = "MARK_PREVIOUS_CHAPTERS_READ";
export const MARK_PREVIOUS_CHAPTERS_UNREAD = "MARK_PREVIOUS_CHAPTERS_UNREAD";
@@ -0,0 +1,14 @@
import { SAVE_SCROLL_POSITION } from "../preferences/preference.types";
export const saveScrollPosition =
(position, percentage, chapterId, novelId) => async (dispatch) => {
dispatch({
type: SAVE_SCROLL_POSITION,
payload: {
position,
percentage,
chapterId,
novelId,
},
});
};
+50 -36
View File
@@ -1,7 +1,12 @@
import { SET_CHAPTER_LIST_PREF, SET_LAST_READ } from "./preference.types";
import { SET_NOVEL_SETTINGS } from "../novel/novel.types";
import {
SAVE_SCROLL_POSITION,
SET_CHAPTER_LIST_PREF,
SET_LAST_READ,
} from "./preference.types";
const initialState = {
novelSettings: [],
novelSettings: {},
};
const novelReducer = (state = initialState, action) => {
@@ -11,44 +16,53 @@ const novelReducer = (state = initialState, action) => {
case SET_LAST_READ:
return {
...state,
novelSettings: !state.novelSettings.some(
(novel) => novel.novelId === payload.novelId
)
? [
{
novelId: payload.novelId,
lastRead: payload.chapterId,
},
]
: state.novelSettings.map((novel) =>
novel.novelId === payload.novelId
? { ...novel, lastRead: payload.chapterId }
: novel
),
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
lastRead: payload.chapterId,
},
},
};
case SET_CHAPTER_LIST_PREF:
return {
...state,
novelSettings: state.novelSettings.some(
(novel) => novel.novelId === payload.novelId
)
? state.novelSettings.map((novel) =>
novel.novelId === payload.novelId
? {
...novel,
sort: payload.sort,
filter: payload.filter,
}
: novel
)
: [
...state.novelSettings,
{
novelId: payload.novelId,
sort: payload.sort,
filter: payload.filter,
},
],
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
sort: payload.sort,
filter: payload.filter,
},
},
};
case SET_NOVEL_SETTINGS:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
showChapterTitles: payload.value,
},
},
};
case SAVE_SCROLL_POSITION:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
position: {
...state.novelSettings[payload.novelId].position,
[payload.chapterId]: {
position: payload.position,
percentage: payload.percentage,
},
},
},
},
};
default:
return state;
@@ -1,2 +1,3 @@
export const SET_CHAPTER_LIST_PREF = "SET_CHAPTER_LIST_PREF";
export const SET_LAST_READ = "SET_LAST_READ";
export const SAVE_SCROLL_POSITION = "SAVE_SCROLL_POSITION";
+17 -41
View File
@@ -1,66 +1,42 @@
import {
SWITCH_THEME,
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
UPDATE_READER_TEXT_ALIGN,
UPDATE_READER_PADDING,
SET_READER_FONT,
SET_APP_THEME,
SET_NOVELS_PER_ROW,
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
} from "./settings.types";
export const switchTheme = (val) => (dispatch) => {
export const setAppTheme = (val) => (dispatch) => {
dispatch({
type: SWITCH_THEME,
type: SET_APP_THEME,
payload: val,
});
};
export const setDisplayMode = (val) => (dispatch) => {
export const setNovelsPerRow = (val) => (dispatch) => {
dispatch({
type: SET_DISPLAY_MODE,
type: SET_NOVELS_PER_ROW,
payload: val,
});
};
export const setItemsPerRow = (val) => (dispatch) => {
export const setAppSettings = (key, val) => (dispatch) => {
dispatch({
type: SET_ITEMS_PER_ROW,
payload: val,
type: SET_APP_SETTINGS,
payload: { key, val },
});
};
export const updateReaderTheme = (val) => (dispatch) => {
export const setReaderSettings = (key, val) => (dispatch) => {
dispatch({
type: UPDATE_READER_THEME,
payload: val,
type: SET_READER_SETTINGS,
payload: { key, val },
});
};
export const updateReaderTextSize = (val) => (dispatch) => {
export const setAccentColor = (val) => (dispatch) => {
dispatch({
type: UPDATE_READER_TEXT_SIZE,
payload: val,
});
};
export const updateReaderTextAlign = (val) => (dispatch) => {
dispatch({
type: UPDATE_READER_TEXT_ALIGN,
payload: val,
});
};
export const updateReaderPadding = (val) => (dispatch) => {
dispatch({
type: UPDATE_READER_PADDING,
payload: val,
});
};
export const setReaderFont = (val) => (dispatch) => {
dispatch({
type: SET_READER_FONT,
type: SET_ACCENT_COLOR,
payload: val,
});
};
+42 -44
View File
@@ -1,13 +1,9 @@
import { SET_NOVEL_SETTINGS } from "../novel/novel.types";
import {
SWITCH_THEME,
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
SET_READER_FONT,
UPDATE_READER_PADDING,
UPDATE_READER_TEXT_ALIGN,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
SET_APP_THEME,
SET_NOVELS_PER_ROW,
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
} from "./settings.types";
import {
amoledDarkTheme,
@@ -15,6 +11,7 @@ import {
darkTheme,
midnightDuskTheme,
limeTheme,
irisBlueTheme,
} from "../../Theme/theme";
const themes = {
@@ -23,73 +20,74 @@ const themes = {
2: darkTheme,
3: midnightDuskTheme,
4: limeTheme,
};
const initialState = {
displayMode: 0,
itemsPerRow: 3,
reader: {
theme: 1,
textSize: 16,
textAlign: "left",
padding: 5,
fontFamily: null,
},
theme: themes[2],
5: irisBlueTheme,
};
/**
* Display Mode
*
* 0 -> Compact
* 1 -> Comfortable
* 2 -> List
*/
/**
* Reader Settings
* theme
* Reader Theme
*
* 1 -> Dark
* 2 -> White
* 3 -> Sepia
*/
const initialState = {
theme: darkTheme,
displayMode: 0,
novelsPerRow: 3,
reader: {
theme: 1,
textSize: 16,
textAlign: "left",
padding: 5,
fontFamily: "",
lineHeight: 1.5,
},
showDownloadBadges: true,
showUnreadBadges: true,
showNumberOfNovels: false,
};
const settingsReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case SWITCH_THEME:
case SET_APP_THEME:
return { ...state, theme: themes[payload] };
case SET_DISPLAY_MODE:
case SET_NOVELS_PER_ROW:
return {
...state,
displayMode: payload,
novelsPerRow: state.displayMode !== 2 ? payload : 1,
};
case SET_ITEMS_PER_ROW:
case SET_ACCENT_COLOR:
return {
...state,
itemsPerRow: state.displayMode !== 2 ? payload : 1,
theme: {
...state.theme,
colorAccent: payload,
},
};
case UPDATE_READER_THEME:
return { ...state, reader: { ...state.reader, theme: payload } };
case UPDATE_READER_TEXT_SIZE:
return { ...state, reader: { ...state.reader, textSize: payload } };
case UPDATE_READER_TEXT_ALIGN:
case SET_APP_SETTINGS:
return {
...state,
reader: { ...state.reader, textAlign: payload },
[payload.key]: payload.val,
};
case UPDATE_READER_PADDING:
case SET_READER_SETTINGS:
return {
...state,
reader: { ...state.reader, padding: payload },
reader: {
...state.reader,
[payload.key]: payload.val,
},
};
case SET_READER_FONT:
return {
...state,
reader: { ...state.reader, fontFamily: payload },
};
case SET_NOVEL_SETTINGS:
console.log(state.novelSettings);
default:
return state;
}
+5 -8
View File
@@ -1,8 +1,5 @@
export const SWITCH_THEME = "SWITCH_THEME";
export const SET_DISPLAY_MODE = "SET_DISPLAY_MODE";
export const SET_ITEMS_PER_ROW = "SET_ITEMS_PER_ROW";
export const UPDATE_READER_THEME = "UPDATE_READER_THEME";
export const UPDATE_READER_TEXT_SIZE = "UPDATE_READER_TEXT_SIZE";
export const UPDATE_READER_TEXT_ALIGN = "UPDATE_READER_TEXT_ALIGN";
export const UPDATE_READER_PADDING = "UPDATE_READER_PADDING";
export const SET_READER_FONT = "SET_READER_FONT";
export const SET_APP_SETTINGS = "SET_APP_SETTINGS";
export const SET_APP_THEME = "SET_APP_THEME";
export const SET_NOVELS_PER_ROW = "SET_NOVELS_PER_ROW";
export const SET_READER_SETTINGS = "SET_READER_SETTINGS";
export const SET_ACCENT_COLOR = "SET_ACCENT_COLOR";
+13 -6
View File
@@ -1,19 +1,26 @@
import { GET_SOURCES, PIN_SOURCES, SEARCH_SOURCES } from "./source.types";
import { fetchSources } from "../../Services/Source/source";
import { showToast } from "../../Hooks/showToast";
export const getSourcesAction = () => async (dispatch) => {
const res = await fetchSources();
try {
const res = await fetchSources();
dispatch({
type: GET_SOURCES,
payload: res,
});
dispatch({
type: GET_SOURCES,
payload: res,
});
} catch (error) {
showToast(error.message);
}
};
export const searchSourcesAction = (searchText) => async (dispatch) => {
const res = searchText.toLowerCase();
dispatch({
type: SEARCH_SOURCES,
payload: searchText,
payload: res,
});
};
+1 -1
View File
@@ -27,7 +27,7 @@ const sourceReducer = (state = initialState, action) => {
return {
...state,
search: state.sources.filter((source) =>
source.sourceName.includes(payload)
source.sourceName.toLowerCase().includes(payload)
),
};
default:

Some files were not shown because too many files have changed in this diff Show More