Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b5ee1667c | |||
| d5b3915ef1 | |||
| 7b41f10dd6 | |||
| b12eef2699 | |||
| 7a8c8ded24 | |||
| b6e8b0057b | |||
| c06ce93581 | |||
| c4c156eb1d | |||
| f1d86b798e | |||
| 4f1aa7c2cc | |||
| f8199b4e4e | |||
| 82e7111a40 | |||
| a15369967f | |||
| 5a936bea02 | |||
| 13b49dfd21 | |||
| 689185bab6 | |||
| 917dea2261 | |||
| 6afcdcf96e | |||
| b74066111f | |||
| b42fb6319b | |||
| cb05231335 | |||
| 7819c7dcdb | |||
| 9a7e1f7e53 | |||
| 2c2e814a0d | |||
| f4fc3d096f | |||
| d230b64846 | |||
| a280fc95e1 | |||
| 1f87a50762 | |||
| 99945aa0d8 | |||
| fda59f825f | |||
| 67096eb1ff | |||
| 3a156f32a0 | |||
| 555552a64d | |||
| 53fc800b90 | |||
| d1e10433e2 | |||
| cc53e309ad | |||
| 2170b77607 | |||
| 632c2e47c7 | |||
| 5e2c4d4e27 | |||
| a21800f208 | |||
| 5f943fcc92 | |||
| 8f254b4ef5 | |||
| 773913048c |
@@ -12,7 +12,9 @@ import { fonts } from "./src/Theme/fonts";
|
||||
const App = () => {
|
||||
const [loaded] = useFonts(fonts);
|
||||
|
||||
useEffect(() => createDB(), []);
|
||||
useEffect(() => {
|
||||
createDB();
|
||||
}, []);
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 28 KiB |
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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({});
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;");
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, "");
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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))
|
||||
}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { showToast } from "../../Hooks/showToast";
|
||||
|
||||
export const fetchSources = async () => {
|
||||
const url = `https://lnreader-extensions.vercel.app/api/`;
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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"),
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||