Compare commits

...

47 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 9b3d65dded refactor: Reorganize reader sheet 2021-08-11 09:53:18 +05:30
Rajarshee Chatterjee aa3a5c2d0c Minor fixes 2021-08-10 14:32:25 +05:30
Rajarshee Chatterjee 2aab5621a6 feat: Select novels in library 2021-08-10 13:34:19 +05:30
Rajarshee Chatterjee b4b7e6770d feat(source): Add Divine Dao Library
Closes LNReader/lnreader-sources#54
2021-08-09 12:44:32 +05:30
Rajarshee Chatterjee 0ed65d64e4 fix: novelCover crash in list view 2021-08-09 07:26:04 +05:30
Rajarshee Chatterjee a7aaa7d230 feat: Custom CSS 2021-08-07 12:08:31 +05:30
Rajarshee Chatterjee e358b3753f feat(source): Add ranobehub 2021-08-07 08:34:08 +05:30
Rajarshee Chatterjee 780a67e1a6 Update README.md 2021-08-06 10:50:00 +05:30
Rajarshee Chatterjee 5b57e78486 Update README.md 2021-08-06 10:37:22 +05:30
Rajarshee Chatterjee 6430c11cae Update README.md 2021-08-06 10:31:09 +05:30
Rajarshee Chatterjee 322bfc224d Full backups (Closes #34) 2021-08-05 12:24:54 +05:30
Rajarshee Chatterjee a14d35f031 Minor fixes 2021-08-05 12:23:59 +05:30
Rajarshee Chatterjee f03b1703e2 fix(source): ArNovel (Closes #61)
Closes LNReader/lnreader-sources#52
2021-08-04 09:20:35 +05:30
Rajarshee Chatterjee 40d2052f16 Custom covers and open links in reader 2021-08-03 14:38:54 +05:30
Rajarshee Chatterjee 01814e92d0 Fix empty novelCover crash 2021-08-03 10:52:13 +05:30
Rajarshee Chatterjee dffc9c3b99 fix(source): LightNovelPub 2021-08-03 10:48:12 +05:30
Rajarshee Chatterjee 8bb3ffe82c fix(source): ComradeMao 2021-08-03 10:40:34 +05:30
Rajarshee Chatterjee c746081dee Minor fixes 2021-08-03 09:44:14 +05:30
Rajarshee Chatterjee d25fe85a64 Fix discord link 2021-08-02 16:52:32 +05:30
Rajarshee Chatterjee 92d9d53da7 Fix no novelCover crash 2021-07-31 13:32:00 +05:30
Rajarshee Chatterjee 17fcd96f16 Source fixes 2021-07-30 09:15:15 +05:30
Rajarshee Chatterjee 31bada1c1a Refactor 2021-07-29 20:49:21 +05:30
Rajarshee Chatterjee 605bd6a514 Add edit info modal 2021-07-29 11:17:13 +05:30
Rajarshee Chatterjee 01bac76bed Minor fixes 2021-07-29 09:35:55 +05:30
Rajarshee Chatterjee b5ed62d9f8 Fix global search and splashscreen 2021-07-28 17:47:23 +05:30
Rajarshee Chatterjee fd21cc6e85 Minor fixes 2021-07-28 12:14:38 +05:30
Rajarshee Chatterjee 40f1495cf1 Add webview option to support tables and original text style 2021-07-28 11:46:57 +05:30
Rajarshee Chatterjee 983b069dba Add Readwn.com source 2021-07-28 09:47:12 +05:30
Rajarshee Chatterjee e21101ca28 Fix build 2021-07-26 17:01:18 +05:30
Bread Genie d381e878bc docs(readme): Add badges (#60) 2021-07-26 16:59:43 +05:30
Rajarshee Chatterjee bcf5632668 Remove history from redux state and minor fixes 2021-07-26 08:24:59 +05:30
Bread Genie ac8bca7f03 feat(sources): Add ReaperScans Novels (#59) 2021-07-25 21:38:33 +05:30
Bread Genie 62a5e27b1e fix: Indonesia -> Indonesian (#58) 2021-07-25 19:19:08 +05:30
Rajarshee Chatterjee 7c5a9a407f Add new sources 2021-07-23 09:04:47 +05:30
Rajarshee Chatterjee d8d231cb90 Add source WLNUpdates 2021-07-22 09:08:40 +05:30
Rajarshee Chatterjee a5f90eaf8b Add new sources 2021-07-21 20:32:30 +05:30
Rajarshee Chatterjee ec50189ea4 Minor fixes 2021-07-21 08:34:45 +05:30
Rajarshee Chatterjee ccdccdb6bb Remove prevChapter and nextChapter 2021-07-20 08:38:55 +05:30
Bread Genie 059f440713 fix(source): nextChapter link undefined (#55) 2021-07-20 07:55:48 +05:30
Rajarshee Chatterjee b695f57ef4 Refactor 2021-07-19 09:32:48 +05:30
Rajarshee Chatterjee db86b707e7 Fix bookmark icon 2021-07-19 06:00:51 +05:30
Rajarshee Chatterjee 7db4e4b872 Minor fixes 2021-07-18 18:21:25 +05:30
Rajarshee Chatterjee a2c6e9fe58 Custom checkbox 2021-07-18 10:31:07 +05:30
Rajarshee Chatterjee 6c27993454 Remove [hr]() chapter content 2021-07-18 10:30:21 +05:30
Rajarshee Chatterjee 63de0a3794 Fix long chapter names (Closes #53) 2021-07-17 20:24:32 +05:30
Marcin Majewski 93fbcf1246 fix(syosetu): change oneshot chapter name (#52)
Changed oneshot chapter names to "Oneshot" to avoid too long chapters names destroying UI.
2021-07-17 19:55:24 +05:30
Rajarshee Chatterjee 50357f7d6f Release v1.1.2 2021-07-17 08:12:04 +05:30
151 changed files with 4490 additions and 3249 deletions
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

+2
View File
@@ -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();
}, []);
+33 -12
View File
@@ -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)
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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
View File
@@ -68,6 +68,6 @@
"arrowParens": "always"
},
"private": true,
"name": "lnreader-eject",
"name": "lnreader",
"version": "1.0.0"
}
+5 -123
View File
@@ -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 -4
View File
@@ -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",
}}
/>
+73
View File
@@ -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",
},
});
+72
View File
@@ -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>
);
+75
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
};
+48 -13
View File
@@ -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,
+4 -2
View File
@@ -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,
},
});
+5 -5
View File
@@ -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",
},
});
+23 -15
View File
@@ -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
+1 -1
View File
@@ -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(
+47
View File
@@ -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;
}
};
-2
View File
@@ -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
)`;
+1 -1
View File
@@ -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, "");
+3 -3
View File
@@ -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 = () => {
+38 -27
View File
@@ -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}
+5 -1
View File
@@ -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() || {};
+1
View File
@@ -31,6 +31,7 @@ export const resumeDownloads = (chapters) => async (dispatch) => {
parameters: {
delay: 1000,
},
linkingURI: "lnreader://downloads",
progressBar: {
max: chapters.length,
value: 0,
-75
View File
@@ -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);
}
};
-35
View File
@@ -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;
-5
View File
@@ -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";
+1
View File
@@ -323,6 +323,7 @@ export const downloadAllChaptersAction =
type: "drawable",
},
color: "#00adb5",
linkingURI: "lnreader://downloads/redirect",
parameters: {
delay: 1000,
},
+16 -13
View File
@@ -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,
}
-2
View File
@@ -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,
-2
View File
@@ -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";
-168
View File
@@ -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,
},
});
+18 -7
View File
@@ -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}
+13 -8
View File
@@ -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
View File
@@ -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,
},
-257
View File
@@ -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,
+24 -22
View File
@@ -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}
+433 -422
View File
@@ -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>
`,
}}
/>
);
+2 -6
View File
@@ -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 -8
View File
@@ -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}
/>
);
+59 -14
View File
@@ -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>
+14 -4
View File
@@ -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"
+5 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
};
+5 -55
View File
@@ -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}
+19 -14
View File
@@ -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>
</>
);
+124 -80
View File
@@ -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
View File
@@ -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
View File
@@ -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}
+6 -6
View File
@@ -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={() =>
+8 -13
View File
@@ -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
+15 -34
View File
@@ -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}
/>
));
};
+10 -13
View File
@@ -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>
);
+39
View File
@@ -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,
},
});
+15 -33
View File
@@ -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
View File
@@ -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 -50
View File
@@ -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",
},
});
+25 -2
View File
@@ -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}
+17 -19
View File
@@ -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>
-31
View File
@@ -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();
+132
View File
@@ -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,
};
+71
View File
@@ -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 };
+8 -2
View File
@@ -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,
+4 -3
View File
@@ -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 = {
+1 -1
View File
@@ -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+/);
+186
View File
@@ -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;
+1 -9
View File
@@ -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;
-23
View File
@@ -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;
+35 -47
View File
@@ -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;
+152
View File
@@ -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 -23
View File
@@ -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;
-29
View File
@@ -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;
-31
View File
@@ -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