Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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.2"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Generated
+2
-2
@@ -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",
|
||||
|
||||
+1
-1
@@ -68,6 +68,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,73 @@
|
||||
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: 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: 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(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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -219,14 +253,15 @@ const styles = StyleSheet.create({
|
||||
fontFamily: "pt-sans-bold",
|
||||
fontSize: 14,
|
||||
padding: 8,
|
||||
textShadowColor: "rgba(0, 0, 0, 0.75)",
|
||||
textShadowOffset: { width: -1, height: 1 },
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
linearGradient: {
|
||||
bottom: -1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
opac: {
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 4,
|
||||
padding: 4.8,
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
},
|
||||
@@ -245,7 +280,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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -113,13 +113,26 @@ export const markChapterRead = async (chapterId) => {
|
||||
});
|
||||
};
|
||||
|
||||
const markChapterUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId = ?`;
|
||||
const markAllChaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE novelId = ?`;
|
||||
|
||||
export const markChapterUnread = async (chapterId) => {
|
||||
export const markAllChaptersRead = async (novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
markChapterUnreadQuery,
|
||||
[chapterId],
|
||||
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)
|
||||
);
|
||||
@@ -147,7 +160,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 +179,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 +269,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.2";
|
||||
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";
|
||||
@@ -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,7 +1,5 @@
|
||||
import { GET_UPDATES } from "./updates.types";
|
||||
|
||||
import store from "../store";
|
||||
|
||||
import { getUpdates } from "../../database/queries/UpdateQueries";
|
||||
import { updateAllNovels } from "../../services/updates";
|
||||
|
||||
|
||||
@@ -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,7 +60,7 @@ const BrowseSettings = ({ navigation }) => {
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar onBackAction={navigation.goBack} title="Sources" />
|
||||
<View>
|
||||
<ListSubHeader theme={theme}>Global Search</ListSubHeader>
|
||||
<List.SubHeader theme={theme}>Global Search</List.SubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
@@ -84,8 +94,8 @@ const BrowseSettings = ({ navigation }) => {
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<ListSubHeader theme={theme}>Discover</ListSubHeader>
|
||||
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Discover</List.SubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
@@ -107,7 +117,8 @@ const BrowseSettings = ({ navigation }) => {
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<ListSubHeader theme={theme}>Languages</ListSubHeader>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Languages</List.SubHeader>
|
||||
<FlatList
|
||||
data={languages}
|
||||
keyExtractor={(item) => item}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+211
-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,202 @@ 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%;
|
||||
}
|
||||
@font-face {
|
||||
font-family: ${reader.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${
|
||||
reader.fontFamily
|
||||
}.ttf");
|
||||
}
|
||||
|
||||
${reader.customCSS}
|
||||
</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 +518,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 +527,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 {
|
||||
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,376 +24,291 @@ 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 = () => (
|
||||
<View style={{ flex: 1, paddingVertical: 16 }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: "center",
|
||||
}}
|
||||
/> */}
|
||||
<View style={styles.readerSettingsContainer}>
|
||||
<ReaderSettingTitle title="Text Size" />
|
||||
<Slider
|
||||
style={{
|
||||
width: Dimensions.get("window").width,
|
||||
height: 40,
|
||||
}}
|
||||
value={reader.textSize}
|
||||
minimumValue={12}
|
||||
maximumValue={20}
|
||||
step={1}
|
||||
minimumTrackTintColor={theme.colorAccent}
|
||||
maximumTrackTintColor="#000000"
|
||||
thumbTintColor={theme.colorAccent}
|
||||
onSlidingComplete={(value) =>
|
||||
dispatch(setReaderSettings("textSize", value))
|
||||
}
|
||||
/>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Reader Theme" />
|
||||
<Row>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) => {
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"theme",
|
||||
value ?? reader.theme
|
||||
)
|
||||
);
|
||||
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,
|
||||
>
|
||||
Text size
|
||||
</Text>
|
||||
<Slider
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 40,
|
||||
}}
|
||||
value={reader.textSize}
|
||||
minimumValue={12}
|
||||
maximumValue={20}
|
||||
step={1}
|
||||
minimumTrackTintColor={theme.colorAccent}
|
||||
maximumTrackTintColor="#000000"
|
||||
thumbTintColor={theme.colorAccent}
|
||||
onSlidingComplete={(value) =>
|
||||
dispatch(setReaderSettings("textSize", value))
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ paddingLeft: 16, paddingRight: 8 }}>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<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
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* <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,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Text Align" />
|
||||
<Row>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) =>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Text align
|
||||
</Text>
|
||||
<Row>
|
||||
{textAlignments.map((item) => (
|
||||
<ToggleButton
|
||||
key={item.value}
|
||||
selected={item.value === reader.textAlign}
|
||||
icon={item.icon}
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textAlign",
|
||||
value ?? reader.textAlign
|
||||
item.value
|
||||
)
|
||||
)
|
||||
}
|
||||
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" />
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.padding <= 0 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"padding",
|
||||
reader.padding - 1
|
||||
)
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Padding
|
||||
</Text>
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.padding <= 0 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"padding",
|
||||
reader.padding - 1
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{`${reader.padding}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.padding >= 10 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"padding",
|
||||
reader.padding + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Line Height" />
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={
|
||||
reader.lineHeight <= 1.3 ? true : false
|
||||
}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"lineHeight",
|
||||
reader.lineHeight - 0.1
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{`${Math.round(reader.lineHeight * 10) / 10}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.lineHeight >= 2 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"lineHeight",
|
||||
reader.lineHeight + 0.1
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Pressable
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
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>
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{fonts.map((font) => (
|
||||
<Menu.Item
|
||||
key={font.fontFamily}
|
||||
{`${reader.padding}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.padding >= 10 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"padding",
|
||||
reader.padding + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Line height
|
||||
</Text>
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.lineHeight <= 1.3 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"lineHeight",
|
||||
reader.lineHeight - 0.1
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{`${Math.round(reader.lineHeight * 10) / 10}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.lineHeight >= 2 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"lineHeight",
|
||||
reader.lineHeight + 0.1
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
marginRight: 16,
|
||||
}}
|
||||
>
|
||||
Font style
|
||||
</Text>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
contentContainerStyle={{
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{fonts.map((font) => (
|
||||
<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(
|
||||
@@ -392,93 +317,171 @@ const ReaderSheet = ({
|
||||
)
|
||||
)
|
||||
}
|
||||
icon={
|
||||
font.fontFamily === reader.fontFamily &&
|
||||
"check"
|
||||
}
|
||||
title={font.name}
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showScrollPercentage",
|
||||
!showScrollPercentage
|
||||
)
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontFamily: font.fontFamily,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{font.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</Row>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const SecondRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Pressable
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"useWebViewForChapter",
|
||||
!useWebViewForChapter
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
Use WebView
|
||||
</Text>
|
||||
<Switch
|
||||
value={useWebViewForChapter}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"useWebViewForChapter",
|
||||
!useWebViewForChapter
|
||||
)
|
||||
}
|
||||
>
|
||||
<ReaderSettingTitle title="Show Progress Percentage" />
|
||||
<Switch
|
||||
value={showScrollPercentage}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showScrollPercentage",
|
||||
!showScrollPercentage
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
)
|
||||
}
|
||||
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",
|
||||
!showScrollPercentage
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
{!useWebViewForChapter && (
|
||||
<Pressable
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings("textSelectable", !selectText))
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
onPress={() =>
|
||||
>
|
||||
Select text
|
||||
</Text>
|
||||
<Switch
|
||||
value={selectText}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings("textSelectable", !selectText)
|
||||
)
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<ReaderSettingTitle title="Select Text " />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
(Justify does not work with selectable text)
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={selectText}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"textSelectable",
|
||||
!selectText
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
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]}
|
||||
>
|
||||
<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 +505,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.2 (17/07/21 08:00 AM)"
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader/releases/tag/v1.1.2"
|
||||
)
|
||||
}
|
||||
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,7 @@ import {
|
||||
} from "../../redux/settings/settings.actions";
|
||||
import ColorPickerModal from "../../components/ColorPickerModal";
|
||||
import SwitchSetting from "../../components/Switch/Switch";
|
||||
import { ScrollView } from "react-native";
|
||||
|
||||
const GenralSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -31,14 +27,20 @@ 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 displayModeLabel = (displayMode) => {
|
||||
const label = {
|
||||
0: "Compact Grid",
|
||||
1: "Comfortable Grid",
|
||||
2: "List",
|
||||
3: "No Title Grid",
|
||||
};
|
||||
|
||||
return label[displayMode] ?? "Compact Grid";
|
||||
@@ -93,86 +95,128 @@ 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}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+172
-102
@@ -1,12 +1,24 @@
|
||||
import React, { useState } from "react";
|
||||
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
import { ToggleButton } from "react-native-paper";
|
||||
import {
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Button } from "react-native-paper";
|
||||
import WebView from "react-native-webview";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
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";
|
||||
@@ -22,13 +34,6 @@ const ReaderSettings = ({ navigation }) => {
|
||||
|
||||
const reader = useReaderSettings();
|
||||
|
||||
const textColor = {
|
||||
1: "rgba(255,255,255,0.7)",
|
||||
2: "#000000",
|
||||
3: "#000000",
|
||||
4: "#CCCCCC",
|
||||
};
|
||||
|
||||
/**
|
||||
* Reader Background Color Modal
|
||||
*/
|
||||
@@ -81,54 +86,77 @@ const ReaderSettings = ({ navigation }) => {
|
||||
{ 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: "center", icon: "format-align-center" },
|
||||
{ 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={{ height: 360 }}>
|
||||
<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>
|
||||
@font-face {
|
||||
font-family: ${reader.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${reader.fontFamily}.ttf");
|
||||
}
|
||||
body {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: ${reader.padding}%;
|
||||
padding-right: ${reader.padding}%;
|
||||
font-family: ${reader.fontFamily};
|
||||
text-align: ${reader.textAlign};
|
||||
line-height: ${reader.lineHeight};
|
||||
font-size: ${reader.textSize}px;
|
||||
color: ${reader.textColor};
|
||||
}
|
||||
${reader.customCSS}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<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 +165,50 @@ const ReaderSettings = ({ navigation }) => {
|
||||
>
|
||||
Preset Themes
|
||||
</Text>
|
||||
<Row>
|
||||
<ToggleButton.Row
|
||||
onValueChange={onValueChange}
|
||||
value={reader.theme}
|
||||
>
|
||||
<ScrollView
|
||||
style={{ marginLeft: 16 }}
|
||||
horizontal={true}
|
||||
>
|
||||
<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,12 +216,9 @@ const ReaderSettings = ({ navigation }) => {
|
||||
}
|
||||
onPress={showReaderTextColorModal}
|
||||
theme={theme}
|
||||
iconColor={
|
||||
reader.textColor || readerTextColor(reader.theme)
|
||||
}
|
||||
right="circle"
|
||||
/>
|
||||
<ListSubHeader theme={theme}>Text</ListSubHeader>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Text</List.SubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressableListItem}
|
||||
@@ -211,33 +242,72 @@ 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"
|
||||
modalVisible={readerBackgroundColorModal}
|
||||
|
||||
@@ -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 === 1 && <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}
|
||||
|
||||
@@ -73,27 +73,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
|
||||
@@ -208,5 +206,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;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import cheerio from "react-native-cheerio";
|
||||
|
||||
import { htmlToText } from "../helpers/htmlToText";
|
||||
|
||||
const baseUrl = "https://foxaholic.com/";
|
||||
|
||||
const popularNovels = async (page) => {
|
||||
@@ -38,8 +36,6 @@ const popularNovels = async (page) => {
|
||||
const parseNovelAndChapters = async (novelUrl) => {
|
||||
const url = `${baseUrl}novel/${novelUrl}`;
|
||||
|
||||
console.log(url);
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
@@ -97,8 +93,6 @@ const parseNovelAndChapters = async (novelUrl) => {
|
||||
|
||||
const novelId = $("#manga-chapters-holder").attr("data-id");
|
||||
|
||||
console.log(novelId);
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("action", "manga_get_chapters");
|
||||
formData.append("manga", novelId);
|
||||
@@ -111,7 +105,6 @@ const parseNovelAndChapters = async (novelUrl) => {
|
||||
}
|
||||
);
|
||||
const text = await data.text();
|
||||
console.log(text);
|
||||
|
||||
$ = cheerio.load(text);
|
||||
|
||||
@@ -148,34 +141,12 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
|
||||
const chapterName = $("h1#chapter-heading").text();
|
||||
let chapterText = $(".reading-content").html();
|
||||
chapterText = htmlToText(chapterText);
|
||||
|
||||
let nextChapter = null;
|
||||
|
||||
if ($(".nav-next").length) {
|
||||
nextChapter = $(".nav-next").find("a").attr("href").split("/");
|
||||
nextChapter[6]
|
||||
? (nextChapter = nextChapter[5] + "/" + nextChapter[6])
|
||||
: (nextChapter = nextChapter[5]);
|
||||
}
|
||||
|
||||
let prevChapter = null;
|
||||
|
||||
if ($(".nav-previous").length) {
|
||||
prevChapter = $(".nav-previous").find("a").attr("href").split("/");
|
||||
prevChapter[6]
|
||||
? (prevChapter = prevChapter[5] + "/" + prevChapter[6])
|
||||
: (prevChapter = prevChapter[5]);
|
||||
}
|
||||
|
||||
const chapter = {
|
||||
sourceId: 22,
|
||||
novelUrl,
|
||||
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://freewebnovel.com/";
|
||||
|
||||
@@ -40,8 +39,6 @@ const popularNovels = async (page) => {
|
||||
const parseNovelAndChapters = async (novelUrl) => {
|
||||
const url = `${baseUrl}${novelUrl.replace("/", "")}.html`;
|
||||
|
||||
console.log(url);
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
@@ -114,9 +111,6 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
let novelId = novelUrl.replace("/", "");
|
||||
|
||||
const url = `${baseUrl}${novelId}/${chapterUrl}.html`;
|
||||
console.log(novelUrl, chapterUrl);
|
||||
|
||||
console.log(url);
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
@@ -126,29 +120,6 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
let chapterName = $("h1.tit").text();
|
||||
|
||||
let chapterText = $("div.txt").html();
|
||||
chapterText = htmlToText(chapterText);
|
||||
|
||||
console.log(chapterText);
|
||||
|
||||
let nextChapter = null;
|
||||
|
||||
if ($("[title='Read Next chapter']").length) {
|
||||
nextChapter = $("[title='Read Next chapter']")
|
||||
.attr("href")
|
||||
.replace("/" + novelUrl + "/", "")
|
||||
.replace(".html", "");
|
||||
}
|
||||
|
||||
let prevChapter = null;
|
||||
|
||||
if ($("[title='Read Privious Chapter']").length) {
|
||||
prevChapter = $("[title='Read Privious Chapter']")
|
||||
.attr("href")
|
||||
.replace("/" + novelUrl + "/", "")
|
||||
.replace(".html", "");
|
||||
}
|
||||
|
||||
chapterUrl = chapterUrl;
|
||||
|
||||
const chapter = {
|
||||
sourceId: 13,
|
||||
@@ -156,8 +127,6 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
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