Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 625acdb585 | |||
| 93617e60bd | |||
| d75c607f67 | |||
| dd087ff485 | |||
| 5da3e05bba | |||
| e628c3cfd6 | |||
| deba021384 | |||
| c6d2ba8442 | |||
| 6a3736dced | |||
| f63ae573ba | |||
| 8f3020a544 | |||
| 04e32a4234 | |||
| 9b3d65dded | |||
| aa3a5c2d0c | |||
| 2aab5621a6 | |||
| b4b7e6770d | |||
| 0ed65d64e4 | |||
| a7aaa7d230 | |||
| e358b3753f | |||
| 780a67e1a6 | |||
| 5b57e78486 | |||
| 6430c11cae | |||
| 322bfc224d | |||
| a14d35f031 | |||
| f03b1703e2 | |||
| 40d2052f16 | |||
| 01814e92d0 | |||
| dffc9c3b99 | |||
| 8bb3ffe82c | |||
| c746081dee | |||
| d25fe85a64 | |||
| 92d9d53da7 | |||
| 17fcd96f16 | |||
| 31bada1c1a | |||
| 605bd6a514 | |||
| 01bac76bed | |||
| b5ed62d9f8 | |||
| fd21cc6e85 | |||
| 40f1495cf1 | |||
| 983b069dba | |||
| e21101ca28 | |||
| d381e878bc | |||
| bcf5632668 | |||
| ac8bca7f03 | |||
| 62a5e27b1e | |||
| 7c5a9a407f | |||
| d8d231cb90 | |||
| a5f90eaf8b | |||
| ec50189ea4 | |||
| ccdccdb6bb | |||
| 059f440713 | |||
| b695f57ef4 | |||
| db86b707e7 | |||
| 7db4e4b872 | |||
| a2c6e9fe58 | |||
| 6c27993454 | |||
| 63de0a3794 | |||
| 93fbcf1246 | |||
| 50357f7d6f |
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -6,6 +6,7 @@ import { Provider as PaperProvider } from "react-native-paper";
|
||||
import { Provider } from "react-redux";
|
||||
import { persistor, store } from "./src/redux/store";
|
||||
import { PersistGate } from "redux-persist/lib/integration/react";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
|
||||
import Main from "./src/navigators/Main";
|
||||
|
||||
@@ -16,6 +17,7 @@ const App = () => {
|
||||
const [loaded] = useFonts(fonts);
|
||||
|
||||
useEffect(() => {
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
createDB();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
<h1 align="center">
|
||||
<br>
|
||||
LNReader
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<p align="center">LNReader is a Tachiyomi-like, free and open source light novel reader for Android.</p>
|
||||
|
||||
---
|
||||
|
||||
## Screenshots:
|
||||
<p align="center">
|
||||
<img src="./.github/readme-images/screenshots.png" align="center" height="350" />
|
||||
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
|
||||
<p>
|
||||
<h1 align="center">
|
||||
LNReader
|
||||
</h1>
|
||||
<p align="center">
|
||||
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
|
||||
</p>
|
||||
<div align="center">
|
||||
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/LNReader/lnreader/CI">
|
||||
</a>
|
||||
<a href="https://discord.gg/QdcWN4MD63">
|
||||
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader/releases/latest">
|
||||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<img src="./.github/readme-images/screenshots.png" align="center" />
|
||||
</p>
|
||||
|
||||
## Download
|
||||
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
|
||||
|
||||
## Sources
|
||||
LNReader does not have any affiliation with the content providers available.
|
||||
|
||||
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
|
||||
|
||||
## License
|
||||
[MIT ](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
|
||||
|
||||
@@ -136,7 +136,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.1.1"
|
||||
versionName "1.1.4"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Generated
+2
-19
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "lnreader-eject",
|
||||
"name": "lnreader",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lnreader-eject",
|
||||
"name": "lnreader",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.0",
|
||||
@@ -36,7 +36,6 @@
|
||||
"react-native-device-info": "^8.1.3",
|
||||
"react-native-easing-gradient": "^0.1.2",
|
||||
"react-native-fast-image": "^8.3.4",
|
||||
"react-native-fit-image": "^1.5.5",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-navigation-bar-color": "^2.0.1",
|
||||
"react-native-pager-view": "^5.1.10",
|
||||
@@ -8355,14 +8354,6 @@
|
||||
"react-native": ">=0.60.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/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",
|
||||
@@ -17943,14 +17934,6 @@
|
||||
"integrity": "sha512-LpzAdjUphihUpVEBn5fEv5AILe55rHav0YiZroPZ1rumKDhAl4u2cG01ku2Pb7l8sayjTsNu7FuURAlXUUDsow==",
|
||||
"requires": {}
|
||||
},
|
||||
"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",
|
||||
|
||||
+1
-2
@@ -35,7 +35,6 @@
|
||||
"react-native-device-info": "^8.1.3",
|
||||
"react-native-easing-gradient": "^0.1.2",
|
||||
"react-native-fast-image": "^8.3.4",
|
||||
"react-native-fit-image": "^1.5.5",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-navigation-bar-color": "^2.0.1",
|
||||
"react-native-pager-view": "^5.1.10",
|
||||
@@ -68,6 +67,6 @@
|
||||
"arrowParens": "always"
|
||||
},
|
||||
"private": true,
|
||||
"name": "lnreader-eject",
|
||||
"name": "lnreader",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
+5
-123
@@ -1,17 +1,11 @@
|
||||
import React, { useRef } from "react";
|
||||
import { View, TextInput, StyleSheet } from "react-native";
|
||||
import {
|
||||
TouchableRipple,
|
||||
IconButton,
|
||||
Appbar as MaterialAppbar,
|
||||
} from "react-native-paper";
|
||||
import Constants from "expo-constants";
|
||||
import React from "react";
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Appbar as MaterialAppbar } from "react-native-paper";
|
||||
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
|
||||
export const Appbar = ({ title, onBackAction, style, children }) => {
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<MaterialAppbar.Header
|
||||
@@ -28,115 +22,3 @@ export const Appbar = ({ title, onBackAction, style, children }) => {
|
||||
</MaterialAppbar.Header>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchAppbar = ({
|
||||
placeholder,
|
||||
getSearchResults,
|
||||
setSearchText,
|
||||
searchText,
|
||||
getNovels,
|
||||
setLoading,
|
||||
screen,
|
||||
openWebView,
|
||||
}) => {
|
||||
const searchRef = useRef(null);
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
borderless
|
||||
onPress={() => searchRef.current.focus()}
|
||||
style={[
|
||||
styles.searchAppbarContainer,
|
||||
{ backgroundColor: theme.searchBarColor },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, flexDirection: "row" }}>
|
||||
{screen === "Library" ? (
|
||||
<IconButton
|
||||
icon="magnify"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginLeft: 0 }}
|
||||
size={23}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginLeft: 0 }}
|
||||
size={23}
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
onChangeText={(text) => {
|
||||
setSearchText(text);
|
||||
if (screen === "Library") {
|
||||
getSearchResults(text);
|
||||
}
|
||||
}}
|
||||
onSubmitEditing={() => {
|
||||
if (screen === "Extension") {
|
||||
getSearchResults(searchText);
|
||||
}
|
||||
}}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</View>
|
||||
{searchText !== "" && (
|
||||
<IconButton
|
||||
icon="close"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={() => {
|
||||
setLoading?.(true);
|
||||
getNovels();
|
||||
setSearchText("");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{openWebView && (
|
||||
<IconButton
|
||||
icon="earth"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={openWebView}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchAppbarContainer: {
|
||||
marginTop: Constants.statusBarHeight + 8,
|
||||
height: 48,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 12,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,11 +4,11 @@ import { View } from "react-native";
|
||||
const BottomSheetHandle = ({ theme }) => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.textColorHint,
|
||||
height: 5,
|
||||
width: 30,
|
||||
backgroundColor: "rgba(255,255,255,0.4)",
|
||||
height: 4,
|
||||
width: 40,
|
||||
borderRadius: 50,
|
||||
top: 10,
|
||||
top: 8,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
export const Checkbox = ({ label, status, onPress, theme }) => (
|
||||
<View
|
||||
style={[
|
||||
styles.checkboxContainer,
|
||||
status && { backgroundColor: theme.rippleColor },
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: status ? theme.colorAccent : theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{status && (
|
||||
<MaterialCommunityIcons
|
||||
name="check"
|
||||
color={theme.colorAccent}
|
||||
size={21}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export const SortItem = ({ label, status, onPress, theme }) => (
|
||||
<View
|
||||
style={[
|
||||
styles.checkboxContainer,
|
||||
status && { backgroundColor: theme.rippleColor },
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: status ? theme.colorAccent : theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{status && (
|
||||
<MaterialCommunityIcons
|
||||
name={status === "asc" ? "arrow-up" : "arrow-down"}
|
||||
color={theme.colorAccent}
|
||||
size={21}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
checkboxContainer: {
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginVertical: 4,
|
||||
},
|
||||
pressable: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
icon: {
|
||||
position: "absolute",
|
||||
right: 16,
|
||||
alignSelf: "center",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginHorizontal: 4,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 10,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={
|
||||
selected
|
||||
? theme.colorAccent
|
||||
: color
|
||||
? color
|
||||
: theme.textColorPrimary
|
||||
}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export const ToggleColorButton = ({
|
||||
selected,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
onPress,
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 50,
|
||||
overflow: "hidden",
|
||||
marginHorizontal: 6,
|
||||
height: 44,
|
||||
width: 44,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: textColor }}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
backgroundColor: backgroundColor,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
{selected && (
|
||||
<MaterialCommunityIcons
|
||||
name="check"
|
||||
color={textColor}
|
||||
size={24}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import { IconButton } from "react-native-paper";
|
||||
|
||||
const icons = ["(・o・;)", "Σ(ಠ_ಠ)", "ಥ_ಥ", "(˘・_・˘)", "(; ̄Д ̄)", "(・Д・。"];
|
||||
|
||||
export const ErrorView = ({ errorName, actions, theme }) => (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<Text style={[styles.emptyViewIcon, { color: theme.textColorHint }]}>
|
||||
{/* {icons[Math.floor(Math.random() * 5)]} */}
|
||||
ಥ_ಥ
|
||||
</Text>
|
||||
<Text style={[styles.emptyViewText, { color: theme.textColorHint }]}>
|
||||
{errorName}
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
{actions.map((action) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
margin: 16,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
borderless: false,
|
||||
}}
|
||||
onPress={action.onPress}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: 20,
|
||||
minWidth: 100,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
icon={action.icon}
|
||||
color={theme.textColorHint}
|
||||
size={24}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: theme.textColorHint,
|
||||
}}
|
||||
>
|
||||
{action.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
emptyViewContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
emptyViewIcon: {
|
||||
fontSize: 45,
|
||||
},
|
||||
emptyViewText: {
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
textAlign: "center",
|
||||
paddingHorizontal: 30,
|
||||
},
|
||||
});
|
||||
|
||||
+57
-10
@@ -1,19 +1,19 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { List, Divider as PaperDivider } from "react-native-paper";
|
||||
import { List as PaperList, 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>
|
||||
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
|
||||
);
|
||||
|
||||
const ListSubHeader = ({ children, theme }) => (
|
||||
<List.Subheader
|
||||
<PaperList.Subheader
|
||||
style={[styles.listSubHeader, { color: theme.colorAccent }]}
|
||||
>
|
||||
{children}
|
||||
</List.Subheader>
|
||||
</PaperList.Subheader>
|
||||
);
|
||||
|
||||
const ListItem = ({
|
||||
@@ -27,7 +27,7 @@ const ListItem = ({
|
||||
titleStyle,
|
||||
style,
|
||||
}) => (
|
||||
<List.Item
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
style={style}
|
||||
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
|
||||
@@ -37,7 +37,7 @@ const ListItem = ({
|
||||
left={() => (
|
||||
<View style={{ justifyContent: "center" }}>
|
||||
{icon && (
|
||||
<List.Icon
|
||||
<PaperList.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{
|
||||
@@ -67,13 +67,13 @@ const Divider = ({ theme }) => (
|
||||
);
|
||||
|
||||
const InfoItem = ({ title, icon, theme }) => (
|
||||
<List.Item
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
|
||||
titleNumberOfLines={5}
|
||||
left={() =>
|
||||
icon && (
|
||||
<List.Icon
|
||||
<PaperList.Icon
|
||||
color={theme.textColorSecondary}
|
||||
icon={icon}
|
||||
style={{ marginVertical: 0 }}
|
||||
@@ -83,7 +83,54 @@ const InfoItem = ({ title, icon, theme }) => (
|
||||
/>
|
||||
);
|
||||
|
||||
export { ListSection, ListSubHeader, ListItem, Divider, InfoItem };
|
||||
const ColorItem = ({ title, description, theme, onPress }) => (
|
||||
<Pressable
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View>
|
||||
<Text style={{ color: theme.textColorPrimary, fontSize: 16 }}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: description,
|
||||
height: 24,
|
||||
width: 24,
|
||||
borderRadius: 50,
|
||||
marginRight: 16,
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const Icon = ({ icon, theme }) => (
|
||||
<PaperList.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const List = {
|
||||
Section: ListSection,
|
||||
SubHeader: ListSubHeader,
|
||||
Item: ListItem,
|
||||
Divider,
|
||||
InfoItem,
|
||||
ColorItem,
|
||||
Icon,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listSection: {
|
||||
|
||||
+35
-31
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import { StyleSheet, View, Text, Pressable } from "react-native";
|
||||
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
|
||||
const ListView = ({
|
||||
item,
|
||||
@@ -10,39 +10,43 @@ const ListView = ({
|
||||
inLibraryBadge,
|
||||
theme,
|
||||
onPress,
|
||||
isSelected,
|
||||
onLongPress,
|
||||
}) => {
|
||||
return (
|
||||
<TouchableRipple
|
||||
borderless
|
||||
centered
|
||||
rippleColor={theme.rippleColor}
|
||||
style={styles.listView}
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={[
|
||||
styles.listView,
|
||||
isSelected && { backgroundColor: theme.rippleColor },
|
||||
]}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
>
|
||||
<>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={[
|
||||
styles.extensionIcon,
|
||||
inLibraryBadge && { opacity: 0.5 },
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary },
|
||||
styles.novelName,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
<View style={styles.badgeContainer}>
|
||||
{downloadBadge}
|
||||
{unreadBadge}
|
||||
{inLibraryBadge}
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
<FastImage
|
||||
source={{
|
||||
uri:
|
||||
item.novelCover && !item.novelCover.startsWith("/")
|
||||
? item.novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
style={[
|
||||
styles.extensionIcon,
|
||||
inLibraryBadge && { opacity: 0.5 },
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[{ color: theme.textColorPrimary }, styles.novelName]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
<View style={styles.badgeContainer}>
|
||||
{downloadBadge}
|
||||
{unreadBadge}
|
||||
{inLibraryBadge}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,15 @@ import ListView from "./ListView";
|
||||
import { getDeviceOrientation } from "../services/utils/helpers";
|
||||
import { useSettings } from "../hooks/reduxHooks";
|
||||
|
||||
const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
|
||||
const NovelCover = ({
|
||||
item,
|
||||
onPress,
|
||||
libraryStatus,
|
||||
theme,
|
||||
isSelected,
|
||||
onLongPress,
|
||||
selectedNovels,
|
||||
}) => {
|
||||
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
|
||||
useSettings();
|
||||
|
||||
@@ -31,18 +39,32 @@ const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
|
||||
[]
|
||||
);
|
||||
|
||||
const selectNovel = () => onLongPress && onLongPress(item.novelId);
|
||||
|
||||
return displayMode !== 2 ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1 / getNovelsPerRow(),
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
flex: 1 / getNovelsPerRow(),
|
||||
borderRadius: 6,
|
||||
overflow: "hidden",
|
||||
margin: 2,
|
||||
},
|
||||
isSelected && {
|
||||
backgroundColor: theme.colorAccent,
|
||||
opacity: 0.8,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
style={styles.opac}
|
||||
onPress={onPress}
|
||||
onPress={
|
||||
selectedNovels && selectedNovels.length > 0
|
||||
? selectNovel
|
||||
: onPress
|
||||
}
|
||||
onLongPress={selectNovel}
|
||||
>
|
||||
<>
|
||||
<View style={styles.badgeContainer}>
|
||||
@@ -64,7 +86,13 @@ const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
|
||||
)}
|
||||
</View>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
source={{
|
||||
uri:
|
||||
item.novelCover &&
|
||||
!item.novelCover.startsWith("/")
|
||||
? item.novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
style={[
|
||||
{ height: getHeight(), borderRadius: 4 },
|
||||
libraryStatus && { opacity: 0.5 },
|
||||
@@ -110,7 +138,13 @@ const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
|
||||
}
|
||||
inLibraryBadge={libraryStatus && <InLibraryBadge theme={theme} />}
|
||||
theme={theme}
|
||||
onPress={onPress}
|
||||
onPress={
|
||||
selectedNovels && selectedNovels.length > 0
|
||||
? selectNovel
|
||||
: onPress
|
||||
}
|
||||
onLongPress={selectNovel}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -140,7 +174,15 @@ const CompactTitle = ({ novelName }) => (
|
||||
>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
|
||||
style={[
|
||||
styles.title,
|
||||
{
|
||||
color: "rgba(255,255,255,1)",
|
||||
textShadowColor: "rgba(0, 0, 0, 0.75)",
|
||||
textShadowOffset: { width: -1, height: 1 },
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{novelName}
|
||||
</Text>
|
||||
@@ -221,12 +263,10 @@ const styles = StyleSheet.create({
|
||||
padding: 8,
|
||||
},
|
||||
linearGradient: {
|
||||
bottom: -1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
opac: {
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 4,
|
||||
padding: 4.8,
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
},
|
||||
@@ -245,7 +285,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
|
||||
downloadBadge: {
|
||||
backgroundColor: "#47a84a",
|
||||
backgroundColor: "#2979FF",
|
||||
color: "#FFFFFF",
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
|
||||
@@ -33,7 +33,10 @@ const NovelList = ({
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.flatListCont}
|
||||
contentContainerStyle={[
|
||||
styles.flatListCont,
|
||||
displayMode !== 2 && { paddingHorizontal: 4 },
|
||||
]}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={[orientation, getNovelsPerRow()]}
|
||||
data={data}
|
||||
@@ -54,6 +57,5 @@ const styles = StyleSheet.create({
|
||||
flatListCont: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,20 +78,20 @@ export const Searchbar = ({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchbarContainer: {
|
||||
marginTop: StatusBar.currentHeight + 4,
|
||||
height: 48,
|
||||
marginTop: StatusBar.currentHeight + 8,
|
||||
height: 46,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 6,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 12,
|
||||
elevation: 2,
|
||||
backgroundColor: "white",
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -126,6 +126,32 @@ export const markChapterUnread = async (chapterId) => {
|
||||
});
|
||||
};
|
||||
|
||||
const markAllChaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE novelId = ?`;
|
||||
|
||||
export const markAllChaptersRead = async (novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
markAllChaptersReadQuery,
|
||||
[novelId],
|
||||
(tx, res) => {},
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markAllChaptersUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE novelId = ?`;
|
||||
|
||||
export const markAllChaptersUnread = async (novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
markAllChaptersUnreadQuery,
|
||||
[novelId],
|
||||
(tx, res) => {},
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const isChapterDownloadedQuery = `SELECT * FROM downloads WHERE downloadChapterId=?`;
|
||||
|
||||
export const isChapterDownloaded = async (chapterId) => {
|
||||
@@ -147,7 +173,7 @@ export const isChapterDownloaded = async (chapterId) => {
|
||||
);
|
||||
};
|
||||
|
||||
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?)`;
|
||||
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)`;
|
||||
|
||||
export const downloadChapter = async (
|
||||
sourceId,
|
||||
@@ -166,15 +192,10 @@ export const downloadChapter = async (
|
||||
);
|
||||
tx.executeSql(
|
||||
downloadChapterQuery,
|
||||
[
|
||||
chapterId,
|
||||
chapter.chapterName,
|
||||
chapter.chapterText,
|
||||
chapter.prevChapter,
|
||||
chapter.nextChapter,
|
||||
],
|
||||
(tx, res) =>
|
||||
console.log(`Downloaded Chapter ${chapter.chapterUrl}`),
|
||||
[chapterId, chapter.chapterName, chapter.chapterText],
|
||||
(tx, res) => {
|
||||
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
@@ -261,7 +282,7 @@ export const markPreviousChaptersUnread = async (chapterId, novelId) => {
|
||||
};
|
||||
|
||||
const getDownloadedChaptersQuery = `
|
||||
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover
|
||||
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover, novels.novelUrl
|
||||
FROM chapters
|
||||
JOIN novels
|
||||
ON chapters.novelId = novels.novelId
|
||||
|
||||
@@ -13,7 +13,7 @@ const getHistoryQuery = `
|
||||
ORDER BY history.historyTimeRead DESC
|
||||
`;
|
||||
|
||||
export const getHistory = async () => {
|
||||
export const getHistoryFromDb = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
import BackgroundService from "react-native-background-actions";
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
|
||||
import { fetchChapters, fetchNovel } from "../../services/Source/source";
|
||||
import { insertChapters } from "./ChapterQueries";
|
||||
@@ -46,6 +47,17 @@ export const followNovel = async (followed, novelId) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const unfollowNovel = async (novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE novels SET followed = 0 WHERE novelId = ?",
|
||||
[novelId],
|
||||
(tx, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const checkNovelInCacheQuery = `SELECT * FROM novels WHERE novelUrl=? LIMIT 1`;
|
||||
|
||||
export const checkNovelInCache = (novelUrl) => {
|
||||
@@ -193,3 +205,38 @@ export const migrateNovel = async (sourceId, novelUrl) => {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateNovelInfo = async (info, novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE novels SET novelName = ?, novelSummary = ?, author = ?, genre = ?, status = ? WHERE novelId = ?",
|
||||
[
|
||||
info.novelName,
|
||||
info.novelSummary,
|
||||
info.author,
|
||||
info.genre,
|
||||
info.status,
|
||||
novelId,
|
||||
],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const setCustomNovelCover = async (novelId) => {
|
||||
const image = await DocumentPicker.getDocumentAsync({ type: "image/*" });
|
||||
|
||||
if (image.uri) {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE novels SET novelCover = ? WHERE novelId = ?",
|
||||
[image.uri, novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
|
||||
return image.uri;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,8 +4,6 @@ export const createDownloadTableQuery = `
|
||||
downloadChapterId INTEGER NOT NULL,
|
||||
chapterName TEXT,
|
||||
chapterText TEXT,
|
||||
nextChapter TEXT,
|
||||
prevChapter TEXT,
|
||||
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
|
||||
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
|
||||
};
|
||||
|
||||
const isNewVersion = (versionTag) => {
|
||||
let currentVersion = "v1.1.1";
|
||||
let currentVersion = "v1.1.4";
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
let newVersion = versionTag.replace(regex, "");
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { shallowEqual, useSelector } from "react-redux";
|
||||
import { showChapterTitlesAction } from "../redux/novel/novel.actions";
|
||||
import { useSelector } from "react-redux";
|
||||
import { darkTheme } from "../theme/theme";
|
||||
|
||||
const useTheme = () => {
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
|
||||
return theme;
|
||||
return theme ?? darkTheme;
|
||||
};
|
||||
|
||||
const useReaderSettings = () => {
|
||||
|
||||
@@ -9,17 +9,24 @@ import History from "../screens/History/History";
|
||||
import Browse from "../screens/Browse/Browse";
|
||||
import More from "../screens/More/More";
|
||||
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
import { useSettings, useTheme } from "../hooks/reduxHooks";
|
||||
|
||||
const Tab = createMaterialBottomTabNavigator();
|
||||
|
||||
const BottomNavigator = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
showHistoryTab = true,
|
||||
showUpdatesTab = true,
|
||||
showLabelsInNav = false,
|
||||
} = useSettings();
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
barStyle={{ backgroundColor: theme.colorPrimary }}
|
||||
activeColor={theme.colorAccent}
|
||||
shifting={!showLabelsInNav}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Library"
|
||||
@@ -34,32 +41,36 @@ const BottomNavigator = () => {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{showUpdatesTab && (
|
||||
<Tab.Screen
|
||||
name="Updates"
|
||||
component={Updates}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="alert-decagram-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showHistoryTab && (
|
||||
<Tab.Screen
|
||||
name="History"
|
||||
component={History}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="history"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Tab.Screen
|
||||
name="Browse"
|
||||
component={Browse}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createStackNavigator } from "@react-navigation/stack";
|
||||
import { setBarColor } from "../theme/utils/setBarColor";
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
import { githubUpdateChecker } from "../hooks/githubUpdateChecker";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
|
||||
// Navigators
|
||||
import BottomNavigator from "./BottomNavigator";
|
||||
@@ -30,7 +31,10 @@ const MainNavigator = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setBarColor(theme);
|
||||
setTimeout(async () => {
|
||||
await SplashScreen.hideAsync();
|
||||
setBarColor(theme);
|
||||
}, 500);
|
||||
}, [theme]);
|
||||
|
||||
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
|
||||
|
||||
@@ -31,6 +31,7 @@ export const resumeDownloads = (chapters) => async (dispatch) => {
|
||||
parameters: {
|
||||
delay: 1000,
|
||||
},
|
||||
linkingURI: "lnreader://downloads",
|
||||
progressBar: {
|
||||
max: chapters.length,
|
||||
value: 0,
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import {
|
||||
CLEAR_NOVEL_HISTORY,
|
||||
GET_HISTORY,
|
||||
LOAD_HISTORY,
|
||||
CLEAR_HISTORY,
|
||||
} from "./history.types";
|
||||
import {
|
||||
getHistory,
|
||||
insertHistory,
|
||||
deleteHistory,
|
||||
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();
|
||||
|
||||
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: SET_LAST_READ,
|
||||
payload: { novelId, chapterId },
|
||||
});
|
||||
|
||||
dispatch(getHistoryAction());
|
||||
};
|
||||
|
||||
export const deleteHistoryAction = (historyId) => async (dispatch) => {
|
||||
await deleteHistory(historyId);
|
||||
console.log("Delete History Action Called");
|
||||
dispatch(getHistoryAction());
|
||||
};
|
||||
|
||||
export const clearAllHistoryAction = () => async (dispatch) => {
|
||||
try {
|
||||
await deleteAllHistory();
|
||||
|
||||
showToast("History deleted");
|
||||
|
||||
dispatch({ type: CLEAR_HISTORY });
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import {
|
||||
CLEAR_HISTORY,
|
||||
CLEAR_NOVEL_HISTORY,
|
||||
GET_HISTORY,
|
||||
LOAD_HISTORY,
|
||||
} from "./history.types";
|
||||
|
||||
const initialState = {
|
||||
history: [],
|
||||
loading: true,
|
||||
};
|
||||
|
||||
const historyReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case LOAD_HISTORY:
|
||||
return { ...state, loading: true };
|
||||
case GET_HISTORY:
|
||||
return { ...state, history: payload, loading: false };
|
||||
// case CLEAR_NOVEL_HISTORY:
|
||||
// return {
|
||||
// ...state,
|
||||
// history: state.history.filter(
|
||||
// (item) => item.novelId !== payload.novelId
|
||||
// ),
|
||||
// };
|
||||
case CLEAR_HISTORY:
|
||||
return { ...state, loading: false, history: [] };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default historyReducer;
|
||||
@@ -1,5 +0,0 @@
|
||||
export const LOAD_HISTORY = "LOAD_HISTORY";
|
||||
export const GET_HISTORY = "GET_HISTORY";
|
||||
export const UPDATE_NOVEL_HISTORY = "UPDATE_NOVEL_HISTORY";
|
||||
export const CLEAR_NOVEL_HISTORY = "CLEAR_NOVEL_HISTORY";
|
||||
export const CLEAR_HISTORY = "CLEAR_HISTORY";
|
||||
@@ -266,9 +266,9 @@ export const markPreviousChaptersUnreadAction =
|
||||
};
|
||||
|
||||
export const markChapterUnreadAction =
|
||||
(chapters, novelId) => async (dispatch) => {
|
||||
(chapters, novelId) => async (dispatch, getState) => {
|
||||
const { sort = "ORDER BY chapterId ASC", filter = "" } =
|
||||
store.getState().preferenceReducer.novelSettings[novelId];
|
||||
getState().preferenceReducer.novelSettings[novelId];
|
||||
|
||||
chapters.map((chapter) => markChapterUnread(chapter.chapterId));
|
||||
|
||||
@@ -323,6 +323,7 @@ export const downloadAllChaptersAction =
|
||||
type: "drawable",
|
||||
},
|
||||
color: "#00adb5",
|
||||
linkingURI: "lnreader://downloads/redirect",
|
||||
parameters: {
|
||||
delay: 1000,
|
||||
},
|
||||
|
||||
@@ -8,25 +8,30 @@ import {
|
||||
SET_AMOLED_MODE,
|
||||
} from "./settings.types";
|
||||
import {
|
||||
amoledDarkTheme,
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
midnightDuskTheme,
|
||||
greenAppleTheme,
|
||||
irisBlueTheme,
|
||||
hotPinkTheme,
|
||||
strawberryDaiquiri,
|
||||
takoTheme,
|
||||
yinYangTheme,
|
||||
springBlossomTheme,
|
||||
colorsAmoled,
|
||||
oceanicTheme,
|
||||
} from "../../theme/theme";
|
||||
|
||||
const themes = {
|
||||
0: amoledDarkTheme,
|
||||
1: lightTheme,
|
||||
2: darkTheme,
|
||||
3: midnightDuskTheme,
|
||||
4: greenAppleTheme,
|
||||
5: irisBlueTheme,
|
||||
6: hotPinkTheme,
|
||||
6: oceanicTheme,
|
||||
7: strawberryDaiquiri,
|
||||
8: takoTheme,
|
||||
9: yinYangTheme,
|
||||
10: springBlossomTheme,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -37,14 +42,6 @@ const themes = {
|
||||
* 2 -> List
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reader Theme
|
||||
*
|
||||
* 1 -> Dark
|
||||
* 2 -> White
|
||||
* 3 -> Sepia
|
||||
*/
|
||||
|
||||
const initialState = {
|
||||
theme: darkTheme,
|
||||
displayMode: 0,
|
||||
@@ -57,6 +54,7 @@ const initialState = {
|
||||
padding: 5,
|
||||
fontFamily: "",
|
||||
lineHeight: 1.5,
|
||||
customCSS: null,
|
||||
},
|
||||
showDownloadBadges: true,
|
||||
showUnreadBadges: true,
|
||||
@@ -68,6 +66,10 @@ const initialState = {
|
||||
textSelectable: false,
|
||||
updateLibraryOnLaunch: false,
|
||||
downloadNewChapters: false,
|
||||
showHistoryTab: true,
|
||||
showUpdatesTab: true,
|
||||
showLabelsInNav: false,
|
||||
useWebViewForChapter: false,
|
||||
};
|
||||
|
||||
const settingsReducer = (state = initialState, action) => {
|
||||
@@ -102,9 +104,10 @@ const settingsReducer = (state = initialState, action) => {
|
||||
...state,
|
||||
theme: payload.val
|
||||
? {
|
||||
...themes[0],
|
||||
...colorsAmoled,
|
||||
colorAccent: state.theme.colorAccent,
|
||||
rippleColor: state.theme.rippleColor,
|
||||
colorButtonText: state.theme.colorButtonText,
|
||||
id: state.theme.id,
|
||||
name: state.theme.name,
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import settingsReducer from "./settings/settings.reducer";
|
||||
import libraryReducer from "./library/library.reducer";
|
||||
import sourceReducer from "./source/source.reducers";
|
||||
import novelReducer from "./novel/novel.reducer";
|
||||
import historyReducer from "./history/history.reducer";
|
||||
import updatesReducer from "./updates/updates.reducer";
|
||||
import trackerReducer from "./tracker/tracker.reducer";
|
||||
import preferenceReducer from "./preferences/preference.reducer";
|
||||
@@ -26,7 +25,6 @@ const persistedReducer = persistReducer(
|
||||
libraryReducer,
|
||||
sourceReducer,
|
||||
novelReducer,
|
||||
historyReducer,
|
||||
updatesReducer,
|
||||
trackerReducer,
|
||||
preferenceReducer,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { GET_UPDATES } from "./updates.types";
|
||||
|
||||
import store from "../store";
|
||||
import { GET_UPDATES, SET_LAST_UPDATE_TIME } from "./updates.types";
|
||||
|
||||
import { getUpdates } from "../../database/queries/UpdateQueries";
|
||||
import { updateAllNovels } from "../../services/updates";
|
||||
@@ -38,6 +36,8 @@ export const getUpdatesAction = () => async (dispatch) => {
|
||||
export const updateLibraryAction = () => async (dispatch, getState) => {
|
||||
showToast("Updating library");
|
||||
|
||||
dispatch({ type: SET_LAST_UPDATE_TIME, payload: Date.now() });
|
||||
|
||||
const { downloadNewChapters = false } = getState().settingsReducer;
|
||||
|
||||
await updateAllNovels({ downloadNewChapters });
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { LOAD_UPDATES, GET_UPDATES } from "./updates.types";
|
||||
import {
|
||||
LOAD_UPDATES,
|
||||
GET_UPDATES,
|
||||
SET_LAST_UPDATE_TIME,
|
||||
SHOW_LAST_UPDATE_TIME,
|
||||
} from "./updates.types";
|
||||
|
||||
const initialState = {
|
||||
updates: [],
|
||||
lastUpdateTime: null,
|
||||
showLastUpdateTime: true,
|
||||
loading: true,
|
||||
};
|
||||
|
||||
@@ -13,6 +20,16 @@ const updateReducer = (state = initialState, action) => {
|
||||
return { ...state, loading: true };
|
||||
case GET_UPDATES:
|
||||
return { ...state, updates: payload, loading: false };
|
||||
case SET_LAST_UPDATE_TIME:
|
||||
return {
|
||||
...state,
|
||||
lastUpdateTime: payload,
|
||||
};
|
||||
case SHOW_LAST_UPDATE_TIME:
|
||||
return {
|
||||
...state,
|
||||
showLastUpdateTime: payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export const GET_UPDATES = "GET_UPDATES";
|
||||
export const LOAD_UPDATES = "LOAD_UPDATES";
|
||||
export const SET_LAST_UPDATE_TIME = "SET_LAST_UPDATE_TIME";
|
||||
export const SHOW_LAST_UPDATE_TIME = "SHOW_LAST_UPDATE_TIME";
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
|
||||
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
import NovelCover from "../../components/NovelCover";
|
||||
import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
import ErrorView from "../../components/ErrorView";
|
||||
|
||||
import { useTheme, useSettings } from "../../hooks/reduxHooks";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
import { getDeviceOrientation } from "../../services/utils/helpers";
|
||||
import { scrapeSearchResults } from "./discover/MyAnimeListScraper";
|
||||
|
||||
const BrowseMalScreen = ({ navigation, route }) => {
|
||||
const theme = useTheme();
|
||||
const settings = useSettings();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [novels, setNovels] = useState();
|
||||
const [error, setError] = useState();
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const baseUrl = "https://api.jikan.moe/v3/";
|
||||
const malUrl = "https://myanimelist.net/topmanga.php?type=lightnovels";
|
||||
|
||||
const getNovels = async () => {
|
||||
try {
|
||||
const url = `${baseUrl}top/manga/1/novels`;
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
|
||||
setNovels(data.top);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
setNovels([]);
|
||||
setLoading(false);
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const clearSearchbar = () => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setSearchText("");
|
||||
};
|
||||
|
||||
const getSearchResults = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const data = await scrapeSearchResults(searchText);
|
||||
|
||||
setNovels(data);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
setNovels([]);
|
||||
setLoading(false);
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getNovels();
|
||||
}, []);
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<NovelCover
|
||||
item={{ novelName: item.title, novelCover: item.image_url }}
|
||||
onPress={() =>
|
||||
navigation.navigate("GlobalSearch", {
|
||||
novelName: item.title,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
const { displayMode, novelsPerRow } = settings;
|
||||
|
||||
const orientation = getDeviceOrientation();
|
||||
|
||||
const getNovelsPerRow = () => {
|
||||
if (displayMode === 2) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (orientation === "landscape") {
|
||||
return 6;
|
||||
} else {
|
||||
return novelsPerRow;
|
||||
}
|
||||
};
|
||||
|
||||
const ListEmptyComponent = () => (
|
||||
<ErrorView
|
||||
error={error || "No results found"}
|
||||
onRetry={() => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setError();
|
||||
}}
|
||||
openWebView={() => WebBrowser.openBrowserAsync(topNovelsUrl)}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<Searchbar
|
||||
theme={theme}
|
||||
placeholder="Search MyAnimeList"
|
||||
backAction="arrow-left"
|
||||
onBackAction={() => navigation.goBack()}
|
||||
searchText={searchText}
|
||||
onChangeText={(text) => setSearchText(text)}
|
||||
onSubmitEditing={getSearchResults}
|
||||
clearSearchbar={clearSearchbar}
|
||||
actions={[
|
||||
{
|
||||
icon: "earth",
|
||||
onPress: () => WebBrowser.openBrowserAsync(malUrl),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator size={60} color={theme.colorAccent} />
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.novelsContainer}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={getNovelsPerRow()}
|
||||
data={novels}
|
||||
keyExtractor={(item) => item.mal_id.toString()}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowseMalScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
novelsContainer: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { FlatList, Pressable, Text, View } from "react-native";
|
||||
|
||||
import { Switch } from "react-native-paper";
|
||||
@@ -6,7 +6,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { ListSubHeader } from "../../components/List";
|
||||
import { List } from "../../components/List";
|
||||
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import { setAppSettings } from "../../redux/settings/settings.actions";
|
||||
@@ -18,10 +18,20 @@ import {
|
||||
const BrowseSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { filters = [], showMyAnimeList = true } = useSelector(
|
||||
(state) => state.sourceReducer
|
||||
);
|
||||
const languages = ["English", "Spanish", "Japanese", "Chinese", "Arabic"];
|
||||
const languages = [
|
||||
"English",
|
||||
"Spanish",
|
||||
"Japanese",
|
||||
"Chinese",
|
||||
"Arabic",
|
||||
"Indonesian",
|
||||
"Russian",
|
||||
];
|
||||
|
||||
const { searchAllSources = false } = useSettings();
|
||||
|
||||
const renderItem = ({ item }) => {
|
||||
@@ -50,68 +60,83 @@ const BrowseSettings = ({ navigation }) => {
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar onBackAction={navigation.goBack} title="Sources" />
|
||||
<View>
|
||||
<ListSubHeader theme={theme}>Global Search</ListSubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"searchAllSources",
|
||||
!searchAllSources
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
Search all sources
|
||||
</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={searchAllSources}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"searchAllSources",
|
||||
!searchAllSources
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<ListSubHeader theme={theme}>Discover</ListSubHeader>
|
||||
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() => dispatch(enableDiscover("showMyAnimeList"))}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
MyAnimeList
|
||||
</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={showMyAnimeList}
|
||||
onValueChange={() =>
|
||||
dispatch(enableDiscover("showMyAnimeList"))
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<ListSubHeader theme={theme}>Languages</ListSubHeader>
|
||||
<FlatList
|
||||
data={languages}
|
||||
keyExtractor={(item) => item}
|
||||
renderItem={renderItem}
|
||||
ListHeaderComponent={
|
||||
<View>
|
||||
<List.SubHeader theme={theme}>
|
||||
Global Search
|
||||
</List.SubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"searchAllSources",
|
||||
!searchAllSources
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
Search all sources
|
||||
</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={searchAllSources}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"searchAllSources",
|
||||
!searchAllSources
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>
|
||||
Discover
|
||||
</List.SubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(enableDiscover("showMyAnimeList"))
|
||||
}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
MyAnimeList
|
||||
</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={showMyAnimeList}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
enableDiscover("showMyAnimeList")
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>
|
||||
Languages
|
||||
</List.SubHeader>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
import { Searchbar } from "../../../components/Searchbar/Searchbar";
|
||||
import ErrorView from "../../../components/ErrorView";
|
||||
import { ErrorView } from "../../../components/ErrorView/ErrorView";
|
||||
|
||||
import { useTheme, useSettings } from "../../../hooks/reduxHooks";
|
||||
import { showToast } from "../../../hooks/showToast";
|
||||
@@ -106,13 +106,18 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
|
||||
const ListEmptyComponent = () => (
|
||||
<ErrorView
|
||||
error={error || "No results found"}
|
||||
onRetry={() => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setError();
|
||||
}}
|
||||
openWebView={() => WebBrowser.openBrowserAsync(topNovelsUrl)}
|
||||
errorName={error || "No results found"}
|
||||
actions={[
|
||||
{
|
||||
name: "Retry",
|
||||
onPress: () => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setError();
|
||||
},
|
||||
icon: "reload",
|
||||
},
|
||||
]}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -74,9 +74,6 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
setProgress(
|
||||
(progress) => progress + 1 / globalSearchSources.length
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(
|
||||
globalSearchSources[i].sourceName + ": " + error.message
|
||||
@@ -92,6 +89,9 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
setProgress(
|
||||
(progress) => progress + 1 / globalSearchSources.length
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
import { StyleSheet, View, Text, Image, Pressable } from "react-native";
|
||||
|
||||
const GlobalSearchNovelCover = ({
|
||||
novel,
|
||||
@@ -13,18 +11,21 @@ const GlobalSearchNovelCover = ({
|
||||
const { novelName, novelCover } = novel;
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<TouchableRipple
|
||||
borderless
|
||||
centered
|
||||
rippleColor={theme.rippleColor}
|
||||
<View style={{ flex: 1, borderRadius: 6, overflow: "hidden" }}>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.novelContainer}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{ uri: novelCover }}
|
||||
source={{
|
||||
uri:
|
||||
novelCover && !novelCover.startsWith("/")
|
||||
? novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
style={[
|
||||
styles.novelCover,
|
||||
inLibrary && { opacity: 0.5 },
|
||||
@@ -41,7 +42,7 @@ const GlobalSearchNovelCover = ({
|
||||
{novelName}
|
||||
</Text>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,11 +9,7 @@ const GlobalSearchNovelList = ({ data, theme, library, navigation }) => {
|
||||
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
|
||||
);
|
||||
|
||||
const navigateToNovel = (item) =>
|
||||
navigation.navigate("Novel", {
|
||||
...item,
|
||||
sourceId: item.sourceId,
|
||||
});
|
||||
const navigateToNovel = (item) => navigation.push("Novel", item);
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<GlobalSearchNovelCover
|
||||
|
||||
@@ -61,9 +61,6 @@ const MigrationNovels = ({ navigation, route }) => {
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
setProgress(
|
||||
(progress) => progress + 1 / migrationSources.length
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
|
||||
@@ -78,6 +75,7 @@ const MigrationNovels = ({ navigation, route }) => {
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
setProgress((progress) => progress + 1 / migrationSources.length);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+213
-107
@@ -5,15 +5,16 @@ import {
|
||||
Text,
|
||||
StatusBar,
|
||||
ScrollView,
|
||||
Pressable,
|
||||
TouchableWithoutFeedback,
|
||||
} from "react-native";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { IconButton, Portal } from "react-native-paper";
|
||||
import {
|
||||
hideNavigationBar,
|
||||
showNavigationBar,
|
||||
} from "react-native-navigation-bar-color";
|
||||
import WebView from "react-native-webview";
|
||||
|
||||
import {
|
||||
getChapterFromDB,
|
||||
@@ -30,9 +31,7 @@ import {
|
||||
useTrackingStatus,
|
||||
} from "../../hooks/reduxHooks";
|
||||
import { updateChaptersRead } from "../../redux/tracker/tracker.actions";
|
||||
import { insertHistoryAction } from "../../redux/history/history.actions";
|
||||
import {
|
||||
errorTextColor,
|
||||
readerBackground,
|
||||
readerLineHeight,
|
||||
readerTextColor,
|
||||
@@ -48,8 +47,12 @@ import EmptyView from "../../components/EmptyView";
|
||||
import ChapterFooter from "./components/ChapterFooter";
|
||||
import VerticalScrollbar from "./components/VerticalScrollbar";
|
||||
import GestureRecognizer from "react-native-swipe-gestures";
|
||||
import TextToComponents from "./TextToComponent";
|
||||
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
|
||||
import { insertHistory } from "../../database/queries/HistoryQueries";
|
||||
import { SET_LAST_READ } from "../../redux/preferences/preference.types";
|
||||
import { htmlToText } from "../../sources/helpers/htmlToText";
|
||||
import { setAppSettings } from "../../redux/settings/settings.actions";
|
||||
import { cleanHtml } from "../../sources/helpers/cleanHtml";
|
||||
|
||||
const Chapter = ({ route, navigation }) => {
|
||||
const {
|
||||
@@ -69,13 +72,13 @@ const Chapter = ({ route, navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const reader = useReaderSettings();
|
||||
const showScrollPercentage = useSelector(
|
||||
(state) => state.settingsReducer.showScrollPercentage
|
||||
);
|
||||
|
||||
const {
|
||||
showScrollPercentage = true,
|
||||
swipeGestures = true,
|
||||
incognitoMode = false,
|
||||
textSelectable = false,
|
||||
useWebViewForChapter = false,
|
||||
} = useSettings();
|
||||
|
||||
const [hidden, setHidden] = useState(true);
|
||||
@@ -111,6 +114,7 @@ const Chapter = ({ route, navigation }) => {
|
||||
}
|
||||
} else {
|
||||
const res = await fetchChapter(sourceId, novelUrl, chapterUrl);
|
||||
|
||||
setChapter(res);
|
||||
}
|
||||
|
||||
@@ -135,7 +139,13 @@ const Chapter = ({ route, navigation }) => {
|
||||
|
||||
useEffect(() => {
|
||||
getChapter(chapterId);
|
||||
!incognitoMode && dispatch(insertHistoryAction(novelId, chapterId));
|
||||
if (!incognitoMode) {
|
||||
insertHistory(novelId, chapterId);
|
||||
dispatch({
|
||||
type: SET_LAST_READ,
|
||||
payload: { novelId, chapterId },
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
StatusBar.setHidden(false);
|
||||
showNavigationBar();
|
||||
@@ -263,13 +273,16 @@ const Chapter = ({ route, navigation }) => {
|
||||
})
|
||||
: showToast("There's no next chapter");
|
||||
|
||||
const onPressLink = async (link) => {
|
||||
setLoading(true);
|
||||
const res = await fetchChapter(sourceId, novelUrl, link);
|
||||
setChapter(res);
|
||||
setLoading(false);
|
||||
const enableSwipeGestures = () => {
|
||||
dispatch(setAppSettings("swipeGestures", !swipeGestures));
|
||||
showToast(
|
||||
swipeGestures ? "Swipe gestures disabled" : "Swipe gestured enabled"
|
||||
);
|
||||
};
|
||||
|
||||
const enableWebView = () =>
|
||||
dispatch(setAppSettings("useWebViewForChapter", !useWebViewForChapter));
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
@@ -283,107 +296,204 @@ const Chapter = ({ route, navigation }) => {
|
||||
navigation={navigation}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
<ScrollView
|
||||
ref={(ref) => (scrollViewRef.current = ref)}
|
||||
contentContainerStyle={[
|
||||
styles.screenContainer,
|
||||
{ backgroundColor: readerBackground(reader.theme) },
|
||||
]}
|
||||
onScroll={onScroll}
|
||||
onContentSizeChange={(x, y) => setContentSize(y)}
|
||||
showsVerticalScrollIndicator={false}
|
||||
<GestureRecognizer
|
||||
onSwipeRight={swipeGestures && navigateToPrevChapter}
|
||||
onSwipeLeft={swipeGestures && navigateToNextChapter}
|
||||
config={config}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{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
|
||||
<ScrollView
|
||||
ref={(ref) => (scrollViewRef.current = ref)}
|
||||
contentContainerStyle={[
|
||||
styles.screenContainer,
|
||||
{ backgroundColor: readerBackground(reader.theme) },
|
||||
]}
|
||||
onScroll={onScroll}
|
||||
onContentSizeChange={(x, y) => setContentSize(y)}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{error ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<EmptyView
|
||||
icon="Σ(ಠ_ಠ)"
|
||||
description={error}
|
||||
style={{
|
||||
color: errorTextColor(reader.theme),
|
||||
color:
|
||||
reader.textColor ||
|
||||
readerTextColor(reader.theme),
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
</EmptyView>
|
||||
</View>
|
||||
) : loading ? (
|
||||
<LoadingScreen theme={theme} />
|
||||
) : (
|
||||
<GestureRecognizer
|
||||
onSwipeRight={
|
||||
swipeGestures && navigateToPrevChapter
|
||||
}
|
||||
onSwipeLeft={swipeGestures && navigateToNextChapter}
|
||||
config={config}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingVertical: 16,
|
||||
paddingBottom: 32,
|
||||
paddingHorizontal: `${reader.padding}%`,
|
||||
}}
|
||||
onLayout={scrollToSavedProgress}
|
||||
<IconButton
|
||||
icon="reload"
|
||||
size={25}
|
||||
style={{ margin: 0, marginTop: 16 }}
|
||||
color={
|
||||
reader.textColor ||
|
||||
readerTextColor(reader.theme)
|
||||
}
|
||||
onPress={() => {
|
||||
getChapter(chapterId);
|
||||
setLoading(true);
|
||||
setError();
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color:
|
||||
reader.textColor ||
|
||||
readerTextColor(reader.theme),
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
</EmptyView>
|
||||
</View>
|
||||
) : loading ? (
|
||||
<LoadingScreen theme={theme} />
|
||||
) : (
|
||||
<TouchableWithoutFeedback
|
||||
style={{ flex: 1 }}
|
||||
onPress={hideHeader}
|
||||
onLayout={scrollToSavedProgress}
|
||||
>
|
||||
<TextToComponents
|
||||
text={chapter.chapterText}
|
||||
textStyle={readerStyles}
|
||||
textSize={reader.textSize}
|
||||
textColor={
|
||||
reader.textColor ||
|
||||
readerTextColor(reader.theme)
|
||||
}
|
||||
textSelectable={textSelectable}
|
||||
onPressLink={onPressLink}
|
||||
theme={theme}
|
||||
/>
|
||||
</Pressable>
|
||||
</GestureRecognizer>
|
||||
)}
|
||||
<Portal>
|
||||
<ReaderSheet
|
||||
theme={theme}
|
||||
reader={reader}
|
||||
dispatch={dispatch}
|
||||
navigation={navigation}
|
||||
bottomSheetRef={readerSheetRef}
|
||||
selectText={textSelectable}
|
||||
showScrollPercentage={showScrollPercentage}
|
||||
/>
|
||||
</Portal>
|
||||
</ScrollView>
|
||||
<VerticalScrollbar
|
||||
theme={theme}
|
||||
hide={hidden}
|
||||
setLoading={setLoading}
|
||||
contentSize={contentSize}
|
||||
scrollViewRef={scrollViewRef}
|
||||
scrollPercentage={scrollPercentage}
|
||||
setScrollPercentage={setScrollPercentage}
|
||||
/>
|
||||
{useWebViewForChapter ? (
|
||||
<WebView
|
||||
style={{
|
||||
backgroundColor: readerBackground(
|
||||
reader.theme
|
||||
),
|
||||
paddingBottom: 24,
|
||||
}}
|
||||
originWhitelist={["*"]}
|
||||
scalesPageToFit={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onScroll={onScroll}
|
||||
onNavigationStateChange={async ({
|
||||
url,
|
||||
}) => {
|
||||
if (
|
||||
(sourceId === 50 ||
|
||||
sourceId == 62) &&
|
||||
url !== "about:blank"
|
||||
) {
|
||||
setLoading(true);
|
||||
const res = await fetchChapter(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
url
|
||||
);
|
||||
setChapter(res);
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
source={{
|
||||
html: `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<style>
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
padding-top: ${StatusBar.currentHeight};
|
||||
}
|
||||
body {
|
||||
padding-left: ${reader.padding}%;
|
||||
padding-right: ${reader.padding}%;
|
||||
font-size: ${reader.textSize}px;
|
||||
color: ${reader.textColor};
|
||||
|
||||
text-align: ${reader.textAlign};
|
||||
line-height: ${reader.lineHeight};
|
||||
font-family: ${reader.fontFamily};
|
||||
}
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
a {
|
||||
color: ${theme.colorAccent};
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
${reader.customCSS}
|
||||
@font-face {
|
||||
font-family: ${reader.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${
|
||||
reader.fontFamily
|
||||
}.ttf");
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${cleanHtml(chapter.chapterText)}
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingVertical: 16,
|
||||
paddingBottom: 32,
|
||||
paddingHorizontal: `${reader.padding}%`,
|
||||
paddingTop: StatusBar.currentHeight,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={readerStyles}
|
||||
selectable={textSelectable}
|
||||
>
|
||||
{htmlToText(chapter.chapterText)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
</ScrollView>
|
||||
</GestureRecognizer>
|
||||
<Portal>
|
||||
<ReaderSheet
|
||||
theme={theme}
|
||||
reader={reader}
|
||||
dispatch={dispatch}
|
||||
navigation={navigation}
|
||||
bottomSheetRef={readerSheetRef}
|
||||
selectText={textSelectable}
|
||||
useWebViewForChapter={useWebViewForChapter}
|
||||
showScrollPercentage={showScrollPercentage}
|
||||
/>
|
||||
</Portal>
|
||||
{!useWebViewForChapter && (
|
||||
<VerticalScrollbar
|
||||
theme={theme}
|
||||
hide={hidden}
|
||||
setLoading={setLoading}
|
||||
contentSize={contentSize}
|
||||
scrollViewRef={scrollViewRef}
|
||||
scrollPercentage={scrollPercentage}
|
||||
setScrollPercentage={setScrollPercentage}
|
||||
/>
|
||||
)}
|
||||
<ChapterFooter
|
||||
theme={theme}
|
||||
swipeGestures={swipeGestures}
|
||||
enableSwipeGestures={enableSwipeGestures}
|
||||
enableWebView={enableWebView}
|
||||
dispatch={dispatch}
|
||||
nextChapter={nextChapter}
|
||||
prevChapter={prevChapter}
|
||||
hide={hidden}
|
||||
useWebViewForChapter={useWebViewForChapter}
|
||||
navigateToNextChapter={navigateToNextChapter}
|
||||
navigateToPrevChapter={navigateToPrevChapter}
|
||||
readerSheetRef={readerSheetRef}
|
||||
@@ -410,10 +520,7 @@ const Chapter = ({ route, navigation }) => {
|
||||
export default Chapter;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screenContainer: {
|
||||
flexGrow: 1,
|
||||
paddingTop: StatusBar.currentHeight,
|
||||
},
|
||||
screenContainer: { flexGrow: 1 },
|
||||
scrollPercentage: {
|
||||
width: "100%",
|
||||
position: "absolute",
|
||||
@@ -422,7 +529,6 @@ const styles = StyleSheet.create({
|
||||
textAlign: "center",
|
||||
textAlignVertical: "center",
|
||||
alignSelf: "center",
|
||||
fontFamily: "noto-sans",
|
||||
fontWeight: "bold",
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
import React, { memo, useCallback, useEffect } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import FitImage from "react-native-fit-image";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
const getHeadingSize = (size) => {
|
||||
const headings = {
|
||||
h1: 6,
|
||||
h2: 5,
|
||||
h3: 4,
|
||||
h4: 3,
|
||||
h5: 2,
|
||||
h6: 1,
|
||||
strong: 0,
|
||||
};
|
||||
|
||||
return headings[size];
|
||||
};
|
||||
|
||||
const Header = ({ size, text, textStyle, textSelectable, textSize }) => (
|
||||
<Text
|
||||
style={[
|
||||
textStyle,
|
||||
{
|
||||
fontSize: textSize + getHeadingSize(size),
|
||||
fontWeight: "bold",
|
||||
},
|
||||
]}
|
||||
selectable={textSelectable}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const DefaultText = ({ text, textStyle, textSelectable }) => (
|
||||
<Text style={textStyle} selectable={textSelectable}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const ItalicText = ({ text, textStyle, textSelectable }) => (
|
||||
<Text
|
||||
style={[textStyle, { fontStyle: "italic" }]}
|
||||
selectable={textSelectable}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const HorizontalRule = ({ textColor }) => (
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
flex: 1,
|
||||
borderBottomColor: textColor,
|
||||
marginVertical: 16,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const Link = ({
|
||||
link,
|
||||
text,
|
||||
textStyle,
|
||||
textSelectable,
|
||||
onPressLink,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
textStyle,
|
||||
{
|
||||
alignSelf: "flex-start",
|
||||
color: theme.colorAccent,
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
]}
|
||||
selectable={textSelectable}
|
||||
onPress={() => onPressLink(link)}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const TextToComponents = ({
|
||||
text,
|
||||
textStyle,
|
||||
textSelectable,
|
||||
textSize,
|
||||
textColor,
|
||||
onPressLink,
|
||||
theme,
|
||||
}) => {
|
||||
try {
|
||||
regex = /(\[(?:\n|.)*?\]\(.*?\))/;
|
||||
|
||||
let parseText = text.trim().split(regex);
|
||||
return parseText.map((part, index) => {
|
||||
if (part) {
|
||||
let match = part.match(/\[((?:\n|.)*?)\]\((.*?)\)/);
|
||||
|
||||
if (match) {
|
||||
tag = match[1];
|
||||
text = match[2];
|
||||
|
||||
if (tag.startsWith("Anchor:")) {
|
||||
link = tag.replace("Anchor: ", "");
|
||||
tag = "a";
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
case "h1":
|
||||
return (
|
||||
<Header
|
||||
size="h1"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h2":
|
||||
return (
|
||||
<Header
|
||||
size="h2"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h3":
|
||||
return (
|
||||
<Header
|
||||
size="h3"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h4":
|
||||
return (
|
||||
<Header
|
||||
size="h4"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h5":
|
||||
return (
|
||||
<Header
|
||||
size="h5"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h6":
|
||||
return (
|
||||
<Header
|
||||
size="h6"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "strong":
|
||||
return (
|
||||
<Header
|
||||
size="strong"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "em":
|
||||
return (
|
||||
<ItalicText
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
|
||||
case "a":
|
||||
return (
|
||||
<Link
|
||||
link={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
text={link}
|
||||
onPressLink={onPressLink}
|
||||
theme={theme}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
case "img":
|
||||
return (
|
||||
<FitImage source={{ uri: text }} key={index} />
|
||||
);
|
||||
case "hr":
|
||||
return (
|
||||
<HorizontalRule
|
||||
key={index}
|
||||
textColor={textColor}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<DefaultText
|
||||
text={part}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<DefaultText
|
||||
text={part}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return (
|
||||
<DefaultText
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(TextToComponents);
|
||||
@@ -1,11 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { Appbar } from "react-native-paper";
|
||||
import {
|
||||
getNextChapterFromDB,
|
||||
getPrevChapterFromDB,
|
||||
} from "../../../database/queries/ChapterQueries";
|
||||
import { bookmarkChapterAction } from "../../../redux/novel/novel.actions";
|
||||
|
||||
const ChapterAppbar = ({
|
||||
@@ -27,7 +23,7 @@ const ChapterAppbar = ({
|
||||
style={{
|
||||
position: "absolute",
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
backgroundColor: "rgba(0,0,0,0.7)",
|
||||
width: "100%",
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Dimensions, Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, View } from "react-native";
|
||||
import { IconButton } from "react-native-paper";
|
||||
import {
|
||||
getNextChapterFromDB,
|
||||
getPrevChapterFromDB,
|
||||
} from "../../../database/queries/ChapterQueries";
|
||||
import { useSettings } from "../../../hooks/reduxHooks";
|
||||
import { showToast } from "../../../hooks/showToast";
|
||||
import { setAppSettings } from "../../../redux/settings/settings.actions";
|
||||
|
||||
const ChapterFooter = ({
|
||||
hide,
|
||||
theme,
|
||||
swipeGestures,
|
||||
dispatch,
|
||||
readerSheetRef,
|
||||
scrollViewRef,
|
||||
navigateToNextChapter,
|
||||
navigateToPrevChapter,
|
||||
nextChapter,
|
||||
prevChapter,
|
||||
useWebViewForChapter,
|
||||
enableSwipeGestures,
|
||||
enableWebView,
|
||||
}) => {
|
||||
const rippleConfig = {
|
||||
color: theme.rippleColor,
|
||||
@@ -27,13 +22,6 @@ const ChapterFooter = ({
|
||||
radius: 50,
|
||||
};
|
||||
|
||||
const enableSwipeGestures = () => {
|
||||
dispatch(setAppSettings("swipeGestures", !swipeGestures));
|
||||
showToast(
|
||||
swipeGestures ? "Swipe gestures disabled" : "Swipe gestured enabled"
|
||||
);
|
||||
};
|
||||
|
||||
if (hide) {
|
||||
return null;
|
||||
} else {
|
||||
@@ -44,7 +32,7 @@ const ChapterFooter = ({
|
||||
zIndex: 2,
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
backgroundColor: "rgba(0,0,0,0.7)",
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
@@ -72,17 +60,31 @@ const ChapterFooter = ({
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
{!useWebViewForChapter && (
|
||||
<Pressable
|
||||
android_ripple={rippleConfig}
|
||||
style={styles.buttonStyles}
|
||||
onPress={() => scrollViewRef.current.scrollTo({})}
|
||||
>
|
||||
<IconButton
|
||||
icon="format-vertical-align-top"
|
||||
size={26}
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
{/* <Pressable
|
||||
android_ripple={rippleConfig}
|
||||
style={styles.buttonStyles}
|
||||
onPress={() => scrollViewRef.current.scrollTo({})}
|
||||
onPress={enableWebView}
|
||||
>
|
||||
<IconButton
|
||||
icon="format-vertical-align-top"
|
||||
icon="language-html5"
|
||||
disabled={!useWebViewForChapter}
|
||||
size={26}
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
</Pressable>
|
||||
</Pressable> */}
|
||||
<Pressable
|
||||
android_ripple={rippleConfig}
|
||||
style={styles.buttonStyles}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text, Dimensions, Pressable } from "react-native";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
Dimensions,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
useWindowDimensions,
|
||||
Animated,
|
||||
} from "react-native";
|
||||
|
||||
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
|
||||
import Slider from "@react-native-community/slider";
|
||||
import { ToggleButton, IconButton, Menu, Switch } from "react-native-paper";
|
||||
import { IconButton, Menu, Switch } from "react-native-paper";
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
|
||||
import BottomSheetHandle from "../../../components/BottomSheetHandle";
|
||||
@@ -14,62 +24,72 @@ import {
|
||||
setAppSettings,
|
||||
setReaderSettings,
|
||||
} from "../../../redux/settings/settings.actions";
|
||||
import {
|
||||
ToggleButton,
|
||||
ToggleColorButton,
|
||||
} from "../../../components/Common/ToggleButton";
|
||||
|
||||
const ReaderSettingTitle = ({ title }) => (
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
);
|
||||
|
||||
const ReaderSheet = ({
|
||||
theme,
|
||||
reader,
|
||||
dispatch,
|
||||
navigation,
|
||||
bottomSheetRef,
|
||||
selectText,
|
||||
useWebViewForChapter,
|
||||
showScrollPercentage,
|
||||
}) => {
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
|
||||
const [fontMenu, setFontMenu] = useState(false);
|
||||
const openFontMenu = () => setFontMenu(true);
|
||||
const closeFontMenu = () => setFontMenu(false);
|
||||
|
||||
const ReaderSettingTitle = ({ title }) => (
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
);
|
||||
const presetThemes = [
|
||||
{ value: 2, backgroundColor: "#f5f5fa", textColor: "#111111" },
|
||||
{ value: 3, backgroundColor: "#F7DFC6", textColor: "#593100" },
|
||||
{ value: 6, backgroundColor: "#dce5e2", textColor: "#000000" },
|
||||
{ value: 4, backgroundColor: "#292832", textColor: "#CCCCCC" },
|
||||
{
|
||||
value: 1,
|
||||
backgroundColor: "#000000",
|
||||
textColor: "rgba(255,255,255,0.7)",
|
||||
},
|
||||
];
|
||||
|
||||
const textColor = {
|
||||
1: "rgba(255,255,255,0.7)",
|
||||
2: "#111111",
|
||||
3: "#593100",
|
||||
4: "#CCCCCC",
|
||||
5: "#CCCCCC",
|
||||
};
|
||||
const textAlignments = [
|
||||
{ value: "left", icon: "format-align-left" },
|
||||
{ value: "center", icon: "format-align-center" },
|
||||
{ value: "justify", icon: "format-align-justify" },
|
||||
{ value: "right", icon: "format-align-right" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 470, bottom: 0 }}
|
||||
snappingPoints={[0, 470]}
|
||||
showBackdrop={true}
|
||||
backdropOpacity={0}
|
||||
>
|
||||
<View style={styles.contentContainer}>
|
||||
<BottomSheetHandle theme={theme} />
|
||||
{/* <IconButton
|
||||
icon="cog-outline"
|
||||
color="#FFFFFF"
|
||||
size={24}
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "ReaderSettings",
|
||||
})
|
||||
}
|
||||
const FirstRoute = useCallback(
|
||||
() => (
|
||||
<View style={{ flex: 1, paddingVertical: 16 }}>
|
||||
<View
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
/> */}
|
||||
<View style={styles.readerSettingsContainer}>
|
||||
<ReaderSettingTitle title="Text Size" />
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Text size
|
||||
</Text>
|
||||
<Slider
|
||||
style={{
|
||||
width: Dimensions.get("window").width,
|
||||
flex: 1,
|
||||
height: 40,
|
||||
}}
|
||||
value={reader.textSize}
|
||||
@@ -83,177 +103,83 @@ const ReaderSheet = ({
|
||||
dispatch(setReaderSettings("textSize", value))
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ paddingLeft: 16, paddingRight: 8 }}>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Reader Theme" />
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Color
|
||||
</Text>
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<ScrollView horizontal={true}>
|
||||
{presetThemes.map((item, index) => (
|
||||
<ToggleColorButton
|
||||
key={index}
|
||||
selected={reader.theme === item.value}
|
||||
backgroundColor={item.backgroundColor}
|
||||
textColor={item.textColor}
|
||||
theme={theme}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"theme",
|
||||
item.value
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textColor",
|
||||
item.textColor
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Text align
|
||||
</Text>
|
||||
<Row>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) => {
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"theme",
|
||||
value ?? reader.theme
|
||||
{textAlignments.map((item) => (
|
||||
<ToggleButton
|
||||
key={item.value}
|
||||
selected={item.value === reader.textAlign}
|
||||
icon={item.icon}
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textAlign",
|
||||
item.value
|
||||
)
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textColor",
|
||||
textColor[value] ?? reader.textColor
|
||||
)
|
||||
);
|
||||
}}
|
||||
value={reader.theme}
|
||||
>
|
||||
<ToggleButton
|
||||
icon={reader.theme === 1 && "check"}
|
||||
color="rgba(255,255,255,0.7)"
|
||||
value={1}
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
borderBottomRightRadius: 50,
|
||||
}}
|
||||
}
|
||||
/>
|
||||
{/* <ToggleButton
|
||||
icon={reader.theme === 5 && "check"}
|
||||
color="#CCCCCC"
|
||||
value={5}
|
||||
style={{
|
||||
backgroundColor: "#2B2C30",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
borderBottomRightRadius: 50,
|
||||
}}
|
||||
/> */}
|
||||
<ToggleButton
|
||||
icon={reader.theme === 4 && "check"}
|
||||
color="#CCCCCC"
|
||||
value={4}
|
||||
style={{
|
||||
backgroundColor: "#292832",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
borderBottomRightRadius: 50,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon={reader.theme === 2 && "check"}
|
||||
color="#111111"
|
||||
value={2}
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
borderRadius: 50,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon={reader.theme === 3 && "check"}
|
||||
color="#593100"
|
||||
value={3}
|
||||
style={{
|
||||
backgroundColor: "#F7DFC6",
|
||||
marginHorizontal: 10,
|
||||
borderRadius: 50,
|
||||
borderTopLeftRadius: 50,
|
||||
borderBottomLeftRadius: 50,
|
||||
}}
|
||||
/>
|
||||
</ToggleButton.Row>
|
||||
))}
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<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,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Padding" />
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Padding
|
||||
</Text>
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
@@ -298,12 +224,12 @@ const ReaderSheet = ({
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Line Height" />
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Line height
|
||||
</Text>
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
@@ -347,75 +273,138 @@ const ReaderSheet = ({
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Pressable
|
||||
<Row
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
marginVertical: 6,
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
onPress={openFontMenu}
|
||||
>
|
||||
<ReaderSettingTitle title="Font Style" />
|
||||
<Menu
|
||||
visible={fontMenu}
|
||||
onDismiss={closeFontMenu}
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
anchor={
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontFamily: reader.fontFamily,
|
||||
paddingHorizontal: 16,
|
||||
}}
|
||||
>
|
||||
{fonts.find(
|
||||
(font) =>
|
||||
font.fontFamily ===
|
||||
reader.fontFamily
|
||||
).name || "Default"}
|
||||
</Text>
|
||||
}
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
marginRight: 16,
|
||||
}}
|
||||
>
|
||||
Font style
|
||||
</Text>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{ alignItems: "center" }}
|
||||
>
|
||||
{fonts.map((font) => (
|
||||
<Menu.Item
|
||||
key={font.fontFamily}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"fontFamily",
|
||||
font.fontFamily
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
reader.fontFamily ===
|
||||
font.fontFamily && {
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"fontFamily",
|
||||
font.fontFamily
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
icon={
|
||||
font.fontFamily === reader.fontFamily &&
|
||||
"check"
|
||||
}
|
||||
title={font.name}
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontFamily: font.fontFamily,
|
||||
color:
|
||||
reader.fontFamily ===
|
||||
font.fontFamily
|
||||
? theme.colorAccent
|
||||
: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{font.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
</Menu>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
</ScrollView>
|
||||
</Row>
|
||||
</View>
|
||||
</View>
|
||||
),
|
||||
[reader]
|
||||
);
|
||||
|
||||
const SecondRoute = useCallback(
|
||||
() => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Pressable
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"useWebViewForChapter",
|
||||
!useWebViewForChapter
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
onPress={() =>
|
||||
>
|
||||
Use WebView
|
||||
</Text>
|
||||
<Switch
|
||||
value={useWebViewForChapter}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"useWebViewForChapter",
|
||||
!useWebViewForChapter
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showScrollPercentage",
|
||||
!showScrollPercentage
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
Show progress percentage
|
||||
</Text>
|
||||
<Switch
|
||||
value={showScrollPercentage}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showScrollPercentage",
|
||||
@@ -423,48 +412,26 @@ const ReaderSheet = ({
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<ReaderSettingTitle title="Show Progress Percentage" />
|
||||
<Switch
|
||||
value={showScrollPercentage}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showScrollPercentage",
|
||||
!showScrollPercentage
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
{!useWebViewForChapter && (
|
||||
<Pressable
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings("textSelectable", !selectText)
|
||||
)
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<ReaderSettingTitle title="Select Text " />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
(Justify does not work with selectable text)
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
Select text
|
||||
</Text>
|
||||
<Switch
|
||||
value={selectText}
|
||||
onValueChange={() =>
|
||||
@@ -478,7 +445,62 @@ const ReaderSheet = ({
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
),
|
||||
[useWebViewForChapter, selectText, showScrollPercentage]
|
||||
);
|
||||
|
||||
const renderScene = SceneMap({
|
||||
first: FirstRoute,
|
||||
second: SecondRoute,
|
||||
});
|
||||
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
const [routes] = useState([
|
||||
{ key: "first", title: "Display" },
|
||||
{ key: "second", title: "General" },
|
||||
]);
|
||||
|
||||
const renderTabBar = (props) => (
|
||||
<TabBar
|
||||
{...props}
|
||||
indicatorStyle={{ backgroundColor: theme.colorAccent }}
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
renderLabel={({ route, focused, color }) => (
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.textColorSecondary}
|
||||
activeColor={theme.colorAccent}
|
||||
pressColor={theme.rippleColor}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 400, bottom: 0 }}
|
||||
snappingPoints={[0, 400]}
|
||||
showBackdrop={true}
|
||||
backdropOpacity={0}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
@@ -502,4 +524,12 @@ const styles = StyleSheet.create({
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
switchStyle: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import WebView from "react-native-webview";
|
||||
|
||||
export const WebViewReader = ({ style, css, text, onScroll }) => (
|
||||
<WebView
|
||||
style={style}
|
||||
originWhitelist={["*"]}
|
||||
scalesPageToFit={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onScroll={onScroll}
|
||||
source={{
|
||||
html: `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<style>
|
||||
${css}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${text}
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -5,6 +5,7 @@ export const readerBackground = (val) => {
|
||||
3: "#F7DFC6",
|
||||
4: "#292832",
|
||||
5: "#2B2C30",
|
||||
6: "#dce5e2",
|
||||
};
|
||||
|
||||
return backgroundColor[val] || val;
|
||||
@@ -17,6 +18,7 @@ export const readerTextColor = (val) => {
|
||||
3: "#593100",
|
||||
4: "#CCCCCC",
|
||||
5: "#CCCCCC",
|
||||
6: "#000000",
|
||||
};
|
||||
|
||||
return textColor[val] || val;
|
||||
@@ -29,9 +31,3 @@ export const readerLineHeight = (fontSize, lineHeightMultiplier) => {
|
||||
|
||||
return lineHeight;
|
||||
};
|
||||
|
||||
export const errorTextColor = (val) => {
|
||||
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.54)";
|
||||
|
||||
return textColor;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
|
||||
import NovelCover from "../../components/NovelCover";
|
||||
import NovelList from "../../components/NovelList";
|
||||
import ErrorView from "../../components/ErrorView";
|
||||
import { ErrorView } from "../../components/ErrorView/ErrorView";
|
||||
|
||||
const Extension = ({ navigation, route }) => {
|
||||
const { sourceId, sourceName, url } = route.params;
|
||||
@@ -126,13 +126,18 @@ const Extension = ({ navigation, route }) => {
|
||||
|
||||
const listEmptyComponent = () => (
|
||||
<ErrorView
|
||||
error={error || "No results found"}
|
||||
onRetry={() => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setError();
|
||||
}}
|
||||
openWebView={() => WebBrowser.openBrowserAsync(url)}
|
||||
errorName={error || "No results found"}
|
||||
actions={[
|
||||
{
|
||||
name: "Retry",
|
||||
onPress: () => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setError();
|
||||
},
|
||||
icon: "reload",
|
||||
},
|
||||
]}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { StyleSheet, FlatList, ActivityIndicator, Text } from "react-native";
|
||||
|
||||
import moment from "moment";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
import HistoryItem from "./HistoryItem";
|
||||
@@ -10,18 +11,52 @@ import RemoveHistoryDialog from "./RemoveHistoryDialog";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
|
||||
import { getHistoryAction } from "../../redux/history/history.actions";
|
||||
import { dateFormat } from "../../services/utils/constants";
|
||||
import { useTheme } from "../../hooks/reduxHooks";
|
||||
import {
|
||||
deleteHistory,
|
||||
getHistoryFromDb,
|
||||
} from "../../database/queries/HistoryQueries";
|
||||
|
||||
const History = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { history, loading } = useSelector((state) => state.historyReducer);
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
|
||||
const [history, setHistory] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const getHistory = async () => {
|
||||
const history = await getHistoryFromDb();
|
||||
|
||||
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],
|
||||
};
|
||||
});
|
||||
|
||||
setHistory(groupedHistory);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Confirm Clear History Dialog
|
||||
*/
|
||||
@@ -29,9 +64,16 @@ const History = ({ navigation }) => {
|
||||
const showDialog = () => setVisible(true);
|
||||
const hideDialog = () => setVisible(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getHistoryAction());
|
||||
}, [getHistoryAction]);
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
getHistory();
|
||||
}, [visible])
|
||||
);
|
||||
|
||||
const deleteChapterHistory = (historyId) => {
|
||||
deleteHistory(historyId);
|
||||
getHistory();
|
||||
};
|
||||
|
||||
const renderHistoryCard = ({ item }) => (
|
||||
<HistoryItem
|
||||
@@ -39,6 +81,7 @@ const History = ({ navigation }) => {
|
||||
history={item}
|
||||
theme={theme}
|
||||
navigation={navigation}
|
||||
deleteHistory={deleteChapterHistory}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -89,10 +132,10 @@ const History = ({ navigation }) => {
|
||||
<FlatList
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
data={searchText ? searchResults : history}
|
||||
keyExtractor={(item, index) => item.date}
|
||||
keyExtractor={(item) => item.date}
|
||||
renderItem={({ item }) => (
|
||||
<FlatList
|
||||
keyExtractor={(item, index) => item.novelId.toString()}
|
||||
keyExtractor={(item) => item.novelId.toString()}
|
||||
data={item.novels}
|
||||
renderItem={renderHistoryCard}
|
||||
ListHeaderComponent={
|
||||
@@ -114,20 +157,22 @@ const History = ({ navigation }) => {
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={theme.colorAccent}
|
||||
style={{ margin: 16 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
ListEmptyComponent={
|
||||
<EmptyView
|
||||
icon="(˘・_・˘)"
|
||||
description="Nothing read recently"
|
||||
/>
|
||||
!loading && (
|
||||
<EmptyView
|
||||
icon="(˘・_・˘)"
|
||||
description="Nothing read recently"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<RemoveHistoryDialog
|
||||
dialogVisible={visible}
|
||||
hideDialog={hideDialog}
|
||||
dispatch={dispatch}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
|
||||
@@ -5,10 +5,15 @@ import moment from "moment";
|
||||
import { IconButton } from "react-native-paper";
|
||||
|
||||
import { parseChapterNumber } from "../../services/utils/helpers";
|
||||
import { deleteHistoryAction } from "../../redux/history/history.actions";
|
||||
import { setNovel } from "../../redux/novel/novel.actions";
|
||||
|
||||
const HistoryItem = ({ history, theme, dispatch, navigation }) => {
|
||||
const HistoryItem = ({
|
||||
history,
|
||||
theme,
|
||||
dispatch,
|
||||
navigation,
|
||||
deleteHistory,
|
||||
}) => {
|
||||
const {
|
||||
historyId,
|
||||
historyTimeRead,
|
||||
@@ -61,7 +66,12 @@ const HistoryItem = ({ history, theme, dispatch, navigation }) => {
|
||||
>
|
||||
<View>
|
||||
<Image
|
||||
source={{ uri: novelCover }}
|
||||
source={{
|
||||
uri:
|
||||
novelCover && !novelCover.startsWith("/")
|
||||
? novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
style={styles.historyItemCover}
|
||||
/>
|
||||
</View>
|
||||
@@ -97,7 +107,7 @@ const HistoryItem = ({ history, theme, dispatch, navigation }) => {
|
||||
icon="delete-outline"
|
||||
size={22}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={() => dispatch(deleteHistoryAction(historyId))}
|
||||
onPress={() => deleteHistory(historyId)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="play"
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
import { Button, Dialog, Portal } from "react-native-paper";
|
||||
import { clearAllHistoryAction } from "../../redux/history/history.actions";
|
||||
import { deleteAllHistory } from "../../database/queries/HistoryQueries";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
const RemoveHistoryDialog = ({
|
||||
dialogVisible,
|
||||
hideDialog,
|
||||
dispatch,
|
||||
theme,
|
||||
}) => {
|
||||
const RemoveHistoryDialog = ({ dialogVisible, hideDialog, theme }) => {
|
||||
const onPress = () => {
|
||||
dispatch(clearAllHistoryAction());
|
||||
deleteAllHistory();
|
||||
showToast("History deleted.");
|
||||
hideDialog();
|
||||
};
|
||||
|
||||
|
||||
+118
-28
@@ -5,7 +5,10 @@ import {
|
||||
View,
|
||||
RefreshControl,
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
} from "react-native";
|
||||
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
|
||||
@@ -21,8 +24,14 @@ import {
|
||||
import { updateLibraryAction } from "../../redux/updates/updates.actions";
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import { setNovel } from "../../redux/novel/novel.actions";
|
||||
import { Portal, TouchableRipple } from "react-native-paper";
|
||||
import { Portal } from "react-native-paper";
|
||||
import LibraryFilterSheet from "./components/LibraryFilterSheet";
|
||||
import { Actionbar } from "../../components/Actionbar/Actionbar";
|
||||
import {
|
||||
markAllChaptersRead,
|
||||
markAllChaptersUnread,
|
||||
} from "../../database/queries/ChapterQueries";
|
||||
import { unfollowNovel } from "../../database/queries/NovelQueries";
|
||||
|
||||
const LibraryScreen = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -72,18 +81,20 @@ const LibraryScreen = ({ navigation }) => {
|
||||
dispatch(searchLibraryAction(text, sort, filter));
|
||||
};
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() => {
|
||||
navigation.navigate("Novel", item);
|
||||
dispatch(setNovel(item));
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
const renderItem = ({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() => {
|
||||
navigation.navigate("Novel", item);
|
||||
dispatch(setNovel(item));
|
||||
}}
|
||||
theme={theme}
|
||||
onLongPress={selectNovel}
|
||||
isSelected={
|
||||
selectedNovels.indexOf(item.novelId) === -1 ? false : true
|
||||
}
|
||||
selectedNovels={selectedNovels}
|
||||
/>
|
||||
);
|
||||
|
||||
const refreshControl = () => (
|
||||
@@ -95,10 +106,6 @@ const LibraryScreen = ({ navigation }) => {
|
||||
/>
|
||||
);
|
||||
|
||||
// if (!loading) {
|
||||
// console.log(novels);
|
||||
// }
|
||||
|
||||
const listEmptyComponent = () =>
|
||||
searchText === "" && (
|
||||
<EmptyView
|
||||
@@ -107,6 +114,22 @@ const LibraryScreen = ({ navigation }) => {
|
||||
/>
|
||||
);
|
||||
|
||||
/**
|
||||
* Selected Novels
|
||||
*/
|
||||
|
||||
const [selectedNovels, setSelectedNovels] = useState([]);
|
||||
|
||||
const selectNovel = (novelId) => {
|
||||
if (selectedNovels.indexOf(novelId) === -1) {
|
||||
setSelectedNovels((prevState) => [...prevState, novelId]);
|
||||
} else {
|
||||
setSelectedNovels((prevState) =>
|
||||
prevState.filter((id) => id !== novelId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
@@ -116,13 +139,22 @@ const LibraryScreen = ({ navigation }) => {
|
||||
]}
|
||||
>
|
||||
<Searchbar
|
||||
placeholder={`Search Library ${
|
||||
showNumberOfNovels ? "(" + novels.length + ")" : ""
|
||||
}`}
|
||||
placeholder={
|
||||
selectedNovels.length === 0
|
||||
? `Search Library ${
|
||||
showNumberOfNovels
|
||||
? "(" + novels.length + ")"
|
||||
: ""
|
||||
}`
|
||||
: `${selectedNovels.length} selected`
|
||||
}
|
||||
searchText={searchText}
|
||||
clearSearchbar={clearSearchbar}
|
||||
onChangeText={onChangeText}
|
||||
backAction="magnify"
|
||||
backAction={selectedNovels.length > 0 ? "close" : "magnify"}
|
||||
onBackAction={() =>
|
||||
selectedNovels.length > 0 && setSelectedNovels([])
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
icon: "filter-variant",
|
||||
@@ -137,12 +169,28 @@ const LibraryScreen = ({ navigation }) => {
|
||||
{incognitoMode && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#424242",
|
||||
paddingVertical: 4,
|
||||
backgroundColor: theme.searchBarColor,
|
||||
paddingVertical: 6,
|
||||
alignItems: "center",
|
||||
marginVertical: 8,
|
||||
elevation: 2,
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#FFFFFF" }}>Incognito Mode</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="incognito"
|
||||
color={theme.textColorSecondary}
|
||||
size={18}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
marginLeft: 8,
|
||||
}}
|
||||
>
|
||||
Incognito Mode
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{loading ? (
|
||||
@@ -150,20 +198,19 @@ const LibraryScreen = ({ navigation }) => {
|
||||
) : (
|
||||
<>
|
||||
{searchText !== "" && (
|
||||
<TouchableRipple
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
navigation.navigate("GlobalSearch", {
|
||||
novelName: searchText,
|
||||
})
|
||||
}
|
||||
borderless
|
||||
rippleColor={theme.colorAccent}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
style={styles.emptySearch}
|
||||
>
|
||||
<Text style={{ color: theme.colorAccent }}>
|
||||
{`Search for "${searchText}" globally`}
|
||||
</Text>
|
||||
</TouchableRipple>
|
||||
</Pressable>
|
||||
)}
|
||||
<NovelList
|
||||
data={novels}
|
||||
@@ -171,6 +218,49 @@ const LibraryScreen = ({ navigation }) => {
|
||||
refreshControl={refreshControl()}
|
||||
ListEmptyComponent={listEmptyComponent}
|
||||
/>
|
||||
{selectedNovels.length > 0 && (
|
||||
<Actionbar
|
||||
theme={theme}
|
||||
actions={[
|
||||
{
|
||||
icon: "check",
|
||||
onPress: () => {
|
||||
selectedNovels.map((id) =>
|
||||
markAllChaptersRead(id)
|
||||
),
|
||||
setSelectedNovels([]);
|
||||
dispatch(
|
||||
getLibraryAction(sort, filter)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "check-outline",
|
||||
onPress: () => {
|
||||
selectedNovels.map((id) =>
|
||||
markAllChaptersUnread(id)
|
||||
);
|
||||
setSelectedNovels([]);
|
||||
dispatch(
|
||||
getLibraryAction(sort, filter)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "delete-outline",
|
||||
onPress: () => {
|
||||
selectedNovels.map((id) =>
|
||||
unfollowNovel(id)
|
||||
);
|
||||
setSelectedNovels([]);
|
||||
dispatch(
|
||||
getLibraryAction(sort, filter)
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Portal>
|
||||
<LibraryFilterSheet
|
||||
bottomSheetRef={libraryFilterSheetRef}
|
||||
|
||||
@@ -6,15 +6,14 @@ import {
|
||||
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";
|
||||
import { Checkbox, SortItem } from "../../../components/Checkbox/Checkbox";
|
||||
|
||||
const LibraryFilterSheet = ({
|
||||
bottomSheetRef,
|
||||
@@ -26,20 +25,11 @@ const LibraryFilterSheet = ({
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
|
||||
const FirstRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Checkbox.Item
|
||||
<View style={{ flex: 1, margin: 8 }}>
|
||||
<Checkbox
|
||||
label="Downloaded"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorHint}
|
||||
status={
|
||||
filter === "chaptersDownloaded > 0"
|
||||
? "checked"
|
||||
: "unchecked"
|
||||
}
|
||||
theme={theme}
|
||||
status={filter === "chaptersDownloaded > 0"}
|
||||
onPress={() =>
|
||||
filter === "chaptersDownloaded > 0"
|
||||
? dispatch(filterLibrary(sort, ""))
|
||||
@@ -48,34 +38,20 @@ const LibraryFilterSheet = ({
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
label="Unread"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorHint}
|
||||
status={filter === "unread = 1" ? "checked" : "unchecked"}
|
||||
theme={theme}
|
||||
status={filter === "unread = 1"}
|
||||
onPress={() =>
|
||||
filter === "unread = 1"
|
||||
? dispatch(filterLibrary(sort, ""))
|
||||
: dispatch(filterLibrary(sort, "unread = 1"))
|
||||
}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
label="Completed"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorHint}
|
||||
status={
|
||||
filter === "chaptersUnread IS NULL"
|
||||
? "checked"
|
||||
: "unchecked"
|
||||
}
|
||||
theme={theme}
|
||||
status={filter === "chaptersUnread IS NULL"}
|
||||
onPress={() =>
|
||||
filter === "chaptersUnread IS NULL"
|
||||
? dispatch(filterLibrary(sort, ""))
|
||||
@@ -87,117 +63,62 @@ const LibraryFilterSheet = ({
|
||||
</View>
|
||||
);
|
||||
|
||||
const sortOrders = [
|
||||
{
|
||||
label: "Date Added",
|
||||
ASC: "novels.novelId ASC",
|
||||
DESC: "novels.novelId DESC",
|
||||
},
|
||||
{
|
||||
label: "Aplhabetically",
|
||||
ASC: "novels.novelName ASC",
|
||||
DESC: "novels.novelName DESC",
|
||||
},
|
||||
{
|
||||
label: "Unread",
|
||||
ASC: "novels.unread ASC",
|
||||
DESC: "novels.unread DESC",
|
||||
},
|
||||
{
|
||||
label: "Downloaded",
|
||||
ASC: "chaptersDownloaded ASC",
|
||||
DESC: "chaptersDownloaded DESC",
|
||||
},
|
||||
{
|
||||
label: "Total Chapters",
|
||||
ASC: "chaptersUnread ASC",
|
||||
DESC: "chaptersUnread DESC",
|
||||
},
|
||||
];
|
||||
|
||||
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 style={{ flex: 1, margin: 8 }}>
|
||||
{sortOrders.map((item, index) => (
|
||||
<SortItem
|
||||
key={index}
|
||||
label={item.label}
|
||||
theme={theme}
|
||||
status={
|
||||
sort === item.ASC
|
||||
? "asc"
|
||||
: sort === item.DESC
|
||||
? "desc"
|
||||
: null
|
||||
}
|
||||
onPress={() =>
|
||||
sort === item.ASC
|
||||
? dispatch(filterLibrary(item.DESC, filter))
|
||||
: dispatch(filterLibrary(item.ASC, filter))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
|
||||
const displayModes = [
|
||||
{ displayMode: 0, label: "Compact Grid" },
|
||||
{ displayMode: 1, label: "Comfortable Grid" },
|
||||
{ displayMode: 3, label: "No Title Grid" },
|
||||
{ displayMode: 2, label: "List" },
|
||||
];
|
||||
|
||||
@@ -210,30 +131,25 @@ const LibraryFilterSheet = ({
|
||||
|
||||
const renderCheckboxes = () => {
|
||||
return displayModes.map((mode) => (
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
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}
|
||||
status={displayMode === mode.displayMode}
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings("displayMode", mode.displayMode))
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const ThirdRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ flex: 1, margin: 8 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
paddingVertical: 8,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
@@ -245,18 +161,15 @@ const LibraryFilterSheet = ({
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
paddingVertical: 8,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
Badges
|
||||
</Text>
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
label="Download Badges"
|
||||
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
|
||||
status={showDownloadBadges ? "checked" : "unchecked"}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccent}
|
||||
status={showDownloadBadges}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
@@ -265,25 +178,21 @@ const LibraryFilterSheet = ({
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
label="Unread Badges"
|
||||
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
|
||||
status={showUnreadBadges ? "checked" : "unchecked"}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccent}
|
||||
status={showUnreadBadges}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings("showUnreadBadges", !showUnreadBadges)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
label="Show number of items"
|
||||
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
|
||||
status={showNumberOfNovels ? "checked" : "unchecked"}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccent}
|
||||
status={showNumberOfNovels}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
@@ -292,6 +201,7 @@ const LibraryFilterSheet = ({
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -317,7 +227,7 @@ const LibraryFilterSheet = ({
|
||||
indicatorStyle={{ backgroundColor: theme.colorAccent }}
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
renderLabel={({ route, focused, color }) => (
|
||||
<Text style={{ color, margin: 8 }}>{route.title}</Text>
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.textColorSecondary}
|
||||
activeColor={theme.colorAccent}
|
||||
@@ -329,8 +239,8 @@ const LibraryFilterSheet = ({
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 470, bottom: 0 }}
|
||||
snappingPoints={[0, 470]}
|
||||
draggableRange={{ top: 480, bottom: 0 }}
|
||||
snappingPoints={[0, 480]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
|
||||
+56
-60
@@ -1,71 +1,67 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import * as Linking from "expo-linking";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { Divider, ListItem, ListSection } from "../../components/List";
|
||||
|
||||
import { List } from "../../components/List";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { MoreHeader } from "./components/MoreHeader";
|
||||
|
||||
const AboutScreen = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar title="About" onBackAction={() => navigation.goBack()} />
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<ListSection>
|
||||
<ListItem
|
||||
title="Version"
|
||||
description="Stable 1.1.1"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Build Time"
|
||||
description="04-07-21 21:00 PM"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader/commits/main"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<Divider theme={theme} />
|
||||
<ListItem
|
||||
title="Discord"
|
||||
description="https://discord.gg/QdcWN4MD63"
|
||||
onPress={() =>
|
||||
Linking.openURL("https://discord.gg/QdcWN4MD63")
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Github"
|
||||
description="https://github.com/LNReader/lnreader"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Sources"
|
||||
description="https://github.com/LNReader/lnreader-sources"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader-sources"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</View>
|
||||
</>
|
||||
<ScreenContainer theme={theme}>
|
||||
<MoreHeader
|
||||
title="About"
|
||||
navigation={navigation}
|
||||
theme={theme}
|
||||
goBack={true}
|
||||
/>
|
||||
<List.Section>
|
||||
<List.Item
|
||||
title="Version"
|
||||
description="Stable 1.1.4 (15/08/21 9:00 AM)"
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader/releases/tag/v1.1.4"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title="Discord"
|
||||
description="https://discord.gg/QdcWN4MD63"
|
||||
onPress={() =>
|
||||
Linking.openURL("https://discord.gg/QdcWN4MD63")
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Github"
|
||||
description="https://github.com/LNReader/lnreader"
|
||||
onPress={() =>
|
||||
Linking.openURL("https://github.com/LNReader/lnreader")
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Sources"
|
||||
description="https://github.com/LNReader/lnreader-sources"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader-sources"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
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 { List } 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
|
||||
@@ -29,59 +22,16 @@ const AdvancedSettings = ({ navigation }) => {
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar title="Advanced" onBackAction={() => navigation.goBack()} />
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Data</ListSubHeader>
|
||||
<ListItem
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Data</List.SubHeader>
|
||||
<List.Item
|
||||
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>
|
||||
</List.Section>
|
||||
<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}
|
||||
|
||||
@@ -2,17 +2,15 @@ import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import {
|
||||
InfoItem,
|
||||
ListItem,
|
||||
ListSection,
|
||||
ListSubHeader,
|
||||
} from "../../components/List";
|
||||
import { List } 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";
|
||||
import {
|
||||
createFullBackup,
|
||||
restoreFullBackup,
|
||||
} from "../../services/backup/backup";
|
||||
|
||||
const BackupSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -25,27 +23,34 @@ const BackupSettings = ({ navigation }) => {
|
||||
title="Backup"
|
||||
onBackAction={() => navigation.goBack()}
|
||||
/>
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Backup</ListSubHeader>
|
||||
<ListItem
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Backup</List.SubHeader>
|
||||
<List.Item
|
||||
title="Create backup"
|
||||
description="Can be used to restore current library"
|
||||
onPress={createBackup}
|
||||
onPress={createFullBackup}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="Restore backup"
|
||||
description="Restore library from backup file"
|
||||
onPress={restoreFullBackup}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title="Restore old backup"
|
||||
description="Restore library from backup file of version v1.1.2 or lower"
|
||||
onPress={() => dispatch(restoreLibraryAction())}
|
||||
theme={theme}
|
||||
/>
|
||||
<InfoItem
|
||||
<List.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>
|
||||
</List.Section>
|
||||
</ScreenContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,12 +2,7 @@ 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 { List } from "../../components/List";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
@@ -22,6 +17,8 @@ import {
|
||||
} from "../../redux/settings/settings.actions";
|
||||
import ColorPickerModal from "../../components/ColorPickerModal";
|
||||
import SwitchSetting from "../../components/Switch/Switch";
|
||||
import { ScrollView } from "react-native";
|
||||
import { SHOW_LAST_UPDATE_TIME } from "../../redux/updates/updates.types";
|
||||
|
||||
const GenralSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -31,14 +28,24 @@ const GenralSettings = ({ navigation }) => {
|
||||
(state) => state.settingsReducer
|
||||
);
|
||||
|
||||
const { updateLibraryOnLaunch = false, downloadNewChapters = false } =
|
||||
useSettings();
|
||||
const {
|
||||
updateLibraryOnLaunch = false,
|
||||
downloadNewChapters = false,
|
||||
showHistoryTab = true,
|
||||
showUpdatesTab = true,
|
||||
showLabelsInNav = false,
|
||||
} = useSettings();
|
||||
|
||||
const { showLastUpdateTime = true } = useSelector(
|
||||
(state) => state.updatesReducer
|
||||
);
|
||||
|
||||
const displayModeLabel = (displayMode) => {
|
||||
const label = {
|
||||
0: "Compact Grid",
|
||||
1: "Comfortable Grid",
|
||||
2: "List",
|
||||
3: "No Title Grid",
|
||||
};
|
||||
|
||||
return label[displayMode] ?? "Compact Grid";
|
||||
@@ -93,86 +100,139 @@ const GenralSettings = ({ navigation }) => {
|
||||
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}>Library</ListSubHeader>
|
||||
<SwitchSetting
|
||||
label="Update library on launch"
|
||||
value={updateLibraryOnLaunch}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"updateLibraryOnLaunch",
|
||||
!updateLibraryOnLaunch
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListSubHeader theme={theme}>Auto-download</ListSubHeader>
|
||||
<SwitchSetting
|
||||
label="Download new chapters"
|
||||
value={downloadNewChapters}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"downloadNewChapters",
|
||||
!downloadNewChapters
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListSubHeader theme={theme}>Theme</ListSubHeader>
|
||||
<ListItem
|
||||
title="Theme"
|
||||
description={theme.name}
|
||||
onPress={showthemeModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Accent Color"
|
||||
description={theme.colorAccent.toUpperCase()}
|
||||
onPress={showAccentColorModal}
|
||||
theme={theme}
|
||||
iconColor={theme.colorAccent}
|
||||
right="circle"
|
||||
/>
|
||||
{theme.statusBar === "light-content" && (
|
||||
<ScrollView>
|
||||
<List.Section>
|
||||
<SwitchSetting
|
||||
label="Pure black dark mode"
|
||||
value={
|
||||
theme.colorPrimary === "#000000" &&
|
||||
theme.colorPrimaryDark === "#000000"
|
||||
}
|
||||
label="Show updates in the nav"
|
||||
value={showUpdatesTab}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAmoledMode(
|
||||
theme.id,
|
||||
!(
|
||||
theme.colorPrimary === "#000000" &&
|
||||
theme.colorPrimaryDark === "#000000"
|
||||
)
|
||||
setAppSettings(
|
||||
"showUpdatesTab",
|
||||
!showUpdatesTab
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</ListSection>
|
||||
<SwitchSetting
|
||||
label="Show history in the nav"
|
||||
value={showHistoryTab}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showHistoryTab",
|
||||
!showHistoryTab
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label="Always show nav labels"
|
||||
value={showLabelsInNav}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showLabelsInNav",
|
||||
!showLabelsInNav
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Display</List.SubHeader>
|
||||
<List.Item
|
||||
title="Display Mode"
|
||||
description={displayModeLabel(displayMode)}
|
||||
onPress={showDisplayModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Items per row in library"
|
||||
description={`${novelsPerRow} items per row`}
|
||||
onPress={showGridSizeModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Library</List.SubHeader>
|
||||
<SwitchSetting
|
||||
label="Update library on launch"
|
||||
value={updateLibraryOnLaunch}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"updateLibraryOnLaunch",
|
||||
!updateLibraryOnLaunch
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label="Show last update time"
|
||||
value={showLastUpdateTime}
|
||||
onPress={() =>
|
||||
dispatch({
|
||||
type: SHOW_LAST_UPDATE_TIME,
|
||||
payload: !showLastUpdateTime,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Auto-download</List.SubHeader>
|
||||
<SwitchSetting
|
||||
label="Download new chapters"
|
||||
value={downloadNewChapters}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"downloadNewChapters",
|
||||
!downloadNewChapters
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Theme</List.SubHeader>
|
||||
<List.Item
|
||||
title="Theme"
|
||||
description={theme.name}
|
||||
onPress={showthemeModal}
|
||||
theme={theme}
|
||||
/>
|
||||
{theme.statusBar === "light-content" && (
|
||||
<SwitchSetting
|
||||
label="Pure black dark mode"
|
||||
value={
|
||||
theme.colorPrimary === "#000000" &&
|
||||
theme.colorPrimaryDark === "#000000"
|
||||
}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAmoledMode(
|
||||
theme.id,
|
||||
!(
|
||||
theme.colorPrimary === "#000000" &&
|
||||
theme.colorPrimaryDark === "#000000"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
<List.ColorItem
|
||||
title="Accent Color"
|
||||
description={theme.colorAccent.toUpperCase()}
|
||||
onPress={showAccentColorModal}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScrollView>
|
||||
<DisplayModeModal
|
||||
displayMode={displayMode}
|
||||
displayModalVisible={displayModalVisible}
|
||||
|
||||
+87
-113
@@ -1,13 +1,15 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Image, Pressable, Text } from "react-native";
|
||||
import { List, Switch } from "react-native-paper";
|
||||
import { Switch } from "react-native-paper";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { Divider, ListItem, ListSection } from "../../components/List";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { List } from "../../components/List";
|
||||
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import { setAppSettings } from "../../redux/settings/settings.actions";
|
||||
import { MoreHeader } from "./components/MoreHeader";
|
||||
|
||||
const MoreScreen = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -20,119 +22,91 @@ const MoreScreen = ({ navigation }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar title="More" style={{ elevation: 0 }} />
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
|
||||
<View
|
||||
style={{
|
||||
paddingTop: 10,
|
||||
paddingBottom: 25,
|
||||
alignItems: "center",
|
||||
backgroundColor: theme.colorPrimary,
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={require("../../../assets/logo.png")}
|
||||
style={{
|
||||
height: 80,
|
||||
width: 80,
|
||||
tintColor: theme.textColorPrimary,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<ListSection>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={enableIncognitoMode}
|
||||
>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<List.Icon
|
||||
color={theme.colorAccent}
|
||||
icon="incognito"
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
Incognito Mode
|
||||
</Text>
|
||||
<Text
|
||||
style={{ color: theme.textColorSecondary }}
|
||||
>
|
||||
Pauses reading history
|
||||
</Text>
|
||||
</View>
|
||||
<ScreenContainer theme={theme}>
|
||||
<MoreHeader title="More" navigation={navigation} theme={theme} />
|
||||
<List.Section>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={enableIncognitoMode}
|
||||
>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<List.Icon theme={theme} icon="incognito" />
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
Incognito Mode
|
||||
</Text>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Pauses reading history
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={incognitoMode}
|
||||
onValueChange={enableIncognitoMode}
|
||||
color={theme.colorAccent}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Switch
|
||||
value={incognitoMode}
|
||||
onValueChange={enableIncognitoMode}
|
||||
color={theme.colorAccent}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title="Download queue"
|
||||
description={
|
||||
downloadQueue.length > 0 &&
|
||||
downloadQueue.length + " remaining"
|
||||
}
|
||||
icon="progress-download"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "DownloadQueue",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Downloads"
|
||||
icon="folder-download"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "Downloads",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title="Settings"
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "SettingsStack",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
title="Download queue"
|
||||
description={
|
||||
downloadQueue.length > 0 &&
|
||||
downloadQueue.length + " remaining"
|
||||
}
|
||||
icon="progress-download"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "DownloadQueue",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Downloads"
|
||||
icon="folder-download"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "Downloads",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
title="Settings"
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "SettingsStack",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
title="About"
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "About",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</View>
|
||||
</>
|
||||
<List.Item
|
||||
title="About"
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "About",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+257
-146
@@ -1,12 +1,27 @@
|
||||
import React, { useState } from "react";
|
||||
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
import { ToggleButton } from "react-native-paper";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import {
|
||||
Dimensions,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Button } from "react-native-paper";
|
||||
import WebView from "react-native-webview";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import ColorPickerModal from "../../components/ColorPickerModal";
|
||||
import { Row, ScreenContainer } from "../../components/Common";
|
||||
import { ListItem, ListSection, ListSubHeader } from "../../components/List";
|
||||
import {
|
||||
ToggleButton,
|
||||
ToggleColorButton,
|
||||
} from "../../components/Common/ToggleButton";
|
||||
import { List } from "../../components/List";
|
||||
|
||||
import { useReaderSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import { setReaderSettings } from "../../redux/settings/settings.actions";
|
||||
@@ -16,18 +31,25 @@ import {
|
||||
readerTextColor,
|
||||
} from "../Chapter/readerStyleController";
|
||||
|
||||
const ReaderSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const FirstRoute = React.memo(({ theme, dispatch, reader }) => {
|
||||
const presetThemes = [
|
||||
{
|
||||
value: 1,
|
||||
backgroundColor: "#000000",
|
||||
textColor: "rgba(255,255,255,0.7)",
|
||||
},
|
||||
{ value: 2, backgroundColor: "#FFFFFF", textColor: "#111111" },
|
||||
{ value: 3, backgroundColor: "#F7DFC6", textColor: "#593100" },
|
||||
{ value: 4, backgroundColor: "#292832", textColor: "#CCCCCC" },
|
||||
{ value: 5, backgroundColor: "#2B2C30", textColor: "#CCCCCC" },
|
||||
];
|
||||
|
||||
const reader = useReaderSettings();
|
||||
|
||||
const textColor = {
|
||||
1: "rgba(255,255,255,0.7)",
|
||||
2: "#000000",
|
||||
3: "#000000",
|
||||
4: "#CCCCCC",
|
||||
};
|
||||
const textAlignments = [
|
||||
{ value: "left", icon: "format-align-left" },
|
||||
{ value: "center", icon: "format-align-center" },
|
||||
{ value: "justify", icon: "format-align-justify" },
|
||||
{ value: "right", icon: "format-align-right" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Reader Background Color Modal
|
||||
@@ -54,81 +76,14 @@ const ReaderSettings = ({ navigation }) => {
|
||||
dispatch(setReaderSettings("textColor", val ?? reader.textColor));
|
||||
};
|
||||
|
||||
const readerStyles = [
|
||||
{
|
||||
paddingVertical: 16,
|
||||
paddingBottom: 32,
|
||||
fontSize: reader.textSize,
|
||||
color: reader.textColor,
|
||||
lineHeight: readerLineHeight(reader.textSize, reader.lineHeight),
|
||||
textAlign: reader.textAlign,
|
||||
paddingHorizontal: `${reader.padding}%`,
|
||||
},
|
||||
reader.fontFamily && {
|
||||
fontFamily: reader.fontFamily,
|
||||
},
|
||||
];
|
||||
|
||||
const presetThemes = [
|
||||
{
|
||||
value: 1,
|
||||
backgroundColor: "#000000",
|
||||
textColor: "rgba(255,255,255,0.7)",
|
||||
},
|
||||
{ value: 2, backgroundColor: "#FFFFFF", textColor: "#111111" },
|
||||
{ value: 3, backgroundColor: "#F7DFC6", textColor: "#593100" },
|
||||
{ value: 4, backgroundColor: "#292832", textColor: "#CCCCCC" },
|
||||
{ value: 5, backgroundColor: "#2B2C30", textColor: "#CCCCCC" },
|
||||
];
|
||||
|
||||
const onValueChange = (value) => {
|
||||
dispatch(setReaderSettings("theme", value ?? reader.theme));
|
||||
dispatch(
|
||||
setReaderSettings("textColor", textColor[value] ?? reader.textColor)
|
||||
);
|
||||
};
|
||||
|
||||
const textAlignments = [
|
||||
{ value: "left", icon: "format-align-left" },
|
||||
{ value: "justify", icon: "format-align-justify" },
|
||||
{ value: "right", icon: "format-align-right" },
|
||||
];
|
||||
|
||||
const onValueChangeTextAlign = (value) => {
|
||||
dispatch(setReaderSettings("textAlign", value ?? reader.textAlign));
|
||||
};
|
||||
const [customHtmlCSS, setcustomHtmlCSS] = useState(reader.customCSS);
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar title="Reader" onBackAction={navigation.goBack} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView>
|
||||
<View
|
||||
style={{ backgroundColor: readerBackground(reader.theme) }}
|
||||
>
|
||||
<Text style={readerStyles}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Ut lobortis nunc id nisl tempor, sed tristique sem
|
||||
sagittis. Mauris consequat orci egestas volutpat
|
||||
vestibulum. Nullam convallis, leo in porttitor
|
||||
fringilla, ex mi finibus felis, non molestie ligula
|
||||
ligula facilisis ligula. Nam eu est a turpis porttitor
|
||||
aliquet sed vel risus. Ut ac scelerisque diam. Nulla
|
||||
fringilla enim non ex rhoncus faucibus. Integer eget
|
||||
lacus justo. Lorem ipsum dolor sit amet, consectetur
|
||||
adipiscing elit. Ut vitae tempor metus. Morbi nec nisl
|
||||
cursus, gravida ex vitae, laoreet leo. Pellentesque in
|
||||
lectus ante. Aliquam porta suscipit pretium. Sed ut
|
||||
ligula at dolor pretium egestas quis vitae urna. In
|
||||
ullamcorper venenatis orci, eu pulvinar nibh venenatis
|
||||
sit amet.
|
||||
</Text>
|
||||
</View>
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Preset</ListSubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressableListItem}
|
||||
>
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Preset</List.SubHeader>
|
||||
<View style={styles.pressableListItem}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
@@ -137,44 +92,51 @@ const ReaderSettings = ({ navigation }) => {
|
||||
>
|
||||
Preset Themes
|
||||
</Text>
|
||||
<Row>
|
||||
<ToggleButton.Row
|
||||
onValueChange={onValueChange}
|
||||
value={reader.theme}
|
||||
>
|
||||
<ScrollView
|
||||
style={{ marginLeft: 16 }}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
<Row>
|
||||
{presetThemes.map((theme) => (
|
||||
<ToggleButton
|
||||
<ToggleColorButton
|
||||
key={theme.value}
|
||||
icon={
|
||||
reader.theme === theme.value &&
|
||||
"check"
|
||||
selected={
|
||||
reader.theme ===
|
||||
theme.backgroundColor
|
||||
}
|
||||
color={theme.textColor}
|
||||
value={theme.value}
|
||||
style={[
|
||||
{
|
||||
backgroundColor:
|
||||
theme.backgroundColor,
|
||||
},
|
||||
styles.presetToggleButton,
|
||||
]}
|
||||
backgroundColor={theme.backgroundColor}
|
||||
textColor={theme.textColor}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"theme",
|
||||
theme.backgroundColor
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textColor",
|
||||
theme.textColor
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ToggleButton.Row>
|
||||
</Row>
|
||||
</Pressable>
|
||||
<ListSubHeader theme={theme}>Reader</ListSubHeader>
|
||||
<ListItem
|
||||
</Row>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Reader</List.SubHeader>
|
||||
<List.ColorItem
|
||||
title="Background Color"
|
||||
description={readerBackground(
|
||||
reader.theme
|
||||
).toUpperCase()}
|
||||
onPress={showReaderBackgroundtColorModal}
|
||||
theme={theme}
|
||||
iconColor={readerBackground(reader.theme)}
|
||||
right="circle"
|
||||
/>
|
||||
<ListItem
|
||||
<List.ColorItem
|
||||
title="Text Color"
|
||||
description={
|
||||
reader.textColor.toUpperCase() ||
|
||||
@@ -182,16 +144,10 @@ const ReaderSettings = ({ navigation }) => {
|
||||
}
|
||||
onPress={showReaderTextColorModal}
|
||||
theme={theme}
|
||||
iconColor={
|
||||
reader.textColor || readerTextColor(reader.theme)
|
||||
}
|
||||
right="circle"
|
||||
/>
|
||||
<ListSubHeader theme={theme}>Text</ListSubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressableListItem}
|
||||
>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Text</List.SubHeader>
|
||||
<Pressable style={styles.pressableListItem}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
@@ -211,32 +167,70 @@ const ReaderSettings = ({ navigation }) => {
|
||||
</Text>
|
||||
</View>
|
||||
<Row>
|
||||
<ToggleButton.Row
|
||||
onValueChange={onValueChangeTextAlign}
|
||||
value={reader.textAlign}
|
||||
>
|
||||
{textAlignments.map((item) => (
|
||||
<ToggleButton
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
color={
|
||||
reader.textAlign === item.value
|
||||
? theme.colorAccent
|
||||
: "#FFFFFF"
|
||||
}
|
||||
value={item.value}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ToggleButton.Row>
|
||||
{textAlignments.map((item) => (
|
||||
<ToggleButton
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textAlign",
|
||||
item.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
selected={item.value === reader.textAlign}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
</Pressable>
|
||||
</ListSection>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>
|
||||
Custom CSS (Only works in webview mode)
|
||||
</List.SubHeader>
|
||||
<View style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
|
||||
<TextInput
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
value={customHtmlCSS}
|
||||
onChangeText={(text) => setcustomHtmlCSS(text)}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
placeholder="Example: body { color: red; }"
|
||||
multiline={true}
|
||||
/>
|
||||
<View style={{ flexDirection: "row-reverse" }}>
|
||||
<Button
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"customCSS",
|
||||
customHtmlCSS
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={() => {
|
||||
setcustomHtmlCSS("");
|
||||
dispatch(
|
||||
setReaderSettings("customCSS", "")
|
||||
);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</List.Section>
|
||||
</ScrollView>
|
||||
<ColorPickerModal
|
||||
title="Reader background color"
|
||||
@@ -254,6 +248,123 @@ const ReaderSettings = ({ navigation }) => {
|
||||
theme={theme}
|
||||
onSubmit={setReaderTextColor}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const SecondRoute = React.memo(({ reader }) => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<WebView
|
||||
originWhitelist={["*"]}
|
||||
style={{
|
||||
backgroundColor: readerBackground(reader.theme),
|
||||
}}
|
||||
source={{
|
||||
html: `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
color: ${reader.textColor};
|
||||
text-align: ${reader.textAlign};
|
||||
line-height: ${reader.lineHeight};
|
||||
font-size: ${reader.textSize}px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: ${reader.padding}%;
|
||||
padding-right: ${reader.padding}%;
|
||||
font-family: ${reader.fontFamily};
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
|
||||
${reader.customCSS}
|
||||
|
||||
@font-face {
|
||||
font-family: ${reader.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${reader.fontFamily}.ttf");
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean lobortis, diam sed malesuada bibendum, nulla libero scelerisque sapien, nec interdum nisl ipsum ac ipsum. In lacinia eros ut quam commodo, in finibus augue ultricies. Vestibulum ex purus, condimentum eget sem at, molestie semper mi. Mauris ac feugiat quam. Pellentesque sagittis bibendum nibh eu lacinia. Aenean rhoncus, velit sit amet mollis egestas, diam turpis ornare velit, a dictum elit velit in erat. Quisque luctus in sem a vulputate.
|
||||
</p>
|
||||
<p> Pellentesque id tempus orci, non finibus tortor. Suspendisse in neque non eros eleifend hendrerit vitae a lacus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc interdum magna nulla, eget pharetra arcu dapibus eu. Pellentesque sed lectus sit amet sem porta lacinia. Curabitur consectetur dolor in nibh varius, ac placerat erat pellentesque. Proin in justo condimentum, fermentum lacus eget, ultrices ante. Fusce tempor blandit erat nec mollis. Vivamus in leo et nunc consectetur elementum nec ac lectus.
|
||||
</p>
|
||||
<p> In consectetur libero tempor metus interdum, sit amet viverra leo dapibus. Nullam tincidunt justo hendrerit lorem maximus, eu pretium erat fringilla. Nulla scelerisque leo at enim convallis, a elementum velit laoreet. Aliquam quis vestibulum quam. Morbi ut sollicitudin mi, et laoreet elit. Vivamus mattis, nulla at blandit sodales, lectus lacus luctus metus, non euismod neque urna vel nisl. Quisque vitae ante vitae orci tempus egestas quis eu nibh. Phasellus eget nulla non velit tincidunt fringilla eget nec libero. Morbi tempor erat quis rutrum condimentum.
|
||||
</p>
|
||||
<p> Cras sem magna, tempus sed urna eget, scelerisque tincidunt tortor. Cras euismod turpis libero, ut condimentum nulla ultrices id. Phasellus faucibus, elit ut imperdiet euismod, enim dui volutpat turpis, in tincidunt ex ligula non felis. Praesent erat erat, ultrices ut imperdiet sed, congue eu leo. Aliquam viverra fringilla ultrices. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Praesent ac malesuada massa. Cras eu augue in dui venenatis laoreet sit amet eget sem. Nullam malesuada sapien felis, et varius neque auctor eu.
|
||||
</p>
|
||||
<p> Sed vestibulum facilisis libero, in aliquet mauris lobortis et. In vitae lectus id risus cursus dictum at ac mi. Praesent ac lectus eget nunc elementum pellentesque. Ut at dui magna. Aliquam mattis posuere gravida. Morbi volutpat, dolor quis varius tincidunt, quam nulla vulputate libero, eu sagittis dui odio et magna. Sed luctus lectus ante, sit amet ultrices ligula convallis vitae. Sed efficitur libero nec quam porta, et fringilla turpis egestas. Nulla facilisi. Duis leo tellus, porta non enim eu, tincidunt cursus lorem.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
));
|
||||
|
||||
const ReaderSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const reader = useReaderSettings();
|
||||
|
||||
const renderScene = ({ route }) => {
|
||||
switch (route.key) {
|
||||
case "first":
|
||||
return (
|
||||
<FirstRoute
|
||||
dispatch={dispatch}
|
||||
theme={theme}
|
||||
reader={reader}
|
||||
/>
|
||||
);
|
||||
case "second":
|
||||
return <SecondRoute reader={reader} />;
|
||||
}
|
||||
};
|
||||
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
const [routes] = useState([
|
||||
{ key: "first", title: "Settings" },
|
||||
{ key: "second", title: "Preview" },
|
||||
]);
|
||||
|
||||
const renderTabBar = (props) => (
|
||||
<TabBar
|
||||
{...props}
|
||||
indicatorStyle={{ backgroundColor: theme.colorAccent }}
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
renderLabel={({ route, focused, color }) => (
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.textColorSecondary}
|
||||
activeColor={theme.colorAccent}
|
||||
pressColor={theme.rippleColor}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar
|
||||
title="Reader"
|
||||
onBackAction={navigation.goBack}
|
||||
style={{ elevation: 0 }}
|
||||
/>
|
||||
|
||||
<TabView
|
||||
navigationState={{ index, routes }}
|
||||
renderTabBar={renderTabBar}
|
||||
renderScene={renderScene}
|
||||
onIndexChange={setIndex}
|
||||
initialLayout={{ width: layout.width }}
|
||||
swipeEnabled={false}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { ListItem } from "../../components/List";
|
||||
import { List } from "../../components/List";
|
||||
|
||||
import { useTheme } from "../../hooks/reduxHooks";
|
||||
|
||||
@@ -18,7 +18,7 @@ const SettingsScreen = ({ navigation }) => {
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="General"
|
||||
icon="tune"
|
||||
onPress={() =>
|
||||
@@ -28,7 +28,7 @@ const SettingsScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="Reader"
|
||||
icon="book-open-outline"
|
||||
onPress={() =>
|
||||
@@ -38,7 +38,7 @@ const SettingsScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="Tracking"
|
||||
icon="sync"
|
||||
onPress={() =>
|
||||
@@ -48,7 +48,7 @@ const SettingsScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="Backup"
|
||||
icon="cloud-upload-outline"
|
||||
onPress={() =>
|
||||
@@ -58,7 +58,7 @@ const SettingsScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="Advanced"
|
||||
icon="code-tags"
|
||||
onPress={() =>
|
||||
|
||||
@@ -11,12 +11,7 @@ import {
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import {
|
||||
Divider,
|
||||
ListItem,
|
||||
ListSection,
|
||||
ListSubHeader,
|
||||
} from "../../components/List";
|
||||
import { List } from "../../components/List";
|
||||
import { removeTracker, setTracker } from "../../redux/tracker/tracker.actions";
|
||||
|
||||
const TrackerScreen = ({ navigation }) => {
|
||||
@@ -39,9 +34,9 @@ const TrackerScreen = ({ navigation }) => {
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Services</ListSubHeader>
|
||||
<ListItem
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Services</List.SubHeader>
|
||||
<List.Item
|
||||
title="MyAnimeList"
|
||||
onPress={async () => {
|
||||
if (tracker) {
|
||||
@@ -82,10 +77,10 @@ const TrackerScreen = ({ navigation }) => {
|
||||
{tracker && tracker.expires_in < new Date(Date.now()) && (
|
||||
<>
|
||||
<Divider theme={theme} />
|
||||
<ListSubHeader theme={theme}>
|
||||
<List.SubHeader theme={theme}>
|
||||
Settings
|
||||
</ListSubHeader>
|
||||
<ListItem
|
||||
</List.SubHeader>
|
||||
<List.Item
|
||||
title="Revalidate MyAnimeList"
|
||||
onPress={() => {
|
||||
const res = malTokenWatcher(tracker);
|
||||
@@ -95,7 +90,7 @@ const TrackerScreen = ({ navigation }) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ListSection>
|
||||
</List.Section>
|
||||
|
||||
<Portal>
|
||||
<Modal
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Text, Pressable } from "react-native";
|
||||
import { Portal, Modal, Checkbox } from "react-native-paper";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import { Portal, Modal } from "react-native-paper";
|
||||
|
||||
import { Checkbox } from "../../../components/Checkbox/Checkbox";
|
||||
|
||||
import { setAppSettings } from "../../../redux/settings/settings.actions";
|
||||
|
||||
const DisplayModeModal = ({
|
||||
@@ -13,44 +17,21 @@ const DisplayModeModal = ({
|
||||
const displayModes = [
|
||||
{ displayMode: 0, label: "Compact Grid" },
|
||||
{ displayMode: 1, label: "Comfortable Grid" },
|
||||
{ displayMode: 3, label: "No Title Grid" },
|
||||
{ displayMode: 2, label: "List" },
|
||||
];
|
||||
|
||||
const renderCheckboxes = () => {
|
||||
return displayModes.map((mode) => (
|
||||
<View
|
||||
<Checkbox
|
||||
key={mode.displayMode}
|
||||
style={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginVertical: 4,
|
||||
},
|
||||
displayMode === mode.displayMode && {
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings("displayMode", mode.displayMode)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
{mode.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
status={displayMode === mode.displayMode}
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings("displayMode", mode.displayMode))
|
||||
}
|
||||
label={mode.label}
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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";
|
||||
import { Checkbox } from "../../../components/Checkbox/Checkbox";
|
||||
|
||||
const GridSizeModal = ({
|
||||
dispatch,
|
||||
@@ -46,19 +47,15 @@ const GridSizeModal = ({
|
||||
>
|
||||
{`${novelsPerRow} per row`}
|
||||
</Text>
|
||||
<RadioButtonGroup
|
||||
onValueChange={(value) => dispatch(setNovelsPerRow(value))}
|
||||
value={novelsPerRow}
|
||||
>
|
||||
{Object.keys(gridSizes).map((item) => (
|
||||
<RadioButton
|
||||
key={item}
|
||||
value={item}
|
||||
label={gridSizes[item]}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</RadioButtonGroup>
|
||||
{Object.keys(gridSizes).map((item) => (
|
||||
<Checkbox
|
||||
key={item}
|
||||
status={item === novelsPerRow}
|
||||
label={gridSizes[item]}
|
||||
onPress={() => dispatch(setNovelsPerRow(item))}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { Image, StyleSheet, View } from "react-native";
|
||||
import { Appbar } from "../../../components/Appbar";
|
||||
|
||||
export const MoreHeader = ({ title, navigation, theme, goBack }) => (
|
||||
<>
|
||||
<Appbar
|
||||
title={title}
|
||||
onBackAction={goBack && navigation.goBack}
|
||||
style={{ elevation: 0 }}
|
||||
/>
|
||||
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
|
||||
<View
|
||||
style={[
|
||||
styles.logoContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={require("../../../../assets/logo.png")}
|
||||
style={[styles.logo, { tintColor: theme.textColorPrimary }]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
logoContainer: {
|
||||
paddingTop: 8,
|
||||
paddingBottom: 24,
|
||||
alignItems: "center",
|
||||
elevation: 4,
|
||||
},
|
||||
logo: {
|
||||
height: 80,
|
||||
width: 80,
|
||||
},
|
||||
});
|
||||
@@ -1,53 +1,35 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, View, Text } from "react-native";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import { Portal, Modal } from "react-native-paper";
|
||||
import { Checkbox } from "../../../components/Checkbox/Checkbox";
|
||||
|
||||
import { setAppTheme } from "../../../redux/settings/settings.actions";
|
||||
|
||||
const ThemeModal = ({ themeModalVisible, hidethemeModal, dispatch, theme }) => {
|
||||
const themes = [
|
||||
{ id: 1, name: "Light" },
|
||||
{ id: 10, name: "Spring Blossom" },
|
||||
{ id: 2, name: "Dark" },
|
||||
{ id: 3, name: "Midnight Dusk" },
|
||||
{ id: 4, name: "Green Apple" },
|
||||
{ id: 5, name: "Iris Blue" },
|
||||
{ id: 6, name: "Oceanic" },
|
||||
{ id: 7, name: "Strawberry Daiquiri" },
|
||||
{ id: 8, name: "Tako" },
|
||||
{ id: 9, name: "Yin & Yang" },
|
||||
];
|
||||
|
||||
const ThemeItem = ({ item }) => (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginVertical: 4,
|
||||
},
|
||||
theme.id === item.id && {
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() => dispatch(setAppTheme(item.id))}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderThemeCheckboxes = () =>
|
||||
themes.map((item) => <ThemeItem item={item} key={item.id} />);
|
||||
themes.map((item) => (
|
||||
<Checkbox
|
||||
key={item.id}
|
||||
status={theme.id === item.id}
|
||||
label={item.name}
|
||||
onPress={() => dispatch(setAppTheme(item.id))}
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
|
||||
+100
-4
@@ -7,6 +7,7 @@ import {
|
||||
StatusBar,
|
||||
Dimensions,
|
||||
Share,
|
||||
Text,
|
||||
} from "react-native";
|
||||
|
||||
import { Provider, Portal, Appbar, IconButton, Menu } from "react-native-paper";
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
markChaptersRead,
|
||||
markChapterUnreadAction,
|
||||
markPreviousChaptersReadAction,
|
||||
setNovel,
|
||||
sortAndFilterChapters,
|
||||
updateNovelAction,
|
||||
} from "../../redux/novel/novel.actions";
|
||||
@@ -40,6 +42,8 @@ import TrackSheet from "./components/Tracker/TrackSheet";
|
||||
import { Row } from "../../components/Common";
|
||||
import JumpToChapterModal from "./components/JumpToChapterModal";
|
||||
import { Actionbar } from "../../components/Actionbar/Actionbar";
|
||||
import EditInfoModal from "./components/EditInfoModal";
|
||||
import { setCustomNovelCover } from "../../database/queries/NovelQueries";
|
||||
|
||||
const Novel = ({ route, navigation }) => {
|
||||
const item = route.params;
|
||||
@@ -52,6 +56,7 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [downloadMenu, showDownloadMenu] = useState(false);
|
||||
const [extraMenu, showExtraMenu] = useState(false);
|
||||
|
||||
let flatlistRef = useRef(null);
|
||||
let chaptersSettingsSheetRef = useRef(null);
|
||||
@@ -136,14 +141,41 @@ const Novel = ({ route, navigation }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToChapter = (chapter) =>
|
||||
navigation.navigate("Chapter", {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelName,
|
||||
...chapter,
|
||||
});
|
||||
|
||||
const showProgressPercentage = (chapter) => {
|
||||
const savedProgress =
|
||||
position &&
|
||||
position[chapter.chapterId] &&
|
||||
position[chapter.chapterId].percentage;
|
||||
if (savedProgress < 100 && savedProgress > 0 && !chapter.read) {
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorHint,
|
||||
fontSize: 12,
|
||||
marginLeft: chapter.releaseDate ? 5 : 0,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{chapter.releaseDate && "• "}
|
||||
{"Progress " + position[chapter.chapterId].percentage + "%"}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }) => (
|
||||
<ChapterItem
|
||||
theme={theme}
|
||||
chapter={item}
|
||||
index={index}
|
||||
novelData={{ sourceId, novelUrl, novelName }}
|
||||
navigation={navigation}
|
||||
position={position}
|
||||
showChapterTitles={showChapterTitles}
|
||||
downloadQueue={downloadQueue}
|
||||
deleteChapter={deleteChapter}
|
||||
@@ -151,9 +183,13 @@ const Novel = ({ route, navigation }) => {
|
||||
isSelected={isSelected}
|
||||
onSelectPress={onSelectPress}
|
||||
onSelectLongPress={onSelectLongPress}
|
||||
navigateToChapter={navigateToChapter}
|
||||
showProgressPercentage={showProgressPercentage}
|
||||
/>
|
||||
);
|
||||
|
||||
const [editInfoModal, showEditInfoModal] = useState(false);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<View
|
||||
@@ -272,13 +308,13 @@ const Novel = ({ route, navigation }) => {
|
||||
}}
|
||||
onPress={() => showJumpToChapterModal(true)}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
icon="share-variant"
|
||||
color="white"
|
||||
size={21}
|
||||
style={{
|
||||
marginTop: StatusBar.currentHeight + 8,
|
||||
marginRight: 16,
|
||||
}}
|
||||
onPress={() =>
|
||||
Share.share({
|
||||
@@ -286,6 +322,59 @@ const Novel = ({ route, navigation }) => {
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Menu
|
||||
visible={extraMenu}
|
||||
onDismiss={() => showExtraMenu(false)}
|
||||
anchor={
|
||||
<IconButton
|
||||
icon="dots-vertical"
|
||||
color="white"
|
||||
size={21}
|
||||
style={{
|
||||
marginTop:
|
||||
StatusBar.currentHeight + 8,
|
||||
marginRight: 16,
|
||||
}}
|
||||
onPress={() => showExtraMenu(true)}
|
||||
/>
|
||||
}
|
||||
contentStyle={{
|
||||
backgroundColor: theme.menuColor,
|
||||
}}
|
||||
>
|
||||
<Menu.Item
|
||||
title="Edit info"
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
onPress={() => {
|
||||
showEditInfoModal(true);
|
||||
showExtraMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Edit cover"
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
onPress={async () => {
|
||||
const cover = await setCustomNovelCover(
|
||||
novelId
|
||||
);
|
||||
|
||||
dispatch(
|
||||
setNovel({
|
||||
...novel,
|
||||
novelCover: cover,
|
||||
})
|
||||
);
|
||||
|
||||
showExtraMenu(false);
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</Row>
|
||||
</View>
|
||||
)}
|
||||
@@ -397,6 +486,13 @@ const Novel = ({ route, navigation }) => {
|
||||
novel={novel}
|
||||
navigation={navigation}
|
||||
/>
|
||||
<EditInfoModal
|
||||
modalVisible={editInfoModal}
|
||||
hideModal={() => showEditInfoModal(false)}
|
||||
novel={novel}
|
||||
theme={theme}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
<ChaptersSettingsSheet
|
||||
novelUrl={novelUrl}
|
||||
bottomSheetRef={chaptersSettingsSheetRef}
|
||||
|
||||
@@ -10,12 +10,9 @@ import {
|
||||
} from "./Chapter/ChapterDownloadButtons";
|
||||
|
||||
const ChapterItem = ({
|
||||
novelData,
|
||||
chapter,
|
||||
theme,
|
||||
navigation,
|
||||
index,
|
||||
position,
|
||||
showChapterTitles,
|
||||
downloadQueue,
|
||||
downloadChapter,
|
||||
@@ -23,54 +20,15 @@ const ChapterItem = ({
|
||||
isSelected,
|
||||
onSelectPress,
|
||||
onSelectLongPress,
|
||||
navigateToChapter,
|
||||
showProgressPercentage,
|
||||
}) => {
|
||||
const { sourceId, novelName, novelUrl } = novelData;
|
||||
const {
|
||||
novelId,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
read,
|
||||
releaseDate,
|
||||
bookmark,
|
||||
} = chapter;
|
||||
const { chapterId, chapterName, read, releaseDate, bookmark } = chapter;
|
||||
|
||||
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
|
||||
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
|
||||
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
|
||||
|
||||
const navigateToChapter = () =>
|
||||
navigation.navigate("Chapter", {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelId,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
novelName,
|
||||
bookmark,
|
||||
});
|
||||
|
||||
const renderProgressPercentage = () => {
|
||||
const savedProgress =
|
||||
position && position[chapterId] && position[chapterId].percentage;
|
||||
if (savedProgress < 100 && savedProgress > 0 && !read) {
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorHint,
|
||||
fontSize: 12,
|
||||
marginLeft: releaseDate ? 5 : 0,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{releaseDate && "• "}
|
||||
{"Progress " + position[chapterId].percentage + "%"}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[
|
||||
@@ -79,12 +37,14 @@ const ChapterItem = ({
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
onPress={() => onSelectPress(chapter, navigateToChapter)}
|
||||
onPress={() =>
|
||||
onSelectPress(chapter, () => navigateToChapter(chapter))
|
||||
}
|
||||
onLongPress={() => onSelectLongPress(chapter)}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Row>
|
||||
{bookmark === true && <ChapterBookmarkButton theme={theme} />}
|
||||
<Row style={{ flex: 1, overflow: "hidden" }}>
|
||||
{!!bookmark && <ChapterBookmarkButton theme={theme} />}
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
@@ -98,7 +58,7 @@ const ChapterItem = ({
|
||||
? parseChapterNumber(chapterName)
|
||||
? "Chapter " + parseChapterNumber(chapterName)
|
||||
: "Chapter " + index
|
||||
: chapterName.substring(0, 50)}
|
||||
: chapterName}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
@@ -119,7 +79,7 @@ const ChapterItem = ({
|
||||
{releaseDate}
|
||||
</Text>
|
||||
)}
|
||||
{renderProgressPercentage()}
|
||||
{showProgressPercentage(chapter)}
|
||||
</View>
|
||||
</View>
|
||||
</Row>
|
||||
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
Animated,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
|
||||
|
||||
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
|
||||
import { Checkbox as PaperCheckbox } from "react-native-paper";
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
|
||||
import { Checkbox, SortItem } from "../../../components/Checkbox/Checkbox";
|
||||
|
||||
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,
|
||||
@@ -34,7 +35,7 @@ const ChaptersSettingsSheet = ({
|
||||
|
||||
const FirstRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Checkbox.Item
|
||||
<PaperCheckbox.Item
|
||||
label="Downloaded"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
@@ -53,7 +54,7 @@ const ChaptersSettingsSheet = ({
|
||||
: filterChapters(filter + " AND downloaded=1")
|
||||
}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<PaperCheckbox.Item
|
||||
label="Unread"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
@@ -80,7 +81,7 @@ const ChaptersSettingsSheet = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<PaperCheckbox.Item
|
||||
label="Bookmarked"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
@@ -101,48 +102,36 @@ const ChaptersSettingsSheet = ({
|
||||
);
|
||||
|
||||
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}
|
||||
<View style={{ flex: 1, margin: 8 }}>
|
||||
<SortItem
|
||||
label="By source"
|
||||
status={sort === "ORDER BY chapterId ASC" ? "asc" : "desc"}
|
||||
onPress={() =>
|
||||
sort === "ORDER BY chapterId ASC"
|
||||
? sortChapters("ORDER BY chapterId DESC")
|
||||
: sortChapters("ORDER BY chapterId ASC")
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const ThirdRoute = () => (
|
||||
<View style={{ flex: 1, padding: 8 }}>
|
||||
<RadioButtonGroup
|
||||
onValueChange={(value) =>
|
||||
dispatch(showChapterTitlesAction(novelId, value))
|
||||
<Checkbox
|
||||
status={!showChapterTitles}
|
||||
label="Source title"
|
||||
onPress={() =>
|
||||
dispatch(showChapterTitlesAction(novelId, false))
|
||||
}
|
||||
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>
|
||||
theme={theme}
|
||||
/>
|
||||
<Checkbox
|
||||
status={showChapterTitles}
|
||||
label="Chapter number"
|
||||
onPress={() => dispatch(showChapterTitlesAction(novelId, true))}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -167,7 +156,7 @@ const ChaptersSettingsSheet = ({
|
||||
indicatorStyle={{ backgroundColor: theme.colorAccent }}
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
renderLabel={({ route, focused, color }) => (
|
||||
<Text style={{ color, margin: 8 }}>{route.title}</Text>
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.textColorSecondary}
|
||||
activeColor={theme.colorAccent}
|
||||
@@ -181,11 +170,12 @@ const ChaptersSettingsSheet = ({
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 220, bottom: 0 }}
|
||||
snappingPoints={[0, 220]}
|
||||
backdropOpacity={0.5}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<TabView
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import React, { useState } from "react";
|
||||
import { FlatList, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { Button, Modal, Portal, Switch, TextInput } from "react-native-paper";
|
||||
import { setNovel } from "../../../redux/novel/novel.actions";
|
||||
import { updateNovelInfo } from "../../../database/queries/NovelQueries";
|
||||
|
||||
const EditInfoModal = ({ theme, hideModal, modalVisible, novel, dispatch }) => {
|
||||
const [info, setInfo] = useState(novel);
|
||||
|
||||
const [tag, setTag] = useState("");
|
||||
|
||||
const textInputTheme = {
|
||||
colors: {
|
||||
primary: theme.colorAccent,
|
||||
placeholder: theme.textColorHint,
|
||||
text: theme.textColorPrimary,
|
||||
background: theme.colorPrimary,
|
||||
},
|
||||
};
|
||||
|
||||
const removeTag = (tag) => {
|
||||
let tags = info.genre.split(",").filter((item) => item !== tag);
|
||||
setInfo({ ...info, genre: tags.join(",") });
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
onDismiss={hideModal}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.modalTitle,
|
||||
{ color: theme.textColorPrimary },
|
||||
]}
|
||||
>
|
||||
Edit info
|
||||
</Text>
|
||||
<TextInput
|
||||
placeholder={`Title: ${info.novelName}`}
|
||||
style={{ fontSize: 14 }}
|
||||
numberOfLines={1}
|
||||
mode="outlined"
|
||||
theme={textInputTheme}
|
||||
onChangeText={(text) =>
|
||||
setInfo({ ...info, novelName: text })
|
||||
}
|
||||
dense
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={`Author: ${info.author}`}
|
||||
style={{ fontSize: 14 }}
|
||||
numberOfLines={1}
|
||||
mode="outlined"
|
||||
theme={textInputTheme}
|
||||
onChangeText={(text) => setInfo({ ...info, author: text })}
|
||||
dense
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={`Description: ${info.novelSummary.substring(
|
||||
0,
|
||||
16
|
||||
)}...`}
|
||||
style={{ fontSize: 14 }}
|
||||
numberOfLines={1}
|
||||
mode="outlined"
|
||||
onChangeText={(text) =>
|
||||
setInfo({ ...info, novelSummary: text })
|
||||
}
|
||||
theme={textInputTheme}
|
||||
dense
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
placeholder={`Add Tag`}
|
||||
style={{ fontSize: 14 }}
|
||||
numberOfLines={1}
|
||||
mode="outlined"
|
||||
onChangeText={(text) => setTag(text)}
|
||||
onSubmitEditing={() => {
|
||||
setInfo({ ...info, genre: info.genre + "," + tag });
|
||||
setTag("");
|
||||
}}
|
||||
theme={textInputTheme}
|
||||
dense
|
||||
/>
|
||||
|
||||
{info.genre !== null && info.genre !== "" && (
|
||||
<FlatList
|
||||
contentContainerStyle={{ marginVertical: 8 }}
|
||||
horizontal
|
||||
data={info.genre.split(",")}
|
||||
keyExtractor={(item) => item}
|
||||
renderItem={({ item }) => (
|
||||
<GenreChip
|
||||
theme={theme}
|
||||
onPress={() => removeTag(item)}
|
||||
>
|
||||
{item}
|
||||
</GenreChip>
|
||||
)}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
)}
|
||||
<View style={{ flexDirection: "row-reverse" }}>
|
||||
<Button
|
||||
labelStyle={{
|
||||
color: theme.colorAccent,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={() => {
|
||||
updateNovelInfo(info, novel.novelId);
|
||||
hideModal();
|
||||
dispatch(setNovel({ ...novel, ...info }));
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
labelStyle={{
|
||||
color: theme.colorAccent,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={hideModal}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditInfoModal;
|
||||
|
||||
const GenreChip = ({ children, theme, onPress }) => (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
borderWidth: 1,
|
||||
borderRadius: 50,
|
||||
borderColor: theme.colorAccent,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 8,
|
||||
marginVertical: 4,
|
||||
marginRight: 8,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: theme.colorPrimary,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.colorAccent, textTransform: "capitalize" }}>
|
||||
{children}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="close-circle"
|
||||
color={theme.colorAccent}
|
||||
size={16}
|
||||
onPress={onPress}
|
||||
style={{ marginLeft: 4 }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
margin: 30,
|
||||
padding: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
marginBottom: 16,
|
||||
},
|
||||
errorText: {
|
||||
color: "#FF0033",
|
||||
paddingTop: 8,
|
||||
},
|
||||
genreChip: {},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from "react";
|
||||
import { Text, StyleSheet } from "react-native";
|
||||
|
||||
export const GenreChip = ({ children, theme }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{
|
||||
color: theme.colorAccent,
|
||||
borderColor: theme.colorAccent,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
genreChip: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 10,
|
||||
marginHorizontal: 2,
|
||||
fontSize: 13,
|
||||
borderWidth: 1,
|
||||
borderRadius: 50,
|
||||
},
|
||||
});
|
||||
@@ -1,16 +1,7 @@
|
||||
import React from "react";
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
ImageBackground,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Pressable,
|
||||
} from "react-native";
|
||||
import { FlatList, StyleSheet, Text, View, Pressable } from "react-native";
|
||||
|
||||
import { Chip, IconButton } from "react-native-paper";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { IconButton } from "react-native-paper";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import easeGradient from "react-native-easing-gradient";
|
||||
import FastImage from "react-native-fast-image";
|
||||
@@ -66,10 +57,7 @@ const NovelAuthor = ({ theme, children }) => (
|
||||
|
||||
const NovelInfo = ({ theme, children }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.novelInfo,
|
||||
{ color: theme.textColorSecondary, textTransform: "capitalize" },
|
||||
]}
|
||||
style={[styles.novelInfo, { color: theme.textColorSecondary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{children}
|
||||
@@ -241,5 +229,6 @@ const styles = StyleSheet.create({
|
||||
fontSize: 13,
|
||||
borderWidth: 1,
|
||||
borderRadius: 50,
|
||||
textTransform: "capitalize",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,9 +53,32 @@ const NovelInfoHeader = ({
|
||||
|
||||
return (
|
||||
<View>
|
||||
<CoverImage source={{ uri: item.novelCover }} theme={theme}>
|
||||
<CoverImage
|
||||
source={{
|
||||
uri: loading
|
||||
? item.novelCover && !item.novelCover.startsWith("/")
|
||||
? item.novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true"
|
||||
: novel.novelCover && !novel.novelCover.startsWith("/")
|
||||
? novel.novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
theme={theme}
|
||||
>
|
||||
<NovelInfoContainer>
|
||||
<NovelThumbnail source={{ uri: item.novelCover }} />
|
||||
<NovelThumbnail
|
||||
source={{
|
||||
uri: loading
|
||||
? item.novelCover &&
|
||||
!item.novelCover.startsWith("/")
|
||||
? item.novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true"
|
||||
: novel.novelCover &&
|
||||
!novel.novelCover.startsWith("/")
|
||||
? novel.novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
/>
|
||||
<View style={styles.novelDetails}>
|
||||
<NovelTitle
|
||||
theme={theme}
|
||||
@@ -76,10 +99,10 @@ const NovelInfoHeader = ({
|
||||
{!loading && (
|
||||
<>
|
||||
<NovelAuthor theme={theme}>
|
||||
{novel.author ?? "Unknown author"}
|
||||
{novel.author || "Unknown author"}
|
||||
</NovelAuthor>
|
||||
<NovelInfo theme={theme}>
|
||||
{novel.status ?? "Unknown status"}
|
||||
{novel.status || "Unknown status"}
|
||||
</NovelInfo>
|
||||
<NovelInfo theme={theme}>
|
||||
{novel.source}
|
||||
|
||||
@@ -28,7 +28,12 @@ import {
|
||||
const Updates = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { updates, loading } = useSelector((state) => state.updatesReducer);
|
||||
const {
|
||||
updates,
|
||||
lastUpdateTime = null,
|
||||
showLastUpdateTime = true,
|
||||
loading,
|
||||
} = useSelector((state) => state.updatesReducer);
|
||||
const { downloadQueue } = useSelector((state) => state.downloadsReducer);
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
@@ -73,27 +78,25 @@ const Updates = ({ navigation }) => {
|
||||
[]
|
||||
);
|
||||
|
||||
const downloadChapter = (
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterId
|
||||
) =>
|
||||
dispatch(
|
||||
downloadChapterAction(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterId
|
||||
)
|
||||
);
|
||||
const downloadChapter = useCallback(
|
||||
(sourceId, novelUrl, chapterUrl, chapterName, chapterId) => {
|
||||
dispatch(
|
||||
downloadChapterAction(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterId
|
||||
)
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const deleteChapter = (chapterId, chapterName) => {
|
||||
const deleteChapter = useCallback((chapterId, chapterName) => {
|
||||
dispatch(deleteChapterAction(chapterId, chapterName));
|
||||
dispatch(getUpdatesAction());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<UpdatesItem
|
||||
@@ -173,6 +176,18 @@ const Updates = ({ navigation }) => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{showLastUpdateTime && lastUpdateTime && (
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 8,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Library last updated: {moment(lastUpdateTime).fromNow()}
|
||||
</Text>
|
||||
)}
|
||||
<FlatList
|
||||
contentContainerStyle={styles.flatList}
|
||||
data={searchText ? searchResults : updates}
|
||||
@@ -208,5 +223,5 @@ export default Updates;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
flatList: { flexGrow: 1, paddingVertical: 8 },
|
||||
flatList: { flexGrow: 1, paddingBottom: 8 },
|
||||
});
|
||||
|
||||
@@ -33,9 +33,10 @@ export const DownloadButton = ({
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
>
|
||||
<Menu.Item
|
||||
onPress={() =>
|
||||
deleteChapter(chapter.chapterId, chapter.chapterName)
|
||||
}
|
||||
onPress={() => {
|
||||
deleteChapter(chapter.chapterId, chapter.chapterName);
|
||||
hideDeleteChapterMenu();
|
||||
}}
|
||||
title="Delete"
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
|
||||
@@ -26,7 +26,13 @@ const UpdateCard = ({
|
||||
<>
|
||||
<Pressable onPress={onPressCover}>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
source={{
|
||||
uri:
|
||||
item.novelCover &&
|
||||
!item.novelCover.startsWith("/")
|
||||
? item.novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
style={styles.updateIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
import { StorageAccessFramework } from "expo-file-system";
|
||||
|
||||
import { getLibrary } from "../database/queries/LibraryQueries";
|
||||
import { restoreLibrary } from "../database/queries/NovelQueries";
|
||||
import { showToast } from "../hooks/showToast";
|
||||
|
||||
import * as Notifications from "expo-notifications";
|
||||
import BackgroundService from "react-native-background-actions";
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => {
|
||||
@@ -18,34 +15,6 @@ Notifications.setNotificationHandler({
|
||||
},
|
||||
});
|
||||
|
||||
export const createBackup = async () => {
|
||||
const novels = await getLibrary();
|
||||
|
||||
const permissions =
|
||||
await StorageAccessFramework.requestDirectoryPermissionsAsync();
|
||||
|
||||
if (!permissions.granted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = permissions.directoryUri;
|
||||
|
||||
if (uri) {
|
||||
const fileUri = await StorageAccessFramework.createFileAsync(
|
||||
uri,
|
||||
"backup",
|
||||
"application/json"
|
||||
);
|
||||
|
||||
await StorageAccessFramework.writeAsStringAsync(
|
||||
fileUri,
|
||||
JSON.stringify(novels)
|
||||
);
|
||||
|
||||
showToast("Backup created");
|
||||
}
|
||||
};
|
||||
|
||||
export const restoreBackup = async () => {
|
||||
const backup = await DocumentPicker.getDocumentAsync();
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
const getLibraryNovelsQuery = `SELECT * FROM novels WHERE followed = 1`;
|
||||
|
||||
const getLibraryNovels = () => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getLibraryNovelsQuery,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getLibraryChaptersQuery = `SELECT chapters.* FROM chapters JOIN novels ON chapters.novelId = novels.novelId WHERE novels.followed = 1`;
|
||||
|
||||
const getLibraryChapters = () => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getLibraryChaptersQuery,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getLibraryDownloadsQuery = `SELECT downloads.* FROM downloads JOIN chapters ON downloads.downloadChapterId = chapters.chapterId JOIN novels ON chapters.novelId = novels.novelId WHERE novels.followed = 1`;
|
||||
|
||||
const getLibraryDownloads = () => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getLibraryDownloadsQuery,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const restoreNovelsQuery = `INSERT OR REPLACE INTO novels (novelId, novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed, unread) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
const restoreNovels = async (novel) => {
|
||||
db.transaction((tx) =>
|
||||
tx.executeSql(
|
||||
restoreNovelsQuery,
|
||||
[
|
||||
novel.novelId,
|
||||
novel.novelUrl,
|
||||
novel.sourceUrl,
|
||||
novel.sourceId,
|
||||
novel.source,
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelSummary,
|
||||
novel.author,
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
novel.followed,
|
||||
novel.unread,
|
||||
],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const restoreChaptersQuery = `INSERT OR IGNORE INTO chapters (novelId, chapterId, chapterUrl, chapterName, releaseDate, bookmark, \`read\`, downloaded) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
const restoreChapters = async (chapter) => {
|
||||
const {
|
||||
novelId,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
releaseDate,
|
||||
bookmark,
|
||||
read,
|
||||
downloaded,
|
||||
} = chapter;
|
||||
|
||||
db.transaction((tx) =>
|
||||
tx.executeSql(
|
||||
restoreChaptersQuery,
|
||||
[
|
||||
novelId,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
releaseDate,
|
||||
bookmark,
|
||||
read,
|
||||
downloaded,
|
||||
],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const restoreDownloadsQuery = `INSERT OR IGNORE INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)`;
|
||||
|
||||
const restoreDownloads = async (chapter) => {
|
||||
const { downloadChapterId, chapterName, chapterText } = chapter;
|
||||
|
||||
db.transaction((tx) =>
|
||||
tx.executeSql(
|
||||
restoreDownloadsQuery,
|
||||
[downloadChapterId, chapterName, chapterText],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
getLibraryNovels,
|
||||
getLibraryChapters,
|
||||
getLibraryDownloads,
|
||||
restoreNovels,
|
||||
restoreChapters,
|
||||
restoreDownloads,
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
import { StorageAccessFramework } from "expo-file-system";
|
||||
import moment from "moment";
|
||||
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
import {
|
||||
getLibraryChapters,
|
||||
getLibraryDownloads,
|
||||
getLibraryNovels,
|
||||
restoreChapters,
|
||||
restoreDownloads,
|
||||
restoreNovels,
|
||||
} from "./BackupQueries";
|
||||
|
||||
const createFullBackup = async () => {
|
||||
const novels = await getLibraryNovels();
|
||||
const chapters = await getLibraryChapters();
|
||||
const downloads = await getLibraryDownloads();
|
||||
|
||||
const backup = { novels, chapters, downloads };
|
||||
|
||||
/**
|
||||
* Request permissions
|
||||
*/
|
||||
const permissions =
|
||||
await StorageAccessFramework.requestDirectoryPermissionsAsync();
|
||||
if (!permissions.granted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = permissions.directoryUri;
|
||||
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
|
||||
const fileName = "lnreader_backup_" + date;
|
||||
|
||||
if (uri) {
|
||||
const fileUri = await StorageAccessFramework.createFileAsync(
|
||||
uri,
|
||||
fileName,
|
||||
"application/json"
|
||||
);
|
||||
|
||||
await StorageAccessFramework.writeAsStringAsync(
|
||||
fileUri,
|
||||
JSON.stringify(backup)
|
||||
);
|
||||
|
||||
showToast("Backup created.");
|
||||
}
|
||||
};
|
||||
|
||||
const restoreFullBackup = async () => {
|
||||
const backup = await DocumentPicker.getDocumentAsync();
|
||||
|
||||
if (backup.uri) {
|
||||
let data = await StorageAccessFramework.readAsStringAsync(backup.uri);
|
||||
data = JSON.parse(data);
|
||||
|
||||
const { novels, chapters, downloads } = data;
|
||||
|
||||
novels.map((novel) => restoreNovels(novel));
|
||||
chapters.map((chapter) => restoreChapters(chapter));
|
||||
downloads.map((chapter) => restoreDownloads(chapter));
|
||||
}
|
||||
|
||||
showToast("Backup restored.");
|
||||
};
|
||||
|
||||
export { createFullBackup, restoreFullBackup };
|
||||
@@ -127,7 +127,7 @@ export const updateAllNovels = async (updateSettings) => {
|
||||
const options = {
|
||||
taskName: "Library Update",
|
||||
taskTitle: "Updating library",
|
||||
taskDesc: "0/" + libraryNovels.length,
|
||||
taskDesc: "(0/" + libraryNovels.length + ")",
|
||||
taskIcon: {
|
||||
name: "notification_icon",
|
||||
type: "drawable",
|
||||
@@ -136,6 +136,7 @@ export const updateAllNovels = async (updateSettings) => {
|
||||
parameters: {
|
||||
delay: 1000,
|
||||
},
|
||||
linkingURI: "lnreader://updates",
|
||||
progressBar: {
|
||||
max: libraryNovels.length,
|
||||
value: 0,
|
||||
@@ -163,7 +164,12 @@ export const updateAllNovels = async (updateSettings) => {
|
||||
// console.log(libraryNovels[i].novelName + " Updated");
|
||||
await BackgroundService.updateNotification({
|
||||
taskTitle: libraryNovels[i].novelName,
|
||||
taskDesc: i + 1 + "/" + libraryNovels.length,
|
||||
taskDesc:
|
||||
"(" +
|
||||
(i + 1) +
|
||||
"/" +
|
||||
libraryNovels.length +
|
||||
")",
|
||||
progressBar: {
|
||||
max: libraryNovels.length,
|
||||
value: i + 1,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
export const fonts = [
|
||||
{ fontFamily: "", name: "Original" },
|
||||
{ fontFamily: "lora", name: "Lora" },
|
||||
{ fontFamily: "nunito", name: "Nunito" },
|
||||
{ fontFamily: "noto-sans", name: "Noto Sans" },
|
||||
{ fontFamily: "open-sans", name: "Open Sans" },
|
||||
{ fontFamily: "arbutus-slab", name: "Arbutus Slab" },
|
||||
{ fontFamily: "domine", name: "Domine" },
|
||||
{ fontFamily: "lato", name: "Lato" },
|
||||
{ fontFamily: "noto-sans", name: "Noto Sans" },
|
||||
{ fontFamily: "open-sans", name: "Open Sans" },
|
||||
{ fontFamily: "pt-serif", name: "PT Serif" },
|
||||
{ fontFamily: "productsans-regular", name: "Product Sans" },
|
||||
];
|
||||
|
||||
export const dateFormat = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useWindowDimensions } from "react-native";
|
||||
|
||||
export const parseChapterNumber = (chapterName) => {
|
||||
chapterName = chapterName.toLowerCase();
|
||||
chapterName = chapterName.replace(/volume (\d+)/, "");
|
||||
chapterName = chapterName.replace(/volume (\d+)|v(\d+)/, "");
|
||||
|
||||
const basic = chapterName.match(/[ch]i (\d+)/);
|
||||
const occurrence = chapterName.match(/\d+/);
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import cheerio from "react-native-cheerio";
|
||||
|
||||
const sourceId = 59;
|
||||
const sourceName = "ArNovel";
|
||||
|
||||
const baseUrl = "https://arnovel.me/";
|
||||
|
||||
const popularNovels = async (page) => {
|
||||
let totalPages = 21;
|
||||
let url = baseUrl + "novels/page/" + page;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
$(".page-item-detail").each(function () {
|
||||
const novelName = $(this).find(".h5 > a").text();
|
||||
const novelCover =
|
||||
"https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true";
|
||||
|
||||
let novelUrl = $(this).find(".h5 > a").attr("href").split("/")[4];
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return { totalPages, novels };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async (novelUrl) => {
|
||||
const url = `${baseUrl}novel/${novelUrl}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novel = { sourceId, sourceName, url, novelUrl };
|
||||
|
||||
novel.novelName = $(".post-title > h1")
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
|
||||
novel.novelCover =
|
||||
"https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true";
|
||||
|
||||
$(".post-content_item").each(function () {
|
||||
detailName = $(this)
|
||||
.find(".summary-heading > h5")
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
detail = $(this)
|
||||
.find(".summary-content")
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
|
||||
switch (detailName) {
|
||||
case "التصنيفات":
|
||||
novel.genre = detail.trim().replace(/[\t\n]/g, ",");
|
||||
break;
|
||||
case "المؤلف (ين)":
|
||||
novel.author = detail.trim();
|
||||
break;
|
||||
case "الحالة":
|
||||
novel.status = detail.trim();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$(".description-summary > div.summary__content").find("em").remove();
|
||||
|
||||
novel.summary = $(".description-summary > div.summary__content")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
const novelId = $("#manga-chapters-holder").attr("data-id");
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("action", "manga_get_chapters");
|
||||
formData.append("manga", novelId);
|
||||
|
||||
const data = await fetch("https://www.arnovel.me/wp-admin/admin-ajax.php", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const text = await data.text();
|
||||
|
||||
$ = cheerio.load(text);
|
||||
|
||||
$(".wp-manga-chapter").each(function () {
|
||||
chapterName = $(this)
|
||||
.find("a")
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
|
||||
releaseDate = $(this).find("span").text().trim();
|
||||
|
||||
chapterUrl = $(this).find("a").attr("href").split("/");
|
||||
|
||||
chapterUrl[6]
|
||||
? (chapterUrl = chapterUrl[5] + "/" + chapterUrl[6])
|
||||
: (chapterUrl = chapterUrl[5]);
|
||||
|
||||
novelChapters.push({ chapterName, releaseDate, chapterUrl });
|
||||
});
|
||||
|
||||
novel.chapters = novelChapters.reverse();
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = `${baseUrl}novel/${novelUrl}/${chapterUrl}/`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
const chapterName = $("h1#chapter-heading").text();
|
||||
const chapterText = $(".reading-content").html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async (searchTerm) => {
|
||||
const url = `${baseUrl}?s=${searchTerm}&post_type=wp-manga`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
$(".c-tabs-item__content").each(function () {
|
||||
const novelName = $(this).find(".h4 > a").text();
|
||||
const novelCover =
|
||||
"https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true";
|
||||
|
||||
let novelUrl = $(this).find(".h4 > a").attr("href").split("/")[4];
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const ArNovelScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default ArNovelScraper;
|
||||
@@ -117,13 +117,7 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
$ = cheerio.load(body);
|
||||
|
||||
const chapterName = $(".read-section h3").text();
|
||||
let chapterText = "";
|
||||
$(".read-section p").each(function (result) {
|
||||
chapterText += `${$(this).text()}\n`;
|
||||
});
|
||||
|
||||
const nextChapter = null;
|
||||
const prevChapter = null;
|
||||
let chapterText = $(".read-section").html();
|
||||
|
||||
const chapter = {
|
||||
sourceId: 52,
|
||||
@@ -131,8 +125,6 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
nextChapter,
|
||||
prevChapter,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import cheerio from "react-native-cheerio";
|
||||
import { htmlToText } from "../helpers/htmlToText";
|
||||
|
||||
const baseUrl = "https://boxnovel.com/novel";
|
||||
const searchUrl = "https://boxnovel.com/";
|
||||
@@ -134,34 +133,12 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
|
||||
const chapterName = $("div.text-left > h3").text();
|
||||
let chapterText = $(".reading-content").html();
|
||||
chapterText = htmlToText(chapterText);
|
||||
|
||||
let nextChapter = null;
|
||||
|
||||
if ($(".nav-next").length) {
|
||||
nextChapter = $(".nav-next")
|
||||
.find("a")
|
||||
.attr("href")
|
||||
.replace(baseUrl + "/" + novelUrl + "/", "");
|
||||
}
|
||||
|
||||
let prevChapter = null;
|
||||
|
||||
if ($(".nav-previous").length) {
|
||||
prevChapter = $(".nav-previous")
|
||||
.find("a")
|
||||
.attr("href")
|
||||
.replace(baseUrl + "/" + novelUrl + "/", "");
|
||||
}
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
nextChapter,
|
||||
prevChapter,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import cheerio from "react-native-cheerio";
|
||||
import { htmlToText } from "../helpers/htmlToText";
|
||||
|
||||
const sourceId = 27;
|
||||
|
||||
@@ -10,6 +9,7 @@ const baseUrl = "https://comrademao.com/";
|
||||
const popularNovels = async (page) => {
|
||||
let url = baseUrl + "novel/page/" + page;
|
||||
const totalPages = 111;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
@@ -17,13 +17,17 @@ const popularNovels = async (page) => {
|
||||
|
||||
let novels = [];
|
||||
|
||||
$(".newbox")
|
||||
.find("li")
|
||||
$("div.container")
|
||||
.find("section")
|
||||
.each(function () {
|
||||
const novelName = $(this).find("h3").text();
|
||||
const novelName = $(this)
|
||||
.find("div.columns > div:nth-child(2) > a")
|
||||
.text();
|
||||
const novelCover = $(this).find("img").attr("src");
|
||||
|
||||
let novelUrl = $(this).find("a").attr("href");
|
||||
let novelUrl = $(this)
|
||||
.find("div.columns > div:nth-child(2) > a")
|
||||
.attr("href");
|
||||
novelUrl = novelUrl.replace(baseUrl + "novel/", "");
|
||||
|
||||
const novel = {
|
||||
@@ -48,49 +52,40 @@ const parseNovelAndChapters = async (novelUrl) => {
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novel = {};
|
||||
let novel = { sourceId, sourceName, url, novelUrl };
|
||||
|
||||
novel.sourceId = 27;
|
||||
|
||||
novel.sourceName = sourceName;
|
||||
|
||||
novel.url = url;
|
||||
|
||||
novel.novelUrl = novelUrl;
|
||||
|
||||
novel.novelName = $(".booknav2 > h1").text();
|
||||
|
||||
novel.novelCover = $(".bookimg2 > img").attr("src");
|
||||
|
||||
novel.summary = $(".mybox")
|
||||
.find(".tabsnav > div > div.qustime > span")
|
||||
novel.novelName = $(
|
||||
"#NovelInfo > div > div.column.is-one-third.has-text-centered > p"
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
novel.genre = $(".booknav2 > p:nth-child(2)")
|
||||
novel.novelCover = $("img.attachment-post-thumbnail").attr("src");
|
||||
|
||||
novel.summary = $("#NovelInfo > div > div:nth-child(2) > p").text().trim();
|
||||
|
||||
novel.genre = $("#NovelInfo > p:nth-child(2)")
|
||||
.text()
|
||||
.replace(/Genre(s):|\s/g, "");
|
||||
|
||||
novel.status = $(".booknav2 > p:nth-child(5)")
|
||||
novel.status = $("#NovelInfo > p:nth-child(5)")
|
||||
.text()
|
||||
.replace(/Status:|\s/g, "")
|
||||
.trim();
|
||||
|
||||
novel.author = $(".booknav2 > p:nth-child(4)")
|
||||
novel.author = $("#NovelInfo > p:nth-child(4)")
|
||||
.text()
|
||||
.replace(/Publisher:|\s/g, "")
|
||||
.trim();
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
// $(".qustime")
|
||||
// .find("li")
|
||||
// $("table > tbody")
|
||||
// .find("tr")
|
||||
// .each(function () {
|
||||
// const releaseDate = $(this).find("small").text();
|
||||
// const releaseDate = $(this).find("span").text();
|
||||
|
||||
// $(this).find("small").remove();
|
||||
|
||||
// const chapterName = $(this).find("span").text();
|
||||
// const chapterName = $(this).find("a").text();
|
||||
// const chapterUrl = $(this).find("a").attr("href").split("/")[5];
|
||||
|
||||
// novelChapters.push({
|
||||
@@ -100,21 +95,24 @@ const parseNovelAndChapters = async (novelUrl) => {
|
||||
// });
|
||||
// });
|
||||
|
||||
$("small").remove();
|
||||
// $("small").remove();
|
||||
|
||||
let chapterUrlPrefix = $(".qustime")
|
||||
.find("li")
|
||||
let chapterUrlPrefix = $("table > tbody")
|
||||
.find("tr")
|
||||
.first()
|
||||
.find("a")
|
||||
.text()
|
||||
.split(" ");
|
||||
.attr("href")
|
||||
.slice(0, -1)
|
||||
.split("/")
|
||||
.pop()
|
||||
.split("-");
|
||||
|
||||
chapterUrlPrefix.pop();
|
||||
|
||||
chapterUrlPrefix = chapterUrlPrefix.join("-").toLowerCase();
|
||||
|
||||
let latestChapter = $(".qustime")
|
||||
.find("li")
|
||||
let latestChapter = $("table > tbody")
|
||||
.find("tr")
|
||||
.first()
|
||||
.find("a")
|
||||
.text()
|
||||
@@ -151,15 +149,7 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
|
||||
let chapterName = "";
|
||||
|
||||
let chapterText = $(".txtnav").html();
|
||||
chapterText = htmlToText(chapterText);
|
||||
|
||||
let nextChapter = null;
|
||||
|
||||
let prevChapter = null;
|
||||
|
||||
novelUrl = novelUrl;
|
||||
chapterUrl = chapterUrl;
|
||||
let chapterText = $("#content").html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
@@ -167,8 +157,6 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
nextChapter,
|
||||
prevChapter,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import cheerio from "react-native-cheerio";
|
||||
|
||||
const sourceId = 70;
|
||||
|
||||
const sourceName = "Divine Dao Library";
|
||||
|
||||
const baseUrl = "https://www.divinedaolibrary.com/";
|
||||
|
||||
const popularNovels = async (page) => {
|
||||
let url = baseUrl + "novels";
|
||||
const totalPages = 1;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
$("#main")
|
||||
.find("li")
|
||||
.each(function () {
|
||||
const novelName = $(this).find("a").text();
|
||||
const novelCover = null;
|
||||
const novelUrl = $(this).find(" a").attr("href");
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return { novels, totalPages };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async (novelUrl) => {
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novel = { sourceId, sourceName, url, novelUrl };
|
||||
|
||||
novel.novelName = $("h1.entry-title").text().trim();
|
||||
|
||||
novel.novelCover =
|
||||
$(".entry-content").find("img").attr("src") ||
|
||||
"https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true";
|
||||
|
||||
novel.summary = $("#main > article > div > p:nth-child(6)").text().trim();
|
||||
|
||||
novel.genre = null;
|
||||
|
||||
novel.status = null;
|
||||
|
||||
novel.author = $("#main > article > div > h3:nth-child(2)")
|
||||
.text()
|
||||
.replace(/Author:/g, "")
|
||||
.trim();
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
$("#main")
|
||||
.find("li > span > a")
|
||||
.each(function () {
|
||||
const chapterName = $(this).text().trim();
|
||||
const releaseDate = null;
|
||||
const chapterUrl = $(this).attr("href");
|
||||
|
||||
novelChapters.push({ chapterName, releaseDate, chapterUrl });
|
||||
});
|
||||
|
||||
novel.chapters = novelChapters;
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = chapterUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let chapterName = $(".entry-title").text().trim();
|
||||
|
||||
let chapterText = $(".entry-content").html();
|
||||
|
||||
if (!chapterText) {
|
||||
chapterText = $(".page-header").html();
|
||||
}
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async (searchTerm) => {
|
||||
let url = baseUrl + "novels";
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
$("#main")
|
||||
.find("li")
|
||||
.each(function () {
|
||||
const novelName = $(this).find("a").text();
|
||||
const novelCover = null;
|
||||
const novelUrl = $(this).find(" a").attr("href");
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
novels = novels.filter((novel) =>
|
||||
novel.novelName.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const DivineDaoLibraryScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default DivineDaoLibraryScraper;
|
||||
@@ -1,5 +1,4 @@
|
||||
import cheerio from "react-native-cheerio";
|
||||
import { htmlToText } from "../helpers/htmlToText";
|
||||
|
||||
const baseUrl = "https://fastnovel.net";
|
||||
const searchUrl = `https://fastnovel.net/search/`;
|
||||
@@ -115,35 +114,14 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
$ = cheerio.load(body);
|
||||
|
||||
const chapterName = $(".episode-name").text();
|
||||
|
||||
let chapterText = $("#chapter-body").html();
|
||||
chapterText = htmlToText(chapterText);
|
||||
|
||||
let nextChapter = null;
|
||||
|
||||
nextChapter = $("div.btn-group > a")
|
||||
.filter(function () {
|
||||
return $(this).text().trim() === "Next Chapter";
|
||||
})
|
||||
.attr("href")
|
||||
.replace("/", "");
|
||||
|
||||
let prevChapter = null;
|
||||
|
||||
prevChapter = $("div.btn-group > a")
|
||||
.filter(function () {
|
||||
return $(this).text().trim() === "Prev Chapter";
|
||||
})
|
||||
.attr("href")
|
||||
.replace("/", "");
|
||||
|
||||
const chapter = {
|
||||
sourceId: 3,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
nextChapter,
|
||||
prevChapter,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user