Compare commits

..

39 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 8084cdbffb feat(source): Add ArthurScans
LNReader/lnreader-sources#71
2021-09-23 11:31:45 +05:30
Rajarshee Chatterjee dc9529c322 feat(source): Add ReaperScans Br
LNReader/lnreader-sources#71
2021-09-23 10:40:26 +05:30
Rajarshee Chatterjee 91168a335b Minor fixes 2021-09-23 10:39:08 +05:30
Rajarshee Chatterjee a6b3eb9350 Add text to speech play/pause 2021-09-21 09:51:01 +05:30
Rajarshee Chatterjee 0ce4e6b985 Increase bottom padding 2021-09-21 09:49:47 +05:30
Rajarshee Chatterjee 6684848a42 Fix FAB text color 2021-09-21 08:56:03 +05:30
Rajarshee Chatterjee 87d12cabb3 fix(source): RanobeHub 2021-09-21 08:51:40 +05:30
Rajarshee Chatterjee e01fc21a49 feat(source): Add Rain Of Snow
Closes LNReader/lnreader-sources#60
2021-09-15 10:09:09 +05:30
Rajarshee Chatterjee f0cfd9066b feat(source): Add Novel De Glace
Closes LNReader/lnreader-sources#57
2021-09-15 09:41:58 +05:30
Rajarshee Chatterjee c2cc6ea96b Option to clear updates tab 2021-09-15 09:26:24 +05:30
Rajarshee Chatterjee eeeea6b4e0 feat(source): Add Travis Translations
Closes (lnreader-sources#59)
2021-09-13 16:48:10 +05:30
Rajarshee Chatterjee ddce153b53 fix(source): ReaperScans 2021-09-13 16:01:11 +05:30
Rajarshee Chatterjee 38e45ca097 Downloaded only mode 2021-09-13 15:36:05 +05:30
Rajarshee Chatterjee a6a60607a7 Minor fixes 2021-09-12 08:46:58 +05:30
Rajarshee Chatterjee d3e772f0dd Add option to remove backdrop 2021-09-12 08:46:35 +05:30
Rajarshee Chatterjee 095d46681f fix(source): TuNovelaLigera 2021-09-12 08:46:06 +05:30
Rajarshee Chatterjee 3e4e967197 Enlarge cover 2021-09-11 21:42:54 +05:30
Rajarshee Chatterjee c5b4e7b902 Option to use fab instead of button 2021-09-11 10:13:41 +05:30
Rajarshee Chatterjee e9421a5fd0 fix: WPMangaStream (Closes #75) 2021-09-11 08:44:18 +05:30
Rajarshee Chatterjee afef0d63ce Edit status in edit info modal 2021-09-11 08:40:32 +05:30
Rajarshee Chatterjee e68a5c5f82 Minor fixes 2021-09-10 10:04:07 +05:30
Rajarshee Chatterjee ff1635b817 fix(source): Fix ReadLightNovel
Closes LNReader/LNReader-sources#65
2021-09-10 09:35:25 +05:30
Rajarshee Chatterjee f1ca221b91 Add option to only update ongoing novels (Closes #73) 2021-09-10 09:21:13 +05:30
Rajarshee Chatterjee 5a1106fc68 Add Text to Speech (Closes #22) 2021-08-31 10:01:23 +05:30
Rajarshee Chatterjee 3b49a1e516 Minor fixes 2021-08-31 08:03:33 +05:30
Rajarshee Chatterjee 85acbc8c4a Appearance settings 2021-08-29 11:02:24 +05:30
Rajarshee Chatterjee 66e0be5e84 Add option to turn off fullscreen (Closes #70) 2021-08-28 07:27:05 +05:30
Rajarshee Chatterjee 2234006272 Fix fullscreen not working on changing chapters 2021-08-28 07:00:48 +05:30
Rajarshee Chatterjee eca00ddcfa Experimental auto scroll (Closes #63) 2021-08-27 14:28:15 +05:30
Rajarshee Chatterjee 2328e55105 Show time and battery level (Closes #65) 2021-08-27 11:43:17 +05:30
Rajarshee Chatterjee b8a25b9172 Multiple select in chapters (Closes #69) 2021-08-26 21:50:05 +05:30
Rajarshee Chatterjee 9af8d55985 Add option to download unread chapters 2021-08-26 20:18:07 +05:30
Rajarshee Chatterjee 4777f453a4 Refactor themes 2021-08-25 11:15:40 +05:30
Rajarshee Chatterjee 4a0195762d Cross fade 2021-08-25 08:52:51 +05:30
Rajarshee Chatterjee e8785c2905 Minor UI adjustments 2021-08-24 19:26:11 +05:30
Rajarshee Chatterjee 1302cbde16 Draw content under navbar 2021-08-24 16:09:09 +05:30
Rajarshee Chatterjee 43d0adced8 feat(source): Add Mystical Series
Closes LNReader/LNReader-sources#61
2021-08-24 11:16:19 +05:30
Rajarshee Chatterjee 0c8b30cd72 feat(source): Add WuxiaWorld.Site
Closes LNReader/LNReader-sources#58
2021-08-24 11:08:58 +05:30
Rajarshee Chatterjee cc86fc7da4 feat(source): Add EpikNovel
Closes LNReader/LNReader-sources#55
2021-08-24 10:56:16 +05:30
72 changed files with 24558 additions and 22018 deletions
+88 -37
View File
@@ -1,38 +1,89 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.rajarsheechatterjee.LNReader">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="41.0.0"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/>
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="unspecified">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="lnreader"/>
<data android:scheme="com.rajarsheechatterjee.LNReader"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
</application>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.rajarsheechatterjee.LNReader">
<!-- Internet -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<meta-data
android:name="expo.modules.updates.EXPO_UPDATE_URL"
android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
<meta-data
android:name="expo.modules.updates.EXPO_SDK_VERSION"
android:value="41.0.0"/>
<meta-data
android:name="expo.modules.updates.ENABLED"
android:value="false"/>
<meta-data
android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH"
android:value="ALWAYS"/>
<meta-data
android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS"
android:value="0"/>
<meta-data
android:name="expo.modules.notifications.default_notification_icon"
android:resource="@drawable/notification_icon"/>
<meta-data
android:name="expo.modules.notifications.default_notification_color"
android:resource="@color/notification_icon_color"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.App.SplashScreen"
android:screenOrientation="unspecified">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="lnreader"/>
<data android:scheme="com.rajarsheechatterjee.LNReader"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
</application>
</manifest>
File diff suppressed because one or more lines are too long
@@ -3,8 +3,11 @@ import android.content.res.Configuration;
import android.content.Intent;
import android.os.Bundle;
import android.graphics.Color;
import android.view.WindowManager;
import android.os.Build;
import android.view.View;
import android.view.Window;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
@@ -26,19 +29,25 @@ public class MainActivity extends ReactActivity {
sendBroadcast(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// SplashScreen.show(...) has to be called after super.onCreate(...)
// Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(layoutParams);
}
super.onCreate(savedInstanceState);
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
@Override
protected void onCreate(Bundle savedInstanceState) {
// SplashScreen.show(...) has to be called after super.onCreate(...)
// Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(layoutParams);
}
}
Window w = getWindow();
w.setStatusBarColor(Color.TRANSPARENT);
w.setNavigationBarColor(Color.TRANSPARENT);
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
super.onCreate(savedInstanceState);
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
}
/**
@@ -13,6 +13,7 @@ public class BasePackageList {
new expo.modules.errorrecovery.ErrorRecoveryPackage(),
new expo.modules.filesystem.FileSystemPackage(),
new expo.modules.font.FontLoaderPackage(),
new expo.modules.haptics.HapticsPackage(),
new expo.modules.imageloader.ImageLoaderPackage(),
new expo.modules.keepawake.KeepAwakePackage(),
new expo.modules.lineargradient.LinearGradientPackage(),
+19798 -19776
View File
File diff suppressed because it is too large Load Diff
+72 -70
View File
@@ -1,72 +1,74 @@
{
"scripts": {
"start": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.13.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/slider": "3.0.3",
"@react-navigation/material-bottom-tabs": "^5.3.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "^41.0.1",
"expo-document-picker": "^9.1.2",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-notifications": "~0.11.6",
"expo-splash-screen": "^0.11.2",
"expo-sqlite": "~9.1.0",
"expo-updates": "~0.5.4",
"expo-web-browser": "~9.1.0",
"moment": "^2.29.1",
"qs": "^6.10.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "^0.64.2",
"react-native-background-actions": "^2.6.1",
"react-native-cheerio": "^1.0.0-rc.4",
"react-native-device-info": "^8.1.3",
"react-native-easing-gradient": "^0.1.2",
"react-native-fast-image": "^8.3.4",
"react-native-gesture-handler": "~1.10.2",
"react-native-navigation-bar-color": "^2.0.1",
"react-native-pager-view": "^5.1.10",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "^2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-swipe-gestures": "^1.0.5",
"react-native-tab-view": "^3.0.1",
"react-native-unimodules": "~0.13.3",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "^0.16.5",
"react-native-webview": "^11.6.2",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4"
},
"devDependencies": {
"@babel/core": "~7.9.0"
},
"prettier": {
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always"
},
"private": true,
"name": "lnreader",
"version": "1.0.0"
"scripts": {
"start": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.13.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/slider": "3.0.3",
"@react-navigation/material-bottom-tabs": "^5.3.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "^41.0.1",
"expo-document-picker": "^9.1.2",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-haptics": "~10.0.0",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-notifications": "~0.11.6",
"expo-splash-screen": "^0.11.2",
"expo-sqlite": "~9.1.0",
"expo-updates": "~0.5.4",
"expo-web-browser": "~9.1.0",
"moment": "^2.29.1",
"qs": "^6.10.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "^0.64.2",
"react-native-background-actions": "^2.6.1",
"react-native-cheerio": "^1.0.0-rc.4",
"react-native-device-info": "^8.1.3",
"react-native-easing-gradient": "^0.1.2",
"react-native-fast-image": "^8.3.4",
"react-native-gesture-handler": "~1.10.2",
"react-native-navigation-bar-color": "^2.0.1",
"react-native-pager-view": "^5.1.10",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "^2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-swipe-gestures": "^1.0.5",
"react-native-tab-view": "^3.0.1",
"react-native-tts": "^4.1.0",
"react-native-unimodules": "~0.13.3",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "^0.16.5",
"react-native-webview": "^11.6.2",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4"
},
"devDependencies": {
"@babel/core": "~7.9.0"
},
"prettier": {
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always"
},
"private": true,
"name": "lnreader",
"version": "1.0.0"
}
+26 -21
View File
@@ -3,27 +3,32 @@ import { Dimensions, StyleSheet, View } from "react-native";
import { IconButton } from "react-native-paper";
export const Actionbar = ({ actions, theme }) => {
return (
<View
style={[
styles.actionbarContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
{actions.map(
(action, index) =>
action.icon && (
<IconButton
key={index}
icon={action.icon}
color={theme.textColorPrimary}
onPress={action.onPress}
/>
)
)}
</View>
);
export const Actionbar = ({ active, actions, theme, style }) => {
if (active) {
return (
<View
style={[
styles.actionbarContainer,
{ backgroundColor: theme.colorPrimary },
style,
]}
>
{actions.map(
(action, index) =>
action.icon && (
<IconButton
key={index}
icon={action.icon}
color={theme.textColorPrimary}
onPress={action.onPress}
/>
)
)}
</View>
);
} else {
return null;
}
};
const styles = StyleSheet.create({
+1 -1
View File
@@ -4,7 +4,7 @@ import { View } from "react-native";
const BottomSheetHandle = ({ theme }) => (
<View
style={{
backgroundColor: "rgba(255,255,255,0.4)",
backgroundColor: `${theme.textColorPrimary}66`,
height: 4,
width: 40,
borderRadius: 50,
+56 -1
View File
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { StyleSheet, Text } from "react-native";
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
import { Modal, Portal, TextInput } from "react-native-paper";
@@ -43,6 +43,29 @@ const ColorPickerModal = ({
},
};
const accentColors = [
"#EF5350",
"#EC407A",
"#AB47BC",
"#7E57C2",
"#5C6BC0",
"#42A5F5",
"#29B6FC",
"#26C6DA",
"#26A69A",
"#66BB6A",
"#9CCC65",
"#D4E157",
"#FFEE58",
"#FFCA28",
"#FFA726",
"#FF7043",
"#8D6E63",
"#BDBDBD",
"#78909C",
"#000000",
];
return (
<Portal>
<Modal
@@ -61,6 +84,38 @@ const ColorPickerModal = ({
>
{title}
</Text>
{showAccentColors && (
<FlatList
contentContainerStyle={{ marginBottom: 8 }}
data={accentColors}
numColumns={4}
keyExtractor={(item) => item}
renderItem={({ item }) => (
<View
style={{
borderRadius: 4,
overflow: "hidden",
backgroundColor: item,
flex: 1 / 4,
height: 40,
marginHorizontal: 4,
marginVertical: 4,
}}
>
<Pressable
style={{ flex: 1 }}
android_ripple={{
color: "rgba(0,0,0,0.12)",
}}
onPress={() => {
onSubmit(item);
hideModal();
}}
/>
</View>
)}
/>
)}
<TextInput
value={text}
defaultValue={color}
+8 -2
View File
@@ -1,8 +1,14 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { View, StyleSheet } from "react-native";
const ScreenContainer = ({ children, theme }) => (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<View
style={{
flex: 1,
backgroundColor: theme.colorPrimaryDark,
paddingBottom: 16,
}}
>
{children}
</View>
);
+110
View File
@@ -0,0 +1,110 @@
import PropTypes from "prop-types";
import React, { PureComponent, Children } from "react";
import { View, Animated, StyleSheet } from "react-native";
export default class FadeView extends PureComponent {
static defaultProps = {
animationDuration: 225,
active: false,
removeHiddenSubviews: true,
};
static propTypes = {
animationDuration: PropTypes.number,
active: PropTypes.bool,
removeHiddenSubviews: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};
constructor(props) {
super(props);
this.renderChild = this.renderChild.bind(this);
this.mounted = false;
let { active } = this.props;
this.state = {
progress: new Animated.Value(Number(active)),
animating: false,
};
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
UNSAFE_componentWillReceiveProps({ active, animationDuration }) {
let { progress } = this.state;
if (active ^ this.props.active) {
this.setState({ animating: true });
Animated.timing(progress, {
toValue: Number(active),
duration: animationDuration,
useNativeDriver: true,
}).start(() => {
if (this.mounted) {
this.setState({ animating: false });
}
});
}
}
renderChild(child, index) {
let { active, removeHiddenSubviews } = this.props;
let { animating, progress } = this.state;
let opacity = progress.interpolate({
inputRange: [0, 1],
outputRange: index ? [0, 1] : [1, 0],
});
let hidden = active ^ !!index;
if (removeHiddenSubviews) {
if (!animating && hidden) {
return null;
}
}
let pointerEvents = hidden ? "none" : "box-none";
return (
<Animated.View
style={[styles.container, { opacity }]}
pointerEvents={pointerEvents}
>
{child}
</Animated.View>
);
}
render() {
let { children, ...props } = this.props;
return (
<View {...props}>{Children.map(children, this.renderChild)}</View>
);
}
}
const styles = StyleSheet.create({
container: {
// ...StyleSheet.absoluteFillObject,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
});
+4 -4
View File
@@ -32,7 +32,7 @@ export const Searchbar = ({
{backAction && (
<IconButton
icon={backAction}
color={theme.textColorSecondary}
color={theme.textColorPrimary}
style={{ marginLeft: 0 }}
size={23}
onPress={onBackAction}
@@ -42,7 +42,7 @@ export const Searchbar = ({
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorSecondary,
color: theme.textColorPrimary,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
@@ -54,7 +54,7 @@ export const Searchbar = ({
{searchText !== "" && (
<IconButton
icon="close"
color={theme.textColorSecondary}
color={theme.textColorPrimary}
style={{ marginRight: 0 }}
size={23}
onPress={clearSearchbar}
@@ -65,7 +65,7 @@ export const Searchbar = ({
<IconButton
key={index}
icon={action.icon}
color={action.color ?? theme.textColorSecondary}
color={action.color ?? theme.textColorPrimary}
style={{ marginRight: 0 }}
size={23}
onPress={action.onPress}
+172
View File
@@ -0,0 +1,172 @@
import React from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
export const ThemePicker = ({ theme, currentTheme, onPress }) => (
<View
style={{
justifyContent: "center",
alignItems: "center",
marginRight: 16,
}}
>
<View
style={{
backgroundColor: theme.colorPrimaryDark,
borderWidth: 3.6,
borderColor:
currentTheme.id == theme.id
? theme.colorAccent
: currentTheme.colorPrimaryDark,
width: 95,
height: 140,
borderRadius: 16,
overflow: "hidden",
elevation: 1,
}}
>
<Pressable style={{ flex: 1 }} onPress={onPress}>
{currentTheme.id == theme.id && (
<MaterialCommunityIcons
name="check"
color={theme.colorButtonText}
size={15}
style={{
backgroundColor: theme.colorAccent,
position: "absolute",
top: 5,
right: 5,
elevation: 2,
borderRadius: 50,
padding: 1.6,
}}
/>
)}
<View
style={{
height: 20,
backgroundColor: theme.colorPrimary,
elevation: 1,
justifyContent: "center",
}}
>
<View
style={{
backgroundColor: theme.textColorPrimary,
width: 44,
height: 10,
marginLeft: 8,
borderRadius: 50,
}}
/>
</View>
<View style={{ padding: 8 }}>
<View
style={{
height: 18,
backgroundColor: theme.textColorSecondary,
borderRadius: 4,
}}
/>
<View style={{ paddingVertical: 4, flexDirection: "row" }}>
<View
style={{
height: 10,
width: 44,
backgroundColor: theme.textColorPrimary,
borderRadius: 50,
}}
/>
<View
style={{
height: 10,
width: 16,
marginLeft: 4,
backgroundColor: theme.colorAccent,
borderRadius: 50,
}}
/>
</View>
<View style={{ flexDirection: "row" }}>
<View
style={{
height: 10,
width: 24,
backgroundColor: theme.textColorSecondary,
borderRadius: 50,
}}
/>
<View
style={{
height: 10,
width: 24,
backgroundColor: theme.textColorSecondary,
borderRadius: 50,
marginLeft: 4,
}}
/>
</View>
</View>
<View
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 24,
backgroundColor: theme.colorPrimary,
justifyContent: "center",
}}
>
<View
style={{
flex: 1,
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
paddingHorizontal: 16,
}}
>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.textColorPrimary,
opacity: 0.54,
}}
/>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.colorAccent,
}}
/>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.textColorPrimary,
opacity: 0.54,
}}
/>
</View>
</View>
</Pressable>
</View>
<Text
style={{
color: currentTheme.textColorPrimary,
fontSize: 12,
paddingVertical: 4,
}}
>
{theme.name}
</Text>
</View>
);
const styles = StyleSheet.create({});
+2 -3
View File
@@ -200,7 +200,6 @@ export const migrateNovel = async (sourceId, novelUrl) => {
await BackgroundService.updateNotification({
progressBar: { value: 1, indeterminate: false },
});
await BackgroundService.stop();
} catch (error) {
showToast(error.message);
}
@@ -224,10 +223,10 @@ export const updateNovelInfo = async (info, novelId) => {
});
};
export const setCustomNovelCover = async (novelId) => {
export const pickCustomNovelCover = async (novelId) => {
const image = await DocumentPicker.getDocumentAsync({ type: "image/*" });
if (image.uri) {
if (image.type === "success" && image.uri) {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET novelCover = ? WHERE novelId = ?",
+4
View File
@@ -8,6 +8,7 @@ const getUpdatesQuery = `
ON updates.chapterId = chapters.chapterId
JOIN novels
ON updates.novelId = novels.novelId
WHERE date(updates.updateTime) > date('now','-3 months')
ORDER BY updates.updateTime DESC
`;
@@ -36,3 +37,6 @@ export const deleteNovelUpdates = (novelId) => {
);
});
};
export const clearUpdates = () =>
db.transaction((tx) => tx.executeSql("DELETE FROM updates; VACCUM;"));
+1 -1
View File
@@ -4,7 +4,7 @@ import { darkTheme } from "../theme/theme";
const useTheme = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
return theme ?? darkTheme;
return theme || darkTheme;
};
const useReaderSettings = () => {
+6 -1
View File
@@ -2,6 +2,7 @@ import React from "react";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Library from "../screens/Library/Library";
import Updates from "../screens/Updates/Updates";
@@ -15,6 +16,7 @@ const Tab = createMaterialBottomTabNavigator();
const BottomNavigator = () => {
const theme = useTheme();
const insets = useSafeAreaInsets();
const {
showHistoryTab = true,
@@ -24,7 +26,10 @@ const BottomNavigator = () => {
return (
<Tab.Navigator
barStyle={{ backgroundColor: theme.colorPrimary }}
barStyle={{
backgroundColor: theme.colorPrimary,
paddingBottom: insets.bottom,
}}
activeColor={theme.colorAccent}
shifting={!showLabelsInNav}
>
+5
View File
@@ -12,6 +12,7 @@ import AdvancedSettings from "../screens/More/AdvancedSettings";
import GeneralSettings from "../screens/More/GeneralSettings";
import DownloadQueue from "../screens/More/DownloadQueue";
import Downloads from "../screens/More/Downloads";
import AppearanceSettings from "../screens/More/AppearanceSettings";
const Stack = createStackNavigator();
@@ -24,6 +25,10 @@ const SettingsStack = () => (
<Stack.Screen name="ReaderSettings" component={ReaderSettings} />
<Stack.Screen name="TrackerSettings" component={TrackerSettings} />
<Stack.Screen name="BackupSettings" component={BackupSettings} />
<Stack.Screen
name="AppearanceSettings"
component={AppearanceSettings}
/>
<Stack.Screen name="AdvancedSettings" component={AdvancedSettings} />
</Stack.Navigator>
);
+13 -8
View File
@@ -10,15 +10,20 @@ import {
} from "../../database/queries/LibraryQueries";
import { restoreBackup } from "../../services/backup";
export const getLibraryAction = (sort, filter) => async (dispatch) => {
// console.log("1.", sort, filter);
const res = await getLibrary(sort, filter);
export const getLibraryAction =
(sort, filter) => async (dispatch, getState) => {
const { downloadedOnlyMode = false } = getState().settingsReducer;
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
const res = await getLibrary(
sort,
downloadedOnlyMode ? "chaptersDownloaded > 0" : filter
);
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
export const searchLibraryAction =
(searchText, sort, filter) => async (dispatch) => {
+77 -72
View File
@@ -305,6 +305,9 @@ export const downloadChapterAction =
export const downloadAllChaptersAction =
(sourceId, novelUrl, chaps) => async (dispatch) => {
try {
/**
* Filter downloaded chapters
*/
let chapters = chaps.filter((chapter) => chapter.downloaded === 0);
chapters = chapters.map((chapter) => ({
@@ -313,84 +316,86 @@ export const downloadAllChaptersAction =
novelUrl,
}));
dispatch({ type: SET_DOWNLOAD_QUEUE, payload: chapters });
const options = {
taskName: "Library Update",
taskTitle: chapters[0].chapterName,
taskDesc: "0/" + chapters.length,
taskIcon: {
name: "notification_icon",
type: "drawable",
},
color: "#00adb5",
linkingURI: "lnreader://downloads/redirect",
parameters: {
delay: 1000,
},
progressBar: {
max: chapters.length,
value: 0,
},
};
if (chapters.length > 0) {
dispatch({ type: SET_DOWNLOAD_QUEUE, payload: chapters });
const options = {
taskName: "Library Update",
taskTitle: chapters[0].chapterName,
taskDesc: "0/" + chapters.length,
taskIcon: {
name: "notification_icon",
type: "drawable",
},
color: "#00adb5",
linkingURI: "lnreader://downloads/redirect",
parameters: {
delay: 1000,
},
progressBar: {
max: chapters.length,
value: 0,
},
};
const sleep = (time) =>
new Promise((resolve) => setTimeout(() => resolve(), time));
const sleep = (time) =>
new Promise((resolve) => setTimeout(() => resolve(), time));
const veryIntensiveTask = async (taskData) => {
await new Promise(async (resolve) => {
for (
let i = 0;
BackgroundService.isRunning() && i < chapters.length;
i++
) {
if (BackgroundService.isRunning()) {
// dispatch({
// type: CHAPTER_DOWNLOADING,
// payload: chapters[i].chapterId,
// });
const veryIntensiveTask = async (taskData) => {
await new Promise(async (resolve) => {
for (
let i = 0;
BackgroundService.isRunning() &&
i < chapters.length;
i++
) {
if (BackgroundService.isRunning()) {
// dispatch({
// type: CHAPTER_DOWNLOADING,
// payload: chapters[i].chapterId,
// });
if (!chapters[i].downloaded) {
await downloadChapter(
sourceId,
novelUrl,
chapters[i].chapterUrl,
chapters[i].chapterId
);
}
if (!chapters[i].downloaded) {
await downloadChapter(
sourceId,
novelUrl,
chapters[i].chapterUrl,
chapters[i].chapterId
);
}
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapters[i].chapterId,
});
await BackgroundService.updateNotification({
taskTitle: chapters[i].chapterName,
taskDesc: i + 1 + "/" + chapters.length,
progressBar: {
max: chapters.length,
value: i + 1,
},
});
if (i + 1 === chapters.length) {
resolve();
await BackgroundService.stop();
Notifications.scheduleNotificationAsync({
content: {
title: "Downloader",
body: "Download completed",
},
trigger: null,
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapters[i].chapterId,
});
}
await sleep(taskData.delay);
}
}
});
};
await BackgroundService.start(veryIntensiveTask, options);
await BackgroundService.updateNotification({
taskTitle: chapters[i].chapterName,
taskDesc: i + 1 + "/" + chapters.length,
progressBar: {
max: chapters.length,
value: i + 1,
},
});
if (i + 1 === chapters.length) {
resolve();
Notifications.scheduleNotificationAsync({
content: {
title: "Downloader",
body: "Download completed",
},
trigger: null,
});
}
await sleep(taskData.delay);
}
}
});
};
await BackgroundService.start(veryIntensiveTask, options);
}
} catch (error) {
showToast(error.message);
}
+64 -14
View File
@@ -19,6 +19,9 @@ import {
springBlossomTheme,
colorsAmoled,
oceanicTheme,
yangTheme,
yotsubaTheme,
takoLightTheme,
} from "../../theme/theme";
const themes = {
@@ -32,6 +35,9 @@ const themes = {
8: takoTheme,
9: yinYangTheme,
10: springBlossomTheme,
11: yangTheme,
12: yotsubaTheme,
13: takoLightTheme,
};
/**
@@ -40,12 +46,70 @@ const themes = {
* 0 -> Compact
* 1 -> Comfortable
* 2 -> List
* 3 -> No title
*/
const initialState = {
/**
* General settings
*/
incognitoMode: false,
/**
* Appearence settings
*/
theme: darkTheme,
displayMode: 0,
novelsPerRow: 3,
showHistoryTab: true,
showUpdatesTab: true,
showLabelsInNav: false,
useFabForContinueReading: false,
/**
* Library settings
*/
showDownloadBadges: true,
showUnreadBadges: true,
showNumberOfNovels: false,
downloadedOnlyMode: false,
/**
* Browse settings
*/
searchAllSources: false,
/**
* Update settings
*/
onlyUpdateOngoingNovels: false,
updateLibraryOnLaunch: false,
downloadNewChapters: false,
/**
* Novel settings
*/
hideBackdrop: false,
/**
* Reader settings
*/
fullScreenMode: true,
swipeGestures: true,
showScrollPercentage: true,
useWebViewForChapter: false,
showBatteryAndTime: false,
autoScroll: false,
autoScrollInterval: 10,
textSelectable: false,
reader: {
theme: 1,
textColor: "rgba(255,255,255,0.7)",
@@ -56,20 +120,6 @@ const initialState = {
lineHeight: 1.5,
customCSS: null,
},
showDownloadBadges: true,
showUnreadBadges: true,
showNumberOfNovels: false,
showScrollPercentage: true,
swipeGestures: true,
incognitoMode: false,
searchAllSources: false,
textSelectable: false,
updateLibraryOnLaunch: false,
downloadNewChapters: false,
showHistoryTab: true,
showUpdatesTab: true,
showLabelsInNav: false,
useWebViewForChapter: false,
};
const settingsReducer = (state = initialState, action) => {
+3 -2
View File
@@ -38,9 +38,10 @@ export const updateLibraryAction = () => async (dispatch, getState) => {
dispatch({ type: SET_LAST_UPDATE_TIME, payload: Date.now() });
const { downloadNewChapters = false } = getState().settingsReducer;
const { downloadNewChapters = false, onlyUpdateOngoingNovels = false } =
getState().settingsReducer;
await updateAllNovels({ downloadNewChapters });
await updateAllNovels({ downloadNewChapters, onlyUpdateOngoingNovels });
dispatch(getUpdatesAction());
};
+1
View File
@@ -41,6 +41,7 @@ const Browse = ({ navigation }) => {
pinned.indexOf(sourceId) === -1 ? false : true;
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
search = search.filter((source) => filters.indexOf(source.lang) === -1);
let pinnedSources = sources.filter((source) => isPinned(source.sourceId));
+4 -1
View File
@@ -30,7 +30,10 @@ const BrowseSettings = ({ navigation }) => {
"Arabic",
"Indonesian",
"Russian",
];
"Turkish",
"French",
"Portuguese (Brazil)",
].sort();
const { searchAllSources = false } = useSettings();
+280 -23
View File
@@ -6,11 +6,14 @@ import {
StatusBar,
ScrollView,
TouchableWithoutFeedback,
Dimensions,
Pressable,
} from "react-native";
import { useDispatch } from "react-redux";
import { IconButton, Portal } from "react-native-paper";
import {
import Tts from "react-native-tts";
import changeNavigationBarColor, {
hideNavigationBar,
showNavigationBar,
} from "react-native-navigation-bar-color";
@@ -53,6 +56,9 @@ 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";
import { useBatteryLevel } from "react-native-device-info";
import moment from "moment";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const Chapter = ({ route, navigation }) => {
const {
@@ -72,15 +78,22 @@ const Chapter = ({ route, navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const reader = useReaderSettings();
const insets = useSafeAreaInsets();
const {
showScrollPercentage = true,
fullScreenMode = true,
swipeGestures = true,
incognitoMode = false,
textSelectable = false,
useWebViewForChapter = false,
showBatteryAndTime = false,
autoScroll = false,
autoScrollInterval = 10,
} = useSettings();
const batteryLevel = useBatteryLevel();
const [hidden, setHidden] = useState(true);
const { tracker, trackedNovels } = useTrackingStatus();
@@ -97,6 +110,103 @@ const Chapter = ({ route, navigation }) => {
const [contentSize, setContentSize] = useState(0);
const [textToSpeech, setTextToSpeech] = useState();
const [textToSpeechPosition, setTextToSpeechPosition] = useState({
end: 0,
});
useEffect(() => {
Tts.addEventListener("tts-start", () => setTextToSpeech("start"));
Tts.addEventListener("tts-progress", (event) => {
setTextToSpeech("progress");
setTextToSpeechPosition(event);
});
Tts.addEventListener("tts-finish", () => setTextToSpeech("finish"));
Tts.addEventListener("tts-cancel", () => setTextToSpeech("paused"));
return () => Tts.stop();
}, []);
const tts = () => {
if (!loading) {
if (textToSpeech === "progress") {
setTextToSpeechPosition({ end: 0 });
setTextToSpeech("finish");
Tts.stop();
return;
}
const text = htmlToText(chapter.chapterText);
if (text.length >= 3999) {
const splitNChars = (txt, num) => {
let result = [];
for (let i = 0; i < txt.length; i += num) {
result.push(txt.substr(i, num));
}
return result;
};
let splitMe = splitNChars(text, 3999);
splitMe.forEach((value, key) => {
Tts.speak(value, {
androidParams: {
KEY_PARAM_STREAM: "STREAM_MUSIC",
},
});
});
} else {
Tts.stop();
Tts.speak(text, {
androidParams: {
KEY_PARAM_STREAM: "STREAM_MUSIC",
},
});
}
}
};
const pauseTts = () => {
if (!loading) {
if (textToSpeech === "progress") {
setTextToSpeech("paused");
Tts.stop();
return;
} else {
let text = htmlToText(chapter.chapterText);
text = text.slice(textToSpeechPosition.end);
if (text.length >= 3999) {
const splitNChars = (txt, num) => {
let result = [];
for (let i = 0; i < txt.length; i += num) {
result.push(txt.substr(i, num));
}
return result;
};
let splitMe = splitNChars(text, 3999);
splitMe.forEach((value, key) => {
Tts.speak(value, {
androidParams: {
KEY_PARAM_STREAM: "STREAM_MUSIC",
},
});
});
} else {
Tts.stop();
Tts.speak(text, {
androidParams: {
KEY_PARAM_STREAM: "STREAM_MUSIC",
},
});
}
}
}
};
const getChapter = async (chapterId) => {
try {
if (chapterId) {
@@ -138,7 +248,9 @@ const Chapter = ({ route, navigation }) => {
};
useEffect(() => {
setImmersiveMode();
getChapter(chapterId);
if (!incognitoMode) {
insertHistory(novelId, chapterId);
dispatch({
@@ -146,16 +258,48 @@ const Chapter = ({ route, navigation }) => {
payload: { novelId, chapterId },
});
}
return () => {
}, []);
useEffect(() => {
navigation.addListener("beforeRemove", (e) => {
StatusBar.setHidden(false);
showNavigationBar();
};
});
}, []);
useEffect(() => {
setPrevAndNextChap();
}, [chapter]);
const [currentTime, setCurrentTime] = useState();
useEffect(() => {
setInterval(() => {
setCurrentTime(new Date().toISOString());
}, 60000);
}, []);
const [currentOffset, setCurrentOffset] = useState(position?.position || 0);
let scrollTimeout;
useEffect(() => {
if (!useWebViewForChapter && scrollPercentage !== 100 && autoScroll) {
scrollTimeout = setTimeout(() => {
scrollViewRef.current.scrollTo({
x: 0,
y: currentOffset + Dimensions.get("window").height,
animated: true,
});
setCurrentOffset(
(prevState) => prevState + Dimensions.get("window").height
);
}, autoScrollInterval * 1000);
}
return () => clearTimeout(scrollTimeout);
}, [autoScroll, currentOffset]);
const isCloseToBottom = useCallback(
({ layoutMeasurement, contentOffset, contentSize }) => {
const paddingToBottom = 40;
@@ -206,12 +350,16 @@ const Chapter = ({ route, navigation }) => {
}, []);
const setImmersiveMode = () => {
StatusBar.setHidden(true);
hideNavigationBar();
if (fullScreenMode) {
StatusBar.setHidden(true);
hideNavigationBar();
} else {
StatusBar.setBackgroundColor(readerBackground(reader.theme));
changeNavigationBarColor(readerBackground(reader.theme));
}
};
const scrollToSavedProgress = useCallback((event) => {
setImmersiveMode();
if (position && firstLayout) {
position.percentage < 100 &&
scrollViewRef.current.scrollTo({
@@ -224,7 +372,14 @@ const Chapter = ({ route, navigation }) => {
}, []);
const hideHeader = () => {
!hidden && setImmersiveMode();
if (!hidden) {
setImmersiveMode();
} else {
if (fullScreenMode) {
StatusBar.setHidden(false);
showNavigationBar();
}
}
setHidden(!hidden);
};
@@ -280,6 +435,9 @@ const Chapter = ({ route, navigation }) => {
);
};
const enableAutoScroll = () =>
dispatch(setAppSettings("autoScroll", !autoScroll));
const enableWebView = () =>
dispatch(setAppSettings("useWebViewForChapter", !useWebViewForChapter));
@@ -291,10 +449,15 @@ const Chapter = ({ route, navigation }) => {
chapterName={chapterName}
chapterId={chapterId}
bookmark={bookmark}
textToSpeech={textToSpeech}
tts={tts}
readerSheetRef={readerSheetRef}
hide={hidden}
navigation={navigation}
dispatch={dispatch}
theme={theme}
textToSpeechPosition={textToSpeechPosition}
pauseTts={pauseTts}
/>
<GestureRecognizer
onSwipeRight={swipeGestures && navigateToPrevChapter}
@@ -362,7 +525,6 @@ const Chapter = ({ route, navigation }) => {
backgroundColor: readerBackground(
reader.theme
),
paddingBottom: 24,
}}
originWhitelist={["*"]}
scalesPageToFit={true}
@@ -395,10 +557,12 @@ const Chapter = ({ route, navigation }) => {
html {
overflow-x: hidden;
padding-top: ${StatusBar.currentHeight};
word-wrap: break-word;
}
body {
padding-left: ${reader.padding}%;
padding-right: ${reader.padding}%;
padding-bottom: 30px;
font-size: ${reader.textSize}px;
color: ${reader.textColor};
@@ -444,7 +608,6 @@ const Chapter = ({ route, navigation }) => {
style={{
flex: 1,
paddingVertical: 16,
paddingBottom: 32,
paddingHorizontal: `${reader.padding}%`,
paddingTop: StatusBar.currentHeight,
}}
@@ -455,12 +618,77 @@ const Chapter = ({ route, navigation }) => {
>
{htmlToText(chapter.chapterText)}
</Text>
<View
style={{
marginTop: 32,
marginBottom: 16,
}}
>
<Text
style={{
color: reader.textColor,
fontSize: 16,
textAlign: "center",
}}
>
Finished: {chapterName}
</Text>
{nextChapter ? (
<View
style={{
borderRadius: 8,
marginVertical: 4,
overflow: "hidden",
}}
>
<Pressable
style={{
paddingHorizontal: 16,
paddingVertical: 8,
}}
android_ripple={{
color: theme.rippleColor,
}}
onPress={
navigateToNextChapter
}
>
<Text
style={{
color: reader.textColor,
fontSize: 16,
textAlign:
"center",
}}
>
Next:{" "}
{
nextChapter.chapterName
}
</Text>
</Pressable>
</View>
) : (
<Text
style={{
color: reader.textColor,
fontSize: 16,
paddingVertical: 8,
textAlign: "center",
}}
>
There's no next chapter
</Text>
)}
</View>
</View>
)}
</TouchableWithoutFeedback>
)}
</ScrollView>
</GestureRecognizer>
<Portal>
<ReaderSheet
theme={theme}
@@ -469,8 +697,13 @@ const Chapter = ({ route, navigation }) => {
navigation={navigation}
bottomSheetRef={readerSheetRef}
selectText={textSelectable}
autoScroll={autoScroll}
useWebViewForChapter={useWebViewForChapter}
showScrollPercentage={showScrollPercentage}
showBatteryAndTime={showBatteryAndTime}
fullScreenMode={fullScreenMode}
swipeGestures={swipeGestures}
enableSwipeGestures={enableSwipeGestures}
/>
</Portal>
{!useWebViewForChapter && (
@@ -493,24 +726,46 @@ const Chapter = ({ route, navigation }) => {
nextChapter={nextChapter}
prevChapter={prevChapter}
hide={hidden}
autoScroll={autoScroll}
useWebViewForChapter={useWebViewForChapter}
navigateToNextChapter={navigateToNextChapter}
navigateToPrevChapter={navigateToPrevChapter}
readerSheetRef={readerSheetRef}
scrollViewRef={scrollViewRef}
enableAutoScroll={enableAutoScroll}
/>
{showScrollPercentage && (
<Text
{(showScrollPercentage || showBatteryAndTime) && (
<View
style={[
styles.scrollPercentage,
{
color: reader.textColor,
backgroundColor: readerBackground(reader.theme),
styles.scrollPercentageContainer,
{ backgroundColor: readerBackground(reader.theme) },
!fullScreenMode && {
paddingBottom: insets.bottom,
},
]}
>
{scrollPercentage + "%"}
</Text>
{showBatteryAndTime && (
<Text style={{ color: reader.textColor }}>
{Math.ceil(batteryLevel * 100) + "%"}
</Text>
)}
{showScrollPercentage && (
<Text
style={{
flex: 1,
color: reader.textColor,
textAlign: "center",
}}
>
{scrollPercentage + "%"}
</Text>
)}
{showBatteryAndTime && (
<Text style={{ color: reader.textColor }}>
{moment(currentTime).format("h:mm")}
</Text>
)}
</View>
)}
</>
</>
@@ -521,15 +776,17 @@ export default Chapter;
const styles = StyleSheet.create({
screenContainer: { flexGrow: 1 },
scrollPercentage: {
scrollPercentageContainer: {
width: "100%",
position: "absolute",
paddingVertical: 4,
left: 0,
right: 0,
bottom: 0,
height: 30,
textAlign: "center",
textAlignVertical: "center",
alignSelf: "center",
fontWeight: "bold",
zIndex: 1,
flexDirection: "row",
paddingHorizontal: 32,
alignItems: "center",
justifyContent: "space-between",
},
});
+46 -19
View File
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { View } from "react-native";
import { Appbar } from "react-native-paper";
import FadeView from "../../../components/Common/CrossFadeView";
import { bookmarkChapterAction } from "../../../redux/novel/novel.actions";
const ChapterAppbar = ({
@@ -12,53 +13,79 @@ const ChapterAppbar = ({
chapterName,
hide,
dispatch,
tts,
textToSpeech,
theme,
textToSpeechPosition,
pauseTts,
}) => {
const [bookmarked, setBookmarked] = useState(bookmark);
if (hide) {
return null;
} else {
return (
return (
<FadeView
style={{
flex: 1,
position: "absolute",
width: "100%",
top: 0,
zIndex: 1,
}}
active={hide}
animationDuration={150}
>
<View
style={{
position: "absolute",
flex: 1,
backgroundColor: "rgba(0,0,0,0.7)",
width: "100%",
top: 0,
zIndex: 1,
}}
style={{ flex: 1, backgroundColor: `${theme.colorPrimary}E6` }}
>
<Appbar.Header
style={{ backgroundColor: "transparent", elevation: 0 }}
>
<Appbar.BackAction
onPress={navigation.goBack}
color="#FFFFFF"
color={theme.textColorPrimary}
size={26}
style={{ marginRight: 0 }}
/>
<Appbar.Content
title={novelName}
titleStyle={{ color: "#FFFFFF" }}
titleStyle={{ color: theme.textColorPrimary }}
subtitle={chapterName}
subtitleStyle={{ color: "rgba(255, 255, 255, 0.7)" }}
subtitleStyle={{ color: theme.textColorSecondary }}
/>
<Appbar.Action
icon="volume-high"
size={24}
onPress={tts}
color={
textToSpeech === "progress"
? theme.colorAccent
: theme.textColorPrimary
}
/>
{textToSpeechPosition.end > 0 && (
<Appbar.Action
icon={textToSpeech === "paused" ? "play" : "pause"}
size={24}
onPress={pauseTts}
color={theme.textColorPrimary}
/>
)}
<Appbar.Action
icon={bookmarked ? "bookmark" : "bookmark-outline"}
size={26}
size={24}
onPress={() => {
dispatch(
bookmarkChapterAction([{ bookmark, chapterId }])
);
setBookmarked(!bookmarked);
}}
color="#FFFFFF"
color={theme.textColorPrimary}
/>
</Appbar.Header>
</View>
);
}
</FadeView>
);
};
export default ChapterAppbar;
+56 -27
View File
@@ -1,6 +1,8 @@
import React from "react";
import { Pressable, StyleSheet, View } from "react-native";
import { IconButton } from "react-native-paper";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import FadeView from "../../../components/Common/CrossFadeView";
const ChapterFooter = ({
hide,
@@ -14,7 +16,9 @@ const ChapterFooter = ({
prevChapter,
useWebViewForChapter,
enableSwipeGestures,
autoScroll,
enableWebView,
enableAutoScroll,
}) => {
const rippleConfig = {
color: theme.rippleColor,
@@ -22,18 +26,24 @@ const ChapterFooter = ({
radius: 50,
};
if (hide) {
return null;
} else {
return (
const insets = useSafeAreaInsets();
return (
<FadeView
style={{
position: "absolute",
zIndex: 2,
bottom: 0,
width: "100%",
}}
active={hide}
animationDuration={150}
>
<View
style={{
position: "absolute",
zIndex: 2,
bottom: 0,
width: "100%",
backgroundColor: "rgba(0,0,0,0.7)",
backgroundColor: `${theme.colorPrimary}E6`,
flexDirection: "row",
paddingBottom: insets.bottom,
}}
>
<Pressable
@@ -45,7 +55,7 @@ const ChapterFooter = ({
icon="chevron-left"
size={26}
disabled={!prevChapter}
color="#FFFFFF"
color={theme.textColorPrimary}
/>
</Pressable>
<Pressable
@@ -57,21 +67,35 @@ const ChapterFooter = ({
icon="gesture-swipe"
disabled={!swipeGestures}
size={26}
color="#FFFFFF"
color={theme.textColorPrimary}
/>
</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={enableAutoScroll}
>
<IconButton
icon="chevron-triple-down"
disabled={!autoScroll}
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={() => scrollViewRef.current.scrollTo({})}
>
<IconButton
icon="format-vertical-align-top"
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
</>
)}
{/* <Pressable
android_ripple={rippleConfig}
@@ -82,7 +106,7 @@ const ChapterFooter = ({
icon="language-html5"
disabled={!useWebViewForChapter}
size={26}
color="#FFFFFF"
color={theme.textColorPrimary}
/>
</Pressable> */}
<Pressable
@@ -90,7 +114,11 @@ const ChapterFooter = ({
style={styles.buttonStyles}
onPress={() => readerSheetRef.current.show()}
>
<IconButton icon="cog-outline" size={26} color="#FFFFFF" />
<IconButton
icon="cog-outline"
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
<Pressable
android_ripple={rippleConfig}
@@ -101,12 +129,12 @@ const ChapterFooter = ({
icon="chevron-right"
size={26}
disabled={!nextChapter}
color="#FFFFFF"
color={theme.textColorPrimary}
/>
</Pressable>
</View>
);
}
</FadeView>
);
};
export default ChapterFooter;
@@ -117,5 +145,6 @@ const styles = StyleSheet.create({
justifyContent: "center",
alignItems: "center",
paddingVertical: 8,
paddingBottom: 4,
},
});
+468 -387
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from "react";
import React, { memo, useCallback, useState } from "react";
import {
StyleSheet,
View,
@@ -39,16 +39,17 @@ const ReaderSheet = ({
dispatch,
navigation,
bottomSheetRef,
showBatteryAndTime,
selectText,
autoScroll,
useWebViewForChapter,
showScrollPercentage,
swipeGestures,
enableSwipeGestures,
fullScreenMode,
}) => {
const [animatedValue] = useState(new Animated.Value(0));
const [fontMenu, setFontMenu] = useState(false);
const openFontMenu = () => setFontMenu(true);
const closeFontMenu = () => setFontMenu(false);
const presetThemes = [
{ value: 2, backgroundColor: "#f5f5fa", textColor: "#111111" },
{ value: 3, backgroundColor: "#F7DFC6", textColor: "#593100" },
@@ -68,311 +69,315 @@ const ReaderSheet = ({
{ value: "right", icon: "format-align-right" },
];
const FirstRoute = useCallback(
() => (
<View style={{ flex: 1, paddingVertical: 16 }}>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
textAlign: "center",
}}
>
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
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 390, bottom: 0 }}
snappingPoints={[0, 390]}
showBackdrop={true}
backdropOpacity={0}
height={390}
>
<ScrollView
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<BottomSheetHandle theme={theme} />
<View style={{ flex: 1, paddingVertical: 16, paddingTop: 24 }}>
<View
style={{
flexDirection: "row",
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
)
);
}}
/>
))}
</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",
item.value
)
)
}
/>
))}
</Row>
</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",
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,
alignItems: "center",
}}
>
<Text
style={{
color: theme.textColorSecondary,
marginRight: 16,
paddingHorizontal: 16,
textAlign: "center",
}}
>
Font style
Text size
</Text>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ alignItems: "center" }}
<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,
}}
>
{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,
}}
<Text style={{ color: theme.textColorSecondary }}>
Color
</Text>
<View style={{ marginLeft: 16 }}>
<ScrollView horizontal={true}>
{presetThemes.map((item, index) => (
<ToggleColorButton
key={index}
selected={
reader.theme === item.value
}
backgroundColor={
item.backgroundColor
}
textColor={item.textColor}
theme={theme}
onPress={() => {
dispatch(
setReaderSettings(
"theme",
item.value
)
);
dispatch(
setReaderSettings(
"textColor",
item.textColor
)
);
}}
/>
))}
</ScrollView>
</View>
</Row>
<Row
style={{
justifyContent: "space-between",
marginVertical: 6,
}}
>
<Text style={{ color: theme.textColorSecondary }}>
Text align
</Text>
<Row>
{textAlignments.map((item) => (
<ToggleButton
key={item.value}
selected={
item.value === reader.textAlign
}
icon={item.icon}
theme={theme}
onPress={() =>
dispatch(
setReaderSettings(
"fontFamily",
font.fontFamily
"textAlign",
item.value
)
)
}
android_ripple={{
color: theme.rippleColor,
}}
/>
))}
</Row>
</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: theme.textColorPrimary,
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",
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: theme.textColorPrimary,
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}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ alignItems: "center" }}
>
{fonts.map((font) => (
<View
style={[
{
borderRadius: 8,
overflow: "hidden",
marginHorizontal: 6,
},
reader.fontFamily ===
font.fontFamily && {
backgroundColor:
theme.rippleColor,
},
]}
>
<Text
<Pressable
style={{
fontSize: 16,
fontFamily: font.fontFamily,
color:
reader.fontFamily ===
font.fontFamily
? theme.colorAccent
: theme.textColorPrimary,
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 4,
}}
onPress={() =>
dispatch(
setReaderSettings(
"fontFamily",
font.fontFamily
)
)
}
android_ripple={{
color: theme.rippleColor,
}}
>
{font.name}
</Text>
</Pressable>
</View>
))}
</ScrollView>
</Row>
</View>
</View>
),
[reader]
);
const SecondRoute = useCallback(
() => (
<View style={{ flex: 1 }}>
<Pressable
style={styles.switchStyle}
android_ripple={{ color: theme.rippleColor }}
onPress={() =>
dispatch(
setAppSettings(
"useWebViewForChapter",
!useWebViewForChapter
)
)
}
>
<Text
style={{
color: theme.textColorSecondary,
}}
>
Use WebView
</Text>
<Switch
value={useWebViewForChapter}
onValueChange={() =>
<Text
style={{
fontSize: 16,
fontFamily: font.fontFamily,
color:
reader.fontFamily ===
font.fontFamily
? theme.colorAccent
: theme.textColorPrimary,
}}
>
{font.name}
</Text>
</Pressable>
</View>
))}
</ScrollView>
</Row>
</View>
<Pressable
style={styles.switchStyle}
android_ripple={{ color: theme.rippleColor }}
onPress={() =>
dispatch(
setAppSettings(
"useWebViewForChapter",
@@ -380,48 +385,36 @@ const ReaderSheet = ({
)
)
}
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
<Text
style={{
color: theme.textColorSecondary,
}}
>
Use WebView
</Text>
<Switch
value={useWebViewForChapter}
onValueChange={() =>
dispatch(
setAppSettings(
"useWebViewForChapter",
!useWebViewForChapter
)
)
)
}
color={theme.colorAccent}
/>
</Pressable>
{!useWebViewForChapter && (
}
color={theme.colorAccent}
/>
</Pressable>
<Pressable
style={styles.switchStyle}
android_ripple={{ color: theme.rippleColor }}
onPress={() =>
dispatch(
setAppSettings("textSelectable", !selectText)
setAppSettings(
"fullScreenMode",
!fullScreenMode
)
)
}
>
@@ -430,11 +423,139 @@ const ReaderSheet = ({
color: theme.textColorSecondary,
}}
>
Select text
Fullscreen
</Text>
<Switch
value={selectText}
value={fullScreenMode}
onValueChange={() =>
dispatch(
setAppSettings(
"fullScreenMode",
!fullScreenMode
)
)
}
color={theme.colorAccent}
/>
</Pressable>
{!useWebViewForChapter && (
<Pressable
style={styles.switchStyle}
android_ripple={{ color: theme.rippleColor }}
onPress={() =>
dispatch(
setAppSettings("autoScroll", !autoScroll)
)
}
>
<Text
style={{
color: theme.textColorSecondary,
}}
>
AutoScroll
</Text>
<Switch
value={autoScroll}
onValueChange={() =>
dispatch(
setAppSettings(
"autoScroll",
!autoScroll
)
)
}
color={theme.colorAccent}
/>
</Pressable>
)}
<Pressable
style={styles.switchStyle}
android_ripple={{ color: theme.rippleColor }}
onPress={() =>
dispatch(
setAppSettings(
"showBatteryAndTime",
!showBatteryAndTime
)
)
}
>
<Text
style={{
color: theme.textColorSecondary,
}}
>
Show battery and time
</Text>
<Switch
value={showBatteryAndTime}
onValueChange={() =>
dispatch(
setAppSettings(
"showBatteryAndTime",
!showBatteryAndTime
)
)
}
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>
<Pressable
style={styles.switchStyle}
android_ripple={{ color: theme.rippleColor }}
onPress={enableSwipeGestures}
>
<Text
style={{
color: theme.textColorSecondary,
}}
>
Swipe gestures
</Text>
<Switch
value={swipeGestures}
onValueChange={enableSwipeGestures}
color={theme.colorAccent}
/>
</Pressable>
{!useWebViewForChapter && (
<Pressable
style={styles.switchStyle}
android_ripple={{ color: theme.rippleColor }}
onPress={() =>
dispatch(
setAppSettings(
"textSelectable",
@@ -442,71 +563,31 @@ const ReaderSheet = ({
)
)
}
color={theme.colorAccent}
/>
</Pressable>
)}
</View>
),
[useWebViewForChapter, selectText, showScrollPercentage]
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
});
const layout = useWindowDimensions();
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Display" },
{ key: "second", title: "General" },
]);
const renderTabBar = (props) => (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: theme.colorAccent }}
style={{ backgroundColor: theme.colorPrimary }}
renderLabel={({ route, focused, color }) => (
<Text style={{ color }}>{route.title}</Text>
)}
inactiveColor={theme.textColorSecondary}
activeColor={theme.colorAccent}
pressColor={theme.rippleColor}
/>
);
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 400, bottom: 0 }}
snappingPoints={[0, 400]}
showBackdrop={true}
backdropOpacity={0}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<TabView
navigationState={{ index, routes }}
renderTabBar={renderTabBar}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
style={{ borderTopRightRadius: 8, borderTopLeftRadius: 8 }}
/>
</View>
>
<Text style={{ color: theme.textColorSecondary }}>
Select text
</Text>
<Switch
value={selectText}
onValueChange={() =>
dispatch(
setAppSettings(
"textSelectable",
!selectText
)
)
}
color={theme.colorAccent}
/>
</Pressable>
)}
</View>
</ScrollView>
</Bottomsheet>
);
};
export default ReaderSheet;
export default memo(ReaderSheet);
const styles = StyleSheet.create({
contentContainer: {
@@ -3,6 +3,7 @@ import { Dimensions, View, Text, StyleSheet } from "react-native";
import Slider from "@react-native-community/slider";
import { getDeviceOrientation } from "../../../services/utils/helpers";
import FadeView from "../../../components/Common/CrossFadeView";
const VerticalScrollbar = ({
theme,
@@ -24,78 +25,85 @@ const VerticalScrollbar = ({
if (hide) {
return null;
} else if (getDeviceOrientation() === "potrait") {
return (
<View style={styles.verticalSliderContainer}>
<Text
style={{
color: "#FFFFFF",
marginLeft: 16,
transform: [{ rotate: "-90deg" }],
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#FFFFFF"
/>
<Text
style={{
color: "#FFFFFF",
marginRight: 16,
transform: [{ rotate: "-90deg" }],
}}
>
100
</Text>
</View>
);
} else {
return (
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: "#FFFFFF",
marginLeft: 16,
}}
if (getDeviceOrientation() === "potrait") {
return (
<View
style={[
styles.verticalSliderContainer,
{ backgroundColor: `${theme.colorPrimary}E6` },
]}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: "#FFFFFF",
marginRight: 16,
}}
>
100
</Text>
</View>
);
<Text
style={{
color: theme.textColorPrimary,
marginLeft: 16,
transform: [{ rotate: "-90deg" }],
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: theme.textColorPrimary,
marginRight: 16,
transform: [{ rotate: "-90deg" }],
}}
>
100
</Text>
</View>
);
} else {
return (
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: "#FFFFFF",
marginLeft: 16,
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: "#FFFFFF",
marginRight: 16,
}}
>
100
</Text>
</View>
);
}
}
};
@@ -103,23 +111,21 @@ export default VerticalScrollbar;
const styles = StyleSheet.create({
verticalSliderContainer: {
position: "absolute",
zIndex: 2,
right: -(Dimensions.get("window").width / 2) + 40,
bottom: 300,
backgroundColor: "rgba(0,0,0,0.7)",
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
borderRadius: 50,
marginHorizontal: 8,
transform: [{ rotate: "90deg" }],
position: "absolute",
zIndex: 2,
right: -(Dimensions.get("window").width / 2) + 40,
bottom: 300,
},
horizontalSliderContainer: {
position: "absolute",
zIndex: 2,
bottom: 100,
backgroundColor: "rgba(0,0,0,0.7)",
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
+18 -9
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { StyleSheet, View, ActivityIndicator } from "react-native";
import * as WebBrowser from "expo-web-browser";
@@ -75,11 +75,13 @@ const Extension = ({ navigation, route }) => {
}
};
const checkIFInLibrary = (sourceId, novelUrl) => {
return library.some(
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
);
};
const checkIFInLibrary = useCallback(
(sourceId, novelUrl) =>
library.some(
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
),
[]
);
useEffect(() => {
let mounted = true;
@@ -102,8 +104,14 @@ const Extension = ({ navigation, route }) => {
);
};
const onScroll = ({ nativeEvent }) => {
if (!searchText && isCloseToBottom(nativeEvent)) {
// const onScroll = ({ nativeEvent }) => {
// if (!searchText && isCloseToBottom(nativeEvent)) {
// incrementPage();
// }
// };
const getNextPage = () => {
if (!searchText) {
incrementPage();
}
};
@@ -198,7 +206,8 @@ const Extension = ({ navigation, route }) => {
data={novels}
renderItem={renderItem}
ListEmptyComponent={listEmptyComponent()}
onScroll={onScroll}
// onScroll={onScroll}
onEndReached={getNextPage}
ListFooterComponent={
!searchText &&
page < totalPages &&
+10 -5
View File
@@ -1,5 +1,11 @@
import React, { useState, useCallback } from "react";
import { StyleSheet, FlatList, ActivityIndicator, Text } from "react-native";
import {
StyleSheet,
FlatList,
ActivityIndicator,
Text,
View,
} from "react-native";
import moment from "moment";
import { useFocusEffect } from "@react-navigation/native";
@@ -113,7 +119,7 @@ const History = ({ navigation }) => {
};
return (
<ScreenContainer theme={theme}>
<View style={{ flex: 1 }}>
<Searchbar
placeholder="Search History"
searchText={searchText}
@@ -124,13 +130,12 @@ const History = ({ navigation }) => {
actions={[
{
icon: "delete-sweep",
color: theme.textColorSecondary,
onPress: showDialog,
},
]}
/>
<FlatList
contentContainerStyle={{ flexGrow: 1 }}
contentContainerStyle={{ paddingBottom: 40 }}
data={searchText ? searchResults : history}
keyExtractor={(item) => item.date}
renderItem={({ item }) => (
@@ -175,7 +180,7 @@ const History = ({ navigation }) => {
hideDialog={hideDialog}
theme={theme}
/>
</ScreenContainer>
</View>
);
};
+1 -1
View File
@@ -29,7 +29,7 @@ const HistoryItem = ({
} = history;
const getChapterNumber = () =>
`Ch. ${parseChapterNumber(chapterName)} - ${moment(historyTimeRead)
`Chapter ${parseChapterNumber(chapterName)} • ${moment(historyTimeRead)
.format("h:mm a")
.toUpperCase()}`;
+91 -60
View File
@@ -11,6 +11,7 @@ import {
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { useFocusEffect } from "@react-navigation/native";
import { useSelector, useDispatch } from "react-redux";
import * as Haptics from "expo-haptics";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import NovelList from "../../components/NovelList";
@@ -41,6 +42,7 @@ const LibraryScreen = ({ navigation }) => {
showNumberOfNovels,
incognitoMode = false,
updateLibraryOnLaunch = false,
downloadedOnlyMode = false,
} = settings;
const libraryFilterSheetRef = useRef(null);
@@ -81,6 +83,23 @@ const LibraryScreen = ({ navigation }) => {
dispatch(searchLibraryAction(text, sort, filter));
};
/**
* Selected Novels
*/
const [selectedNovels, setSelectedNovels] = useState([]);
const selectNovel = (novelId) => {
if (selectedNovels.indexOf(novelId) === -1) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
setSelectedNovels((prevState) => [...prevState, novelId]);
} else {
setSelectedNovels((prevState) =>
prevState.filter((id) => id !== novelId)
);
}
};
const renderItem = ({ item }) => (
<NovelCover
item={item}
@@ -114,22 +133,6 @@ 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
@@ -159,9 +162,10 @@ const LibraryScreen = ({ navigation }) => {
{
icon: "filter-variant",
onPress: () => libraryFilterSheetRef.current.show(),
color: filter
? theme.filterColor
: theme.textColorSecondary,
color:
filter || downloadedOnlyMode
? theme.filterColor
: theme.textColorPrimary,
},
]}
theme={theme}
@@ -189,7 +193,34 @@ const LibraryScreen = ({ navigation }) => {
marginLeft: 8,
}}
>
Incognito Mode
Incognito mode
</Text>
</View>
)}
{downloadedOnlyMode && (
<View
style={{
backgroundColor: theme.searchBarColor,
paddingVertical: 6,
alignItems: "center",
marginVertical: 4,
elevation: 2,
flexDirection: "row",
justifyContent: "center",
}}
>
<MaterialCommunityIcons
name="cloud-off-outline"
color={theme.textColorSecondary}
size={18}
/>
<Text
style={{
color: theme.textColorSecondary,
marginLeft: 8,
}}
>
Downloaded only
</Text>
</View>
)}
@@ -218,49 +249,48 @@ 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)
);
<Actionbar
active={selectedNovels.length > 0}
theme={theme}
actions={[
{
icon: "check",
onPress: () => {
selectedNovels.map((id) =>
markAllChaptersRead(id)
),
setSelectedNovels([]);
dispatch(
getLibraryAction(sort, filter)
);
},
dispatch(
getLibraryAction(sort, filter)
);
},
{
icon: "delete-outline",
onPress: () => {
selectedNovels.map((id) =>
unfollowNovel(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}
@@ -268,6 +298,7 @@ const LibraryScreen = ({ navigation }) => {
dispatch={dispatch}
sort={sort}
filter={filter}
downloadedOnlyMode={downloadedOnlyMode}
/>
</Portal>
</>
@@ -20,6 +20,7 @@ const LibraryFilterSheet = ({
dispatch,
sort,
filter,
downloadedOnlyMode,
theme,
}) => {
const [animatedValue] = useState(new Animated.Value(0));
@@ -29,14 +30,19 @@ const LibraryFilterSheet = ({
<Checkbox
label="Downloaded"
theme={theme}
status={filter === "chaptersDownloaded > 0"}
onPress={() =>
filter === "chaptersDownloaded > 0"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(sort, "chaptersDownloaded > 0")
)
status={
filter === "chaptersDownloaded > 0" ||
downloadedOnlyMode === true
}
onPress={() => {
if (!downloadedOnlyMode) {
filter === "chaptersDownloaded > 0"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(sort, "chaptersDownloaded > 0")
);
}
}}
/>
<Checkbox
label="Unread"
@@ -239,8 +245,8 @@ const LibraryFilterSheet = ({
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 480, bottom: 0 }}
snappingPoints={[0, 480]}
draggableRange={{ top: 500, bottom: 0 }}
snappingPoints={[0, 500]}
>
<View
style={[
+13 -1
View File
@@ -1,13 +1,16 @@
import React, { useState } from "react";
import { Button, Dialog, Portal } from "react-native-paper";
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import { List } from "../../components/List";
import { useTheme } from "../../hooks/reduxHooks";
import { showToast } from "../../hooks/showToast";
import { deleteNovelCache } from "../../database/queries/NovelQueries";
import { Button, Dialog, Portal } from "react-native-paper";
import { clearUpdates } from "../../database/queries/UpdateQueries";
const AdvancedSettings = ({ navigation }) => {
const theme = useTheme();
@@ -30,6 +33,15 @@ const AdvancedSettings = ({ navigation }) => {
onPress={showClearDatabaseDialog}
theme={theme}
/>
<List.Item
title="Clear updates tab"
description="Clears chapter entries in updates tab"
onPress={() => {
clearUpdates();
showToast("Updates cleared.");
}}
theme={theme}
/>
</List.Section>
<Portal>
<Dialog
+262
View File
@@ -0,0 +1,262 @@
import React, { useState } from "react";
import { ScrollView, Text } from "react-native";
import { useDispatch } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import { List } from "../../components/List";
import { ThemePicker } from "../../components/ThemePicker/ThemePicker";
import SwitchSetting from "../../components/Switch/Switch";
import ColorPickerModal from "../../components/ColorPickerModal";
import { useSettings, useTheme } from "../../hooks/reduxHooks";
import {
setAccentColor,
setAmoledMode,
setAppSettings,
setAppTheme,
setRippleColor,
} from "../../redux/settings/settings.actions";
import {
darkTheme,
greenAppleTheme,
irisBlueTheme,
lightTheme,
midnightDuskTheme,
oceanicTheme,
springBlossomTheme,
strawberryDaiquiri,
takoLightTheme,
takoTheme,
yangTheme,
yinYangTheme,
yotsubaTheme,
} from "../../theme/theme";
const lightThemes = [
lightTheme,
springBlossomTheme,
takoLightTheme,
yangTheme,
yotsubaTheme,
];
const darkThemes = [
darkTheme,
midnightDuskTheme,
strawberryDaiquiri,
takoTheme,
greenAppleTheme,
yinYangTheme,
irisBlueTheme,
oceanicTheme,
];
const AppearanceSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const {
showHistoryTab = true,
showUpdatesTab = true,
showLabelsInNav = false,
hideBackdrop = false,
useFabForContinueReading = false,
} = useSettings();
/**
* Accent Color Modal
*/
const [accentColorModal, setAccentColorModal] = useState(false);
const showAccentColorModal = () => setAccentColorModal(true);
const hideAccentColorModal = () => setAccentColorModal(false);
const hexToRgb = (hex) => {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};
const onSubmit = (val) => {
dispatch(setAccentColor(val));
const rgb = hexToRgb(val);
const rgbaColor = `rgba(${rgb.r},${rgb.g},${rgb.b},0.12)`;
dispatch(setRippleColor(rgbaColor));
};
return (
<ScreenContainer theme={theme}>
<Appbar title="Appearance" onBackAction={navigation.goBack} />
<ScrollView style={{ flex: 1 }}>
<List.Section>
<List.SubHeader theme={theme}>App theme</List.SubHeader>
<Text
style={{
color: theme.textColorPrimary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Light Theme
</Text>
<ScrollView
contentContainerStyle={{
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: "row",
}}
horizontal={true}
showsHorizontalScrollIndicator={false}
>
{lightThemes.map((item) => (
<ThemePicker
key={item.id}
currentTheme={theme}
theme={item}
onPress={() => dispatch(setAppTheme(item.id))}
/>
))}
</ScrollView>
<Text
style={{
color: theme.textColorPrimary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Dark Theme
</Text>
<ScrollView
contentContainerStyle={{
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: "row",
}}
horizontal={true}
showsHorizontalScrollIndicator={false}
>
{darkThemes.map((item) => (
<ThemePicker
key={item.id}
currentTheme={theme}
theme={item}
onPress={() => dispatch(setAppTheme(item.id))}
/>
))}
</ScrollView>
{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.Divider theme={theme} />
<List.SubHeader theme={theme}>Novel info</List.SubHeader>
<SwitchSetting
label="Hide backdrop"
value={hideBackdrop}
onPress={() =>
dispatch(
setAppSettings("hideBackdrop", !hideBackdrop)
)
}
theme={theme}
/>
<SwitchSetting
label="Use FAB instead of button"
value={useFabForContinueReading}
onPress={() =>
dispatch(
setAppSettings(
"useFabForContinueReading",
!useFabForContinueReading
)
)
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Navbar</List.SubHeader>
<SwitchSetting
label="Show updates in the nav"
value={showUpdatesTab}
onPress={() =>
dispatch(
setAppSettings(
"showUpdatesTab",
!showUpdatesTab
)
)
}
theme={theme}
/>
<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.Section>
</ScrollView>
<ColorPickerModal
title="Accent color"
modalVisible={accentColorModal}
hideModal={hideAccentColorModal}
color={theme.colorAccent}
onSubmit={onSubmit}
theme={theme}
showAccentColors={true}
/>
</ScreenContainer>
);
};
export default AppearanceSettings;
+2 -2
View File
@@ -37,13 +37,13 @@ const BackupSettings = ({ navigation }) => {
onPress={restoreFullBackup}
theme={theme}
/>
<List.Divider 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}
/>
/> */}
<List.InfoItem
title="Create backup may not work on devices with Android 9 or lower."
icon="information-outline"
-1
View File
@@ -55,7 +55,6 @@ const DownloadQueue = ({ navigation }) => {
if (downloadQueue.length > 0) {
dispatch(cancelDownload());
showToast("Downloads cancelled.");
BackgroundService.stop();
}
closeMenu();
}}
+23 -136
View File
@@ -1,23 +1,17 @@
import React, { useState } from "react";
import { ScrollView } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { List } from "../../components/List";
import { ScreenContainer } from "../../components/Common";
import { useSettings, useTheme } from "../../hooks/reduxHooks";
import SwitchSetting from "../../components/Switch/Switch";
import DisplayModeModal from "./components/DisplayModeModal";
import GridSizeModal from "./components/GridSizeModal";
import ThemeModal from "./components/ThemeModal";
import {
setAccentColor,
setAmoledMode,
setAppSettings,
setRippleColor,
} from "../../redux/settings/settings.actions";
import ColorPickerModal from "../../components/ColorPickerModal";
import SwitchSetting from "../../components/Switch/Switch";
import { ScrollView } from "react-native";
import { useSettings, useTheme } from "../../hooks/reduxHooks";
import { setAppSettings } from "../../redux/settings/settings.actions";
import { SHOW_LAST_UPDATE_TIME } from "../../redux/updates/updates.types";
const GenralSettings = ({ navigation }) => {
@@ -31,9 +25,7 @@ const GenralSettings = ({ navigation }) => {
const {
updateLibraryOnLaunch = false,
downloadNewChapters = false,
showHistoryTab = true,
showUpdatesTab = true,
showLabelsInNav = false,
onlyUpdateOngoingNovels = false,
} = useSettings();
const { showLastUpdateTime = true } = useSelector(
@@ -58,13 +50,6 @@ const GenralSettings = ({ navigation }) => {
const showDisplayModal = () => setDisplayModalVisible(true);
const hideDisplayModal = () => setDisplayModalVisible(false);
/**
* Theme Modal
*/
const [themeModalVisible, setthemeModalVisible] = useState(false);
const showthemeModal = () => setthemeModalVisible(true);
const hidethemeModal = () => setthemeModalVisible(false);
/**
* Grid Size Modal
*/
@@ -72,75 +57,11 @@ const GenralSettings = ({ navigation }) => {
const showGridSizeModal = () => setGridSizeModalVisible(true);
const hideGridSizeModal = () => setGridSizeModalVisible(false);
/**
* Accent Color Modal
*/
const [accentColorModal, setAccentColorModal] = useState(false);
const showAccentColorModal = () => setAccentColorModal(true);
const hideAccentColorModal = () => setAccentColorModal(false);
const hexToRgb = (hex) => {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};
const onSubmit = (val) => {
dispatch(setAccentColor(val));
const rgb = hexToRgb(val);
const rgbaColor = `rgba(${rgb.r},${rgb.g},${rgb.b},0.12)`;
dispatch(setRippleColor(rgbaColor));
};
return (
<ScreenContainer theme={theme}>
<Appbar title="General" onBackAction={navigation.goBack} />
<ScrollView>
<ScrollView contentContainerStyle={{ paddingBottom: 32 }}>
<List.Section>
<SwitchSetting
label="Show updates in the nav"
value={showUpdatesTab}
onPress={() =>
dispatch(
setAppSettings(
"showUpdatesTab",
!showUpdatesTab
)
)
}
theme={theme}
/>
<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
@@ -170,6 +91,21 @@ const GenralSettings = ({ navigation }) => {
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Global update</List.SubHeader>
<SwitchSetting
label="Only update ongoing novels"
value={onlyUpdateOngoingNovels}
onPress={() =>
dispatch(
setAppSettings(
"onlyUpdateOngoingNovels",
!onlyUpdateOngoingNovels
)
)
}
theme={theme}
/>
<SwitchSetting
label="Show last update time"
value={showLastUpdateTime}
@@ -196,41 +132,6 @@ const GenralSettings = ({ navigation }) => {
}
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
@@ -247,20 +148,6 @@ const GenralSettings = ({ navigation }) => {
hideGridSizeModal={hideGridSizeModal}
theme={theme}
/>
<ThemeModal
dispatch={dispatch}
themeModalVisible={themeModalVisible}
hidethemeModal={hidethemeModal}
theme={theme}
/>
<ColorPickerModal
title="Accent color"
modalVisible={accentColorModal}
hideModal={hideAccentColorModal}
color={theme.colorAccent}
onSubmit={onSubmit}
theme={theme}
/>
</ScreenContainer>
);
};
+40 -3
View File
@@ -14,13 +14,17 @@ import { MoreHeader } from "./components/MoreHeader";
const MoreScreen = ({ navigation }) => {
const theme = useTheme();
const { downloadQueue } = useSelector((state) => state.downloadsReducer);
const { incognitoMode } = useSettings();
const { incognitoMode = false, downloadedOnlyMode = false } = useSettings();
const dispatch = useDispatch();
const enableIncognitoMode = () => {
dispatch(setAppSettings("incognitoMode", !incognitoMode));
};
const enableDownloadedOnlyMode = () => {
dispatch(setAppSettings("downloadedOnlyMode", !downloadedOnlyMode));
};
return (
<ScreenContainer theme={theme}>
<MoreHeader title="More" navigation={navigation} theme={theme} />
@@ -29,7 +33,40 @@ const MoreScreen = ({ navigation }) => {
android_ripple={{ color: theme.rippleColor }}
style={{
paddingHorizontal: 16,
paddingVertical: 12,
paddingVertical: 14,
flexDirection: "row",
justifyContent: "space-between",
}}
onPress={enableDownloadedOnlyMode}
>
<View style={{ flexDirection: "row" }}>
<List.Icon theme={theme} icon="cloud-off-outline" />
<View style={{ marginLeft: 16 }}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
>
Downloaded only
</Text>
<Text style={{ color: theme.textColorSecondary }}>
Filters all novels in your library
</Text>
</View>
</View>
<Switch
value={downloadedOnlyMode}
onValueChange={enableDownloadedOnlyMode}
color={theme.colorAccent}
style={{ marginRight: 8 }}
/>
</Pressable>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
paddingHorizontal: 16,
paddingVertical: 14,
flexDirection: "row",
justifyContent: "space-between",
}}
@@ -44,7 +81,7 @@ const MoreScreen = ({ navigation }) => {
fontSize: 16,
}}
>
Incognito Mode
Incognito mode
</Text>
<Text style={{ color: theme.textColorSecondary }}>
Pauses reading history
+33 -2
View File
@@ -23,8 +23,15 @@ import {
} from "../../components/Common/ToggleButton";
import { List } from "../../components/List";
import { useReaderSettings, useTheme } from "../../hooks/reduxHooks";
import { setReaderSettings } from "../../redux/settings/settings.actions";
import {
useReaderSettings,
useSettings,
useTheme,
} from "../../hooks/reduxHooks";
import {
setAppSettings,
setReaderSettings,
} from "../../redux/settings/settings.actions";
import {
readerBackground,
readerLineHeight,
@@ -51,6 +58,8 @@ const FirstRoute = React.memo(({ theme, dispatch, reader }) => {
{ value: "right", icon: "format-align-right" },
];
const { autoScrollInterval = 10 } = useSettings();
/**
* Reader Background Color Modal
*/
@@ -230,6 +239,28 @@ const FirstRoute = React.memo(({ theme, dispatch, reader }) => {
</Button>
</View>
</View>
<List.SubHeader theme={theme}>
Auto scroll interval (in seconds)
</List.SubHeader>
<View style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
<TextInput
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
defaultValue={autoScrollInterval.toString() ?? 10}
keyboardType="numeric"
onChangeText={(text) =>
text !== "" &&
dispatch(
setAppSettings(
"autoScrollInterval",
parseInt(text)
)
)
}
/>
</View>
</List.Section>
</ScrollView>
<ColorPickerModal
+10
View File
@@ -58,6 +58,16 @@ const SettingsScreen = ({ navigation }) => {
}
theme={theme}
/>
<List.Item
title="Appearance"
icon="palette-outline"
onPress={() =>
navigation.navigate("SettingsStack", {
screen: "AppearanceSettings",
})
}
theme={theme}
/>
<List.Item
title="Advanced"
icon="code-tags"
-58
View File
@@ -1,58 +0,0 @@
import React from "react";
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 renderThemeCheckboxes = () =>
themes.map((item) => (
<Checkbox
key={item.id}
status={theme.id === item.id}
label={item.name}
onPress={() => dispatch(setAppTheme(item.id))}
theme={theme}
/>
));
return (
<Portal>
<Modal
visible={themeModalVisible}
onDismiss={hidethemeModal}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
{renderThemeCheckboxes()}
</Modal>
</Portal>
);
};
export default ThemeModal;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
});
+166 -61
View File
@@ -10,8 +10,16 @@ import {
Text,
} from "react-native";
import { Provider, Portal, Appbar, IconButton, Menu } from "react-native-paper";
import {
Provider,
Portal,
Appbar,
IconButton,
Menu,
FAB,
} from "react-native-paper";
import { useDispatch, useSelector } from "react-redux";
import * as Haptics from "expo-haptics";
import {
bookmarkChapterAction,
@@ -31,19 +39,21 @@ import {
useContinueReading,
useNovel,
usePreferences,
useSettings,
useTheme,
} from "../../hooks/reduxHooks";
import { showToast } from "../../hooks/showToast";
import ChapterItem from "./components/ChapterItem";
import NovelInfoHeader from "./components/NovelHeader";
import NovelInfoHeader from "./components/Info/NovelInfoHeader";
import ChaptersSettingsSheet from "./components/ChaptersSettingsSheet";
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";
import { pickCustomNovelCover } from "../../database/queries/NovelQueries";
import FadeView from "../../components/Common/CrossFadeView";
const Novel = ({ route, navigation }) => {
const item = route.params;
@@ -73,6 +83,9 @@ const Novel = ({ route, navigation }) => {
novel.novelId
);
const { useFabForContinueReading = lastReadChapter && false } =
useSettings();
useEffect(() => {
dispatch(
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter)
@@ -132,12 +145,40 @@ const Novel = ({ route, navigation }) => {
};
const onSelectLongPress = (chapter) => {
if (isSelected(chapter.chapterId)) {
setSelected((selected) =>
selected.filter((item) => item.chapterId !== chapter.chapterId)
);
} else {
if (selected.length === 0) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
setSelected((selected) => [...selected, chapter]);
} else {
/**
* Select custom range
*/
const lastSelectedChapter = selected[selected.length - 1];
if (lastSelectedChapter.chapterId !== chapter.chapterId) {
if (lastSelectedChapter.chapterId > chapter.chapterId) {
setSelected((selected) => [
...selected,
chapter,
...chapters.filter(
(chap) =>
(chap.chapterId <= chapter.chapterId ||
chap.chapterId >=
lastSelectedChapter.chapterId) === false
),
]);
} else {
setSelected((selected) => [
...selected,
chapter,
...chapters.filter(
(chap) =>
(chap.chapterId >= chapter.chapterId ||
chap.chapterId <=
lastSelectedChapter.chapterId) === false
),
]);
}
}
}
};
@@ -171,6 +212,21 @@ const Novel = ({ route, navigation }) => {
}
};
const setCustomNovelCover = async () => {
const cover = await pickCustomNovelCover(novelId);
if (cover) {
dispatch(
setNovel({
...novel,
novelCover: cover,
})
);
}
showExtraMenu(false);
};
const renderItem = ({ item, index }) => (
<ChapterItem
theme={theme}
@@ -198,26 +254,29 @@ const Novel = ({ route, navigation }) => {
{ backgroundColor: theme.colorPrimaryDark },
]}
>
{selected.length > 0 ? (
<FadeView
style={{
position: "absolute",
zIndex: 1,
width: Dimensions.get("window").width,
elevation: 2,
}}
active={selected.length == 0}
animationDuration={100}
>
<View
style={{
backgroundColor: theme.colorPrimary,
position: "absolute",
zIndex: 1,
width: Dimensions.get("window").width,
paddingTop: StatusBar.currentHeight,
flexDirection: "row",
alignItems: "center",
paddingTop: StatusBar.currentHeight,
paddingBottom: 8,
elevation: 2,
}}
>
<Appbar.Action
icon="close"
color={theme.textColorPrimary}
onPress={() => {
setSelected([]);
}}
onPress={() => setSelected([])}
/>
<Appbar.Content
title={selected.length}
@@ -232,7 +291,8 @@ const Novel = ({ route, navigation }) => {
}}
/>
</View>
) : (
</FadeView>
{selected.length == 0 && (
<View
style={{
position: "absolute",
@@ -245,19 +305,42 @@ const Novel = ({ route, navigation }) => {
>
<IconButton
icon="arrow-left"
color="white"
color={theme.textColorPrimary}
size={24}
style={{ marginTop: StatusBar.currentHeight + 8 }}
onPress={() => navigation.goBack()}
/>
<Row>
<IconButton
icon="share-variant"
color={theme.textColorPrimary}
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
}}
onPress={() =>
Share.share({
message: novel.sourceUrl,
})
}
/>
<IconButton
icon="text-box-search-outline"
color={theme.textColorPrimary}
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
}}
onPress={() => showJumpToChapterModal(true)}
/>
<Menu
visible={downloadMenu}
onDismiss={() => showDownloadMenu(false)}
anchor={
<IconButton
icon="download"
color="white"
icon="download-outline"
color={theme.textColorPrimary}
size={24}
style={{
marginTop:
@@ -271,7 +354,7 @@ const Novel = ({ route, navigation }) => {
}}
>
<Menu.Item
title="Download all"
title="All"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{
color: theme.textColorPrimary,
@@ -286,6 +369,25 @@ const Novel = ({ route, navigation }) => {
)
}
/>
<Menu.Item
title="Unread"
style={{ backgroundColor: theme.menuColor }}
titleStyle={{
color: theme.textColorPrimary,
}}
onPress={() =>
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters.filter(
(chapter) =>
!!chapter.read === false
)
)
)
}
/>
<Menu.Item
title="Delete downloads"
style={{ backgroundColor: theme.menuColor }}
@@ -299,36 +401,14 @@ const Novel = ({ route, navigation }) => {
}
/>
</Menu>
<IconButton
icon="text-box-search-outline"
color="white"
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
}}
onPress={() => showJumpToChapterModal(true)}
/>
<IconButton
icon="share-variant"
color="white"
size={21}
style={{
marginTop: StatusBar.currentHeight + 8,
}}
onPress={() =>
Share.share({
message: novel.sourceUrl,
})
}
/>
<Menu
visible={extraMenu}
onDismiss={() => showExtraMenu(false)}
anchor={
<IconButton
icon="dots-vertical"
color="white"
color={theme.textColorPrimary}
size={21}
style={{
marginTop:
@@ -359,20 +439,7 @@ const Novel = ({ route, navigation }) => {
titleStyle={{
color: theme.textColorPrimary,
}}
onPress={async () => {
const cover = await setCustomNovelCover(
novelId
);
dispatch(
setNovel({
...novel,
novelCover: cover,
})
);
showExtraMenu(false);
}}
onPress={setCustomNovelCover}
/>
</Menu>
</Row>
@@ -387,6 +454,7 @@ const Novel = ({ route, navigation }) => {
windowSize={15}
initialNumToRender={7}
renderItem={renderItem}
contentContainerStyle={{ paddingBottom: 100 }}
ListHeaderComponent={
<NovelInfoHeader
item={item}
@@ -395,6 +463,7 @@ const Novel = ({ route, navigation }) => {
filter={filter}
loading={loading}
lastRead={lastReadChapter}
setCustomNovelCover={setCustomNovelCover}
dispatch={dispatch}
chapters={chapters}
navigation={navigation}
@@ -404,9 +473,39 @@ const Novel = ({ route, navigation }) => {
}
refreshControl={refreshControl()}
/>
{selected.length > 0 && (
{useFabForContinueReading &&
!loading &&
chapters.length > 0 &&
lastReadChapter && (
<FAB
style={[
styles.fab,
{ backgroundColor: theme.colorAccent },
]}
small
color={theme.colorButtonText}
uppercase={false}
label={novel.unread ? `Start` : `Resume`}
icon="play"
onPress={() =>
navigation.navigate("Chapter", {
chapterId: lastReadChapter.chapterId,
chapterUrl: lastReadChapter.chapterUrl,
novelUrl: novel.novelUrl,
novelId: lastReadChapter.novelId,
sourceId: novel.sourceId,
chapterName: lastReadChapter.chapterName,
novelName: novel.novelName,
bookmark: lastReadChapter.bookmark,
})
}
/>
)}
<Portal>
<Actionbar
active={selected.length > 0}
theme={theme}
style={{ marginBottom: 24 }}
actions={[
selected.some((obj) => obj.downloaded === 0) && {
icon: "download-outline",
@@ -475,7 +574,7 @@ const Novel = ({ route, navigation }) => {
},
]}
/>
)}
</Portal>
{!loading && (
<Portal>
<JumpToChapterModal
@@ -527,4 +626,10 @@ const styles = StyleSheet.create({
flexDirection: "row",
justifyContent: "space-between",
},
fab: {
position: "absolute",
margin: 16,
right: 0,
bottom: 16,
},
});
+69 -6
View File
@@ -1,5 +1,12 @@
import React, { useState } from "react";
import { FlatList, StyleSheet, Text, View } from "react-native";
import {
FlatList,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { Button, Modal, Portal, Switch, TextInput } from "react-native-paper";
@@ -25,6 +32,8 @@ const EditInfoModal = ({ theme, hideModal, modalVisible, novel, dispatch }) => {
setInfo({ ...info, genre: tags.join(",") });
};
const status = ["Ongoing", "Hiatus", "Completed", "Unknown", "Cancelled"];
return (
<Portal>
<Modal
@@ -43,6 +52,57 @@ const EditInfoModal = ({ theme, hideModal, modalVisible, novel, dispatch }) => {
>
Edit info
</Text>
<View
style={{
marginVertical: 8,
flexDirection: "row",
alignItems: "center",
}}
>
<Text style={{ color: theme.textColorSecondary }}>
Status:
</Text>
<ScrollView
style={{ marginLeft: 8 }}
horizontal
showsHorizontalScrollIndicator={false}
>
{status.map((item) => (
<View
style={{ borderRadius: 8, overflow: "hidden" }}
key={item}
>
<Pressable
style={{
backgroundColor:
info.status === item
? theme.rippleColor
: "transparent",
paddingVertical: 4,
paddingHorizontal: 8,
}}
android_ripple={{
color: theme.rippleColor,
}}
onPress={() =>
setInfo({ ...info, status: item })
}
>
<Text
style={{
color:
info.status === item
? theme.colorAccent
: theme.textColorSecondary,
}}
>
{item}
</Text>
</Pressable>
</View>
))}
</ScrollView>
</View>
<TextInput
placeholder={`Title: ${info.novelName}`}
style={{ fontSize: 14 }}
@@ -149,19 +209,22 @@ const GenreChip = ({ children, theme, onPress }) => (
style={{
flex: 1,
flexDirection: "row",
borderWidth: 1,
borderRadius: 50,
borderColor: theme.colorAccent,
paddingVertical: 2,
paddingVertical: 4,
paddingHorizontal: 8,
marginVertical: 4,
marginRight: 8,
justifyContent: "center",
alignItems: "center",
backgroundColor: theme.colorPrimary,
backgroundColor: theme.dividerColor,
}}
>
<Text style={{ color: theme.colorAccent, textTransform: "capitalize" }}>
<Text
style={{
color: theme.textColorSecondary,
textTransform: "capitalize",
}}
>
{children}
</Text>
<MaterialCommunityIcons
@@ -1,39 +1,110 @@
import React from "react";
import { FlatList, StyleSheet, Text, View, Pressable } from "react-native";
import React, { useState } from "react";
import {
FlatList,
StyleSheet,
Text,
View,
Pressable,
Dimensions,
StatusBar,
} from "react-native";
import { IconButton } from "react-native-paper";
import { IconButton, Portal } from "react-native-paper";
import { LinearGradient } from "expo-linear-gradient";
import easeGradient from "react-native-easing-gradient";
import FastImage from "react-native-fast-image";
import { TouchableWithoutFeedback } from "react-native-gesture-handler";
const NovelInfoContainer = ({ children }) => (
<View style={styles.novelInfoContainer}>{children}</View>
);
const CoverImage = ({ children, source, theme }) => {
const CoverImage = ({ children, source, theme, hideBackdrop }) => {
const { colors, locations } = easeGradient({
colorStops: {
0: { color: "rgba(0,0,0,0.2)" },
0: { color: "rgba(0,0,0,0)" },
1: { color: theme.colorPrimaryDark },
},
});
return (
<FastImage source={source} style={styles.coverImage}>
<LinearGradient
colors={colors}
locations={locations}
style={styles.linearGradient}
>
{children}
</LinearGradient>
</FastImage>
);
if (hideBackdrop) {
return <View>{children}</View>;
} else {
return (
<FastImage source={source} style={styles.coverImage}>
<View
style={{
flex: 1,
backgroundColor: `${theme.colorPrimaryDark}B4`,
}}
>
<LinearGradient
colors={colors}
locations={locations}
style={styles.linearGradient}
>
{children}
</LinearGradient>
</View>
</FastImage>
);
}
};
const NovelThumbnail = ({ source }) => (
<FastImage source={source} style={styles.novelThumbnail} />
);
const NovelThumbnail = ({ source, setCustomNovelCover }) => {
const [expanded, setExpanded] = useState(false);
if (!expanded) {
return (
<TouchableWithoutFeedback onPress={() => setExpanded(!expanded)}>
<FastImage source={source} style={styles.novelThumbnail} />
</TouchableWithoutFeedback>
);
} else {
return (
<Portal>
<IconButton
icon="close"
style={{
position: "absolute",
top: StatusBar.currentHeight,
left: 0,
zIndex: 10,
}}
onPress={() => setExpanded(false)}
/>
<IconButton
icon="pencil-outline"
style={{
position: "absolute",
top: StatusBar.currentHeight,
right: 0,
zIndex: 10,
}}
onPress={setCustomNovelCover}
/>
<Pressable
style={{
position: "absolute",
width: Dimensions.get("window").width,
height: Dimensions.get("window").height + 60,
justifyContent: "center",
backgroundColor: "rgba(0,0,0,0.7)",
}}
onPress={() => setExpanded(false)}
>
<FastImage
source={source}
style={{
width: Dimensions.get("window").width,
height: (Dimensions.get("window").width * 3) / 2,
}}
/>
</Pressable>
</Portal>
);
}
};
const NovelTitle = ({ theme, children, onLongPress, onPress }) => (
<Text
@@ -133,9 +204,8 @@ const GenreChip = ({ children, theme }) => (
style={[
styles.genreChip,
{
color: theme.colorAccent,
borderColor: theme.colorAccent,
backgroundColor: theme.colorPrimary,
color: theme.textColorPrimary,
backgroundColor: theme.dividerColor,
},
]}
>
@@ -176,34 +246,33 @@ export {
const styles = StyleSheet.create({
novelInfoContainer: {
flex: 1,
flexDirection: "row",
margin: 16,
marginTop: 28,
marginBottom: 0,
paddingTop: 90,
},
coverImage: {
height: 285,
height: 270,
},
linearGradient: {
height: 286,
height: 271,
},
novelThumbnail: {
height: 160,
width: 110,
margin: 3.2,
height: 150,
width: 100,
marginHorizontal: 4,
borderRadius: 6,
},
novelTitle: {
fontWeight: "bold",
fontSize: 18,
},
novelAuthor: {
marginVertical: 3,
marginVertical: 5,
fontSize: 14,
fontWeight: "bold",
},
novelInfo: {
marginVertical: 3,
fontSize: 14,
},
followButton: {
@@ -216,18 +285,16 @@ const styles = StyleSheet.create({
elevation: 0,
},
novelGenres: {
// marginTop: 12,
paddingHorizontal: 16,
paddingBottom: 16,
marginBottom: 8,
},
genreChip: {
flex: 1,
justifyContent: "center",
paddingVertical: 2,
paddingHorizontal: 10,
paddingVertical: 5,
paddingHorizontal: 12,
marginHorizontal: 2,
fontSize: 13,
borderWidth: 1,
fontSize: 12,
borderRadius: 50,
textTransform: "capitalize",
},
@@ -1,18 +1,13 @@
import React, { memo } from "react";
import {
View,
Text,
StyleSheet,
Clipboard,
Pressable,
Button,
} from "react-native";
import { View, Text, StyleSheet, Clipboard, Pressable } from "react-native";
import * as WebBrowser from "expo-web-browser";
import { IconButton } from "react-native-paper";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { followNovelAction } from "../../../redux/novel/novel.actions";
import { useTrackingStatus } from "../../../hooks/reduxHooks";
import { followNovelAction } from "../../../../redux/novel/novel.actions";
import { useTrackingStatus, useSettings } from "../../../../hooks/reduxHooks";
import { showToast } from "../../../../hooks/showToast";
import {
CoverImage,
@@ -24,11 +19,20 @@ import {
FollowButton,
TrackerButton,
NovelGenres,
} from "./Info/NovelDetailsComponents";
import { Row } from "../../../components/Common";
import NovelSummary from "./Info/NovelSummary";
import ReadButton from "./Info/ReadButton";
import { showToast } from "../../../hooks/showToast";
} from "./NovelInfoComponents";
import { Row } from "../../../../components/Common";
import NovelSummary from "./NovelSummary";
import ReadButton from "./ReadButton";
// const getStatusIcon = (status) => {
// const icons = {
// Ongoing: "clock-outline",
// Completed: "check-all",
// Unknown: "help",
// };
// return icons[status] || icons.Unknown;
// };
const NovelInfoHeader = ({
item,
@@ -41,6 +45,7 @@ const NovelInfoHeader = ({
lastRead,
navigation,
trackerSheetRef,
setCustomNovelCover,
chaptersSettingsSheetRef,
}) => {
const { tracker, trackedNovels } = useTrackingStatus();
@@ -51,33 +56,39 @@ const NovelInfoHeader = ({
isTracked = trackedNovels.find((obj) => obj.novelId === novel.novelId);
}
const { hideBackdrop = false } = useSettings();
const getNovelCoverUrl = () => {
const defaultCover =
"https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true";
if (loading) {
if (item.novelCover && !item.novelCover.startsWith("/")) {
return item.novelCover;
} else {
return defaultCover;
}
} else {
if (novel.novelCover && !novel.novelCover.startsWith("/")) {
return novel.novelCover;
} else {
return defaultCover;
}
}
};
return (
<View>
<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",
}}
source={{ uri: getNovelCoverUrl() }}
theme={theme}
hideBackdrop={hideBackdrop}
>
<NovelInfoContainer>
<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",
}}
source={{ uri: getNovelCoverUrl() }}
theme={theme}
setCustomNovelCover={setCustomNovelCover}
/>
<View style={styles.novelDetails}>
<NovelTitle
@@ -96,19 +107,32 @@ const NovelInfoHeader = ({
>
{item.novelName}
</NovelTitle>
{!loading && (
<>
<NovelAuthor theme={theme}>
{novel.author || "Unknown author"}
</NovelAuthor>
<>
<NovelAuthor theme={theme}>
{!loading && novel.author
? novel.author
: "Unknown author"}
</NovelAuthor>
<Row>
{/* <MaterialCommunityIcons
name={
loading
? "help"
: getStatusIcon(novel.status)
}
size={16}
color={theme.textColorSecondary}
style={{ marginRight: 4 }}
/> */}
<NovelInfo theme={theme}>
{novel.status || "Unknown status"}
{!loading
? (novel.status || "Unknown status") +
" • " +
novel.source
: "Unknown status • Unknown source"}
</NovelInfo>
<NovelInfo theme={theme}>
{novel.source}
</NovelInfo>
</>
)}
</Row>
</>
</View>
</NovelInfoContainer>
</CoverImage>
@@ -119,6 +143,7 @@ const NovelInfoHeader = ({
justifyContent: "space-around",
paddingHorizontal: 16,
flex: 1,
marginTop: 12,
}}
>
<FollowButton
@@ -238,7 +263,7 @@ const NovelInfoHeader = ({
styles.chapters,
]}
>
{`${chapters.length} Chapters`}
{`${chapters.length} chapters`}
</Text>
<IconButton
icon="filter-variant"
@@ -262,8 +287,9 @@ export default memo(NovelInfoHeader);
const styles = StyleSheet.create({
novelDetails: {
flex: 1,
paddingHorizontal: 16,
paddingTop: 8,
paddingBottom: 16,
paddingLeft: 12,
justifyContent: "center",
},
chapters: {
paddingHorizontal: 16,
@@ -1,34 +1,57 @@
import React, { useState } from "react";
import React, { useCallback, useState } from "react";
import { View, Text, StyleSheet } from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
const NovelSummary = ({ summary, followed, theme }) => {
const [more, showMore] = useState(!followed);
const [expanded, setExpanded] = useState(!followed);
const maxNumberOfLines = expanded ? Number.MAX_SAFE_INTEGER : 3;
const expandNovelSummary = () => setExpanded(!expanded);
return (
<View style={styles.summaryContainer}>
<View
style={[
styles.summaryContainer,
{ paddingBottom: expanded ? 20 : 8 },
]}
>
<Text
style={{
color: theme.textColorSecondary,
lineHeight: 20,
}}
numberOfLines={more ? 200 : 2}
onPress={() => showMore(!more)}
numberOfLines={maxNumberOfLines}
onPress={expandNovelSummary}
ellipsizeMode="clip"
>
{summary}
</Text>
<Text
style={[
{
color: theme.colorAccent,
backgroundColor: theme.colorPrimaryDark,
},
styles.showMoreButton,
]}
onPress={() => showMore(!more)}
<View
style={{
backgroundColor: `${theme.colorPrimaryDark}D1`,
position: "absolute",
flex: 1,
justifyContent: "center",
alignItems: "center",
left: 0,
right: 0,
bottom: expanded ? 0 : 4,
}}
>
{more ? "Less" : "More"}
</Text>
<MaterialCommunityIcons
name={expanded ? "chevron-up" : "chevron-down"}
color={theme.textColorPrimary}
size={24}
onPress={expandNovelSummary}
style={{
borderRadius: 50,
backgroundColor: theme.colorPrimaryDark,
paddingHorizontal: 2,
}}
/>
</View>
</View>
);
};
@@ -39,14 +62,8 @@ const styles = StyleSheet.create({
summaryContainer: {
paddingHorizontal: 16,
marginTop: 8,
marginBottom: 16,
marginBottom: 8,
},
// summaryHeader: {
// marginTop: 5,
// paddingVertical: 5,
// fontSize: 15,
// fontWeight: "bold",
// },
showMoreButton: {
fontWeight: "bold",
position: "absolute",
+55 -43
View File
@@ -2,57 +2,69 @@ import React from "react";
import { StyleSheet } from "react-native";
import { Button } from "react-native-paper";
import { useSettings } from "../../../../hooks/reduxHooks";
const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
return (
chapters.length > 0 &&
(lastRead ? (
<Button
color="white"
style={[
{ backgroundColor: theme.colorAccent },
styles.startButton,
]}
uppercase={false}
labelStyle={{ letterSpacing: 0, color: theme.colorButtonText }}
onPress={() => {
navigation.navigate("Chapter", {
chapterId: lastRead.chapterId,
chapterUrl: lastRead.chapterUrl,
novelUrl: novel.novelUrl,
novelId: lastRead.novelId,
sourceId: novel.sourceId,
chapterName: lastRead.chapterName,
novelName: novel.novelName,
bookmark: lastRead.bookmark,
});
}}
>
{novel.unread ? `Start reading ` : `Continue reading `}
{lastRead.chapterName}
</Button>
) : (
<Button
color={theme.textColorHint}
style={[
styles.startButton,
{ backgroundColor: theme.colorDisabled },
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
>
All chapters read
</Button>
))
);
const { useFabForContinueReading = false } = useSettings();
const navigateToLastReadChapter = () => {
navigation.navigate("Chapter", {
chapterId: lastRead.chapterId,
chapterUrl: lastRead.chapterUrl,
novelUrl: novel.novelUrl,
novelId: lastRead.novelId,
sourceId: novel.sourceId,
chapterName: lastRead.chapterName,
novelName: novel.novelName,
bookmark: lastRead.bookmark,
});
};
if (!useFabForContinueReading) {
return (
chapters.length > 0 &&
(lastRead ? (
<Button
color="white"
style={[
{ backgroundColor: theme.colorAccent },
styles.startButton,
]}
uppercase={false}
labelStyle={{
letterSpacing: 0,
color: theme.colorButtonText,
}}
onPress={navigateToLastReadChapter}
>
{novel.unread ? `Start reading ` : `Continue reading `}
{lastRead.chapterName}
</Button>
) : (
<Button
color={theme.textColorHint}
style={[
styles.startButton,
{ backgroundColor: theme.colorDisabled },
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
>
All chapters read
</Button>
))
);
} else {
return null;
}
};
export default ReadButton;
const styles = StyleSheet.create({
startButton: {
marginTop: 8,
marginBottom: 16,
marginTop: 16,
marginBottom: 8,
marginHorizontal: 16,
},
});
+1
View File
@@ -182,6 +182,7 @@ const Updates = ({ navigation }) => {
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingTop: 8,
paddingBottom: 4,
textAlign: "center",
}}
>
+34 -8
View File
@@ -121,8 +121,19 @@ export const updateNovelChapters = async (
});
};
const sleep = (time) =>
new Promise((resolve) => setTimeout(() => resolve(), time));
export const updateAllNovels = async (updateSettings) => {
const libraryNovels = await getLibrary();
let libraryNovels = await getLibrary();
const { onlyUpdateOngoingNovels = false } = updateSettings;
if (onlyUpdateOngoingNovels) {
libraryNovels = libraryNovels.filter(
(item) => item.status !== "Completed"
);
}
const options = {
taskName: "Library Update",
@@ -143,9 +154,6 @@ export const updateAllNovels = async (updateSettings) => {
},
};
const sleep = (time) =>
new Promise((resolve) => setTimeout(() => resolve(), time));
const veryIntensiveTask = async (taskData) =>
await new Promise(async (resolve) => {
for (
@@ -155,13 +163,19 @@ export const updateAllNovels = async (updateSettings) => {
) {
try {
if (BackgroundService.isRunning()) {
/**
* Update chapters
*/
await updateNovelChapters(
libraryNovels[i].sourceId,
libraryNovels[i].novelUrl,
libraryNovels[i].novelId,
updateSettings
);
// console.log(libraryNovels[i].novelName + " Updated");
/**
* Update notification
*/
await BackgroundService.updateNotification({
taskTitle: libraryNovels[i].novelName,
taskDesc:
@@ -175,9 +189,13 @@ export const updateAllNovels = async (updateSettings) => {
value: i + 1,
},
});
/**
* When updating library is finished
*/
if (libraryNovels.length === i + 1) {
resolve();
await BackgroundService.stop();
Notifications.scheduleNotificationAsync({
content: {
title: "Library Updated",
@@ -189,13 +207,21 @@ export const updateAllNovels = async (updateSettings) => {
});
}
await sleep(taskData.delay);
const nextNovelIndex = i + 1;
if (
nextNovelIndex in libraryNovels &&
libraryNovels[nextNovelIndex].sourceId ===
libraryNovels[i].sourceId
) {
await sleep(taskData.delay);
}
}
} catch (error) {
showToast(
libraryNovels[i].novelName + ": " + error.message
);
break;
continue;
}
}
});
+166
View File
@@ -0,0 +1,166 @@
import cheerio from "react-native-cheerio";
import { Status } from "../helpers/constants";
const baseUrl = "https://arthurscan.xyz/";
const sourceId = 80;
const sourceName = "ArthurScans (Br)";
const popularNovels = async (page) => {
let url = baseUrl + "/manga/page/" + page + "/?m_orderby=rating";
const totalPages = 2;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".page-item-detail").each(function () {
const novelName = $(this).find(".item-summary h3").text().trim();
const novelCover = $(this).find(".img-responsive").attr("data-src");
let novelUrl = $(this).find("div > a").attr("href");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { totalPages, novels };
};
const parseNovelAndChapters = async (novelUrl) => {
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novel = { sourceId, url, sourceName };
$(".post-title > h3 > span").remove();
novel.novelUrl = novelUrl;
novel.novelName = $(".post-title > h1").text().trim();
novel.novelCover = $(".summary_image > a > img").attr("data-src");
$(".post-content_item").each(function () {
const detailName = $(this).find(".summary-heading > h5").text().trim();
const detail = $(this).find(".summary-content").text().trim();
switch (detailName) {
case "Genre(s)":
novel.genre = detail.replace(/[\t\n]/g, ",");
break;
case "Author(s)":
novel.author = detail;
break;
case "Status":
novel.status = detail.includes("OnGoing")
? Status.ONGOING
: Status.COMPLETED;
break;
}
});
$(".description-summary > div.summary__content").find("em").remove();
$(".premium-block").remove();
novel.summary = $("div.summary__content").text().trim();
let novelChapters = [];
const data = await fetch(novelUrl + "ajax/chapters", { method: "POST" });
const text = await data.text();
$ = cheerio.load(text);
$(".wp-manga-chapter").each(function () {
$("i").remove();
const chapterName = $(this).find("a").text().trim();
const releaseDate = null;
const chapterUrl = $(this).find("a").attr("href");
const chapter = { chapterName, releaseDate, chapterUrl };
novelChapters.push(chapter);
});
novel.chapters = novelChapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
const chapterName = $("#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 (result) {
const novelName = $(this).find(".post-title > h3").text().trim();
const novelCover = $(this).find("div > div > a > img").attr("data-src");
let novelUrl = $(this).find("div > div > a").attr("href");
novelUrl = novelUrl.replace(`${baseUrl}/`, "");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
};
const ArthurScansScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default ArthurScansScraper;
+164
View File
@@ -0,0 +1,164 @@
import cheerio from "react-native-cheerio";
import { Status } from "../helpers/constants";
const baseUrl = "https://reaperscans.com.br/";
const sourceId = 79;
const sourceName = "ReaperScans (Br)";
const popularNovels = async (page) => {
let url =
baseUrl +
"/all-series/todas-as-series/page/" +
page +
"/?m_orderby=rating";
const totalPages = 1;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".page-item-detail").each(function (result) {
const novelName = $(this).find(".item-summary h3").text().trim();
const novelCover = $(this).find(".img-responsive").attr("data-src");
let novelUrl = $(this).find("div > a").attr("href");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { totalPages, novels };
};
const parseNovelAndChapters = async (novelUrl) => {
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novel = { sourceId, url, sourceName };
$(".post-title > h3 > span").remove();
novel.novelUrl = novelUrl;
novel.novelName = $(".post-title > h1").text().trim();
novel.novelCover = $(".summary_image > a > img").attr("data-src");
$(".post-content_item").each(function () {
const detailName = $(this).find(".summary-heading > h5").text().trim();
const detail = $(this).find(".summary-content").text().trim();
switch (detailName) {
case "Genre(s)":
novel.genre = detail.replace(/[\t\n]/g, ",");
break;
case "Author(s)":
novel.author = detail;
break;
case "Status":
novel.status = detail.includes("OnGoing")
? Status.ONGOING
: Status.COMPLETED;
break;
}
});
$(".description-summary > div.summary__content").find("em").remove();
$(".premium-block").remove();
novel.summary = $("div.summary__content").text().trim();
let novelChapters = [];
$(".wp-manga-chapter").each(function () {
$("i").remove();
const chapterName = $(this).find("a").text().trim();
const releaseDate = null;
const chapterUrl = $(this).find("a").attr("href");
const chapter = { chapterName, releaseDate, chapterUrl };
novelChapters.push(chapter);
});
novel.chapters = novelChapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
const chapterName = $("#chapter-heading").text();
let 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 (result) {
const novelName = $(this).find(".post-title > h3").text().trim();
const novelCover = $(this).find("div > div > a > img").attr("data-src");
let novelUrl = $(this).find("div > div > a").attr("href");
novelUrl = novelUrl.replace(`${baseUrl}/`, "");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
};
const ReaperScansBrScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default ReaperScansBrScraper;
+1 -1
View File
@@ -18,7 +18,7 @@ const popularNovels = async (page) => {
let novels = [];
$(".update_item.list_category").each(function () {
const novelName = $(this).find("h3").text();
const novelName = $(this).find("h3").text().trim();
const novelCover = $(this).find("img").attr("src");
let novelUrl = $(this).find("h3 > a").attr("href");
+140
View File
@@ -0,0 +1,140 @@
import cheerio from "react-native-cheerio";
import { Status } from "../helpers/constants";
const sourceId = 78;
const sourceName = "RainOfSnow";
const baseUrl = "https://rainofsnow.com/";
const popularNovels = async (page) => {
let url = baseUrl + "novels/page/" + page;
const totalPages = 5;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".minbox").each(function () {
const novelName = $(this).find("h3").text();
const novelCover = $(this).find("img").attr("data-src");
const novelUrl = $(this).find("h3 > 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 = $("h2").text().trim();
novel.novelCover = $(".imagboca1 > img").attr("data-src");
novel.summary = $("#synop").text().trim();
novel.genre = $(
"body > div.queen > div > div > div.row > div.col-md-12.col-lg-7 > div > div.backcolor1 > ul > li:nth-child(5) > small"
)
.text()
.trim();
novel.author = $(
"body > div.queen > div > div > div.row > div.col-md-12.col-lg-7 > div > div.backcolor1 > ul > li:nth-child(2) > small"
).text();
novel.status = Status.UNKNOWN;
let novelChapters = [];
$("#chapter")
.find("li")
.each(function () {
const chapterName = $(this).find(".chapter").first().text().trim();
const releaseDate = $(this).find("small").text();
const chapterUrl = $(this).find("a").attr("href");
novelChapters.push({ chapterName, releaseDate, chapterUrl });
});
novel.chapters = novelChapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const result = await fetch(chapterUrl);
const body = await result.text();
$ = cheerio.load(body);
let chapterName = $(".content > h2").text();
let chapterText = $(".content").html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async (searchTerm) => {
let url = baseUrl + "?s=" + searchTerm;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".minbox").each(function () {
const novelName = $(this).find("h3").text();
const novelCover = $(this).find("img").attr("data-src");
const novelUrl = $(this).find("h3 > a").attr("href");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
};
const RainOfSnowScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default RainOfSnowScraper;
+4 -3
View File
@@ -6,9 +6,9 @@ const sourceName = "ReadLightNovel";
const lang = "en";
const baseUrl = "https://www.readlightnovel.org";
const baseUrl = "https://www.readlightnovel.me";
const searchUrl = "https://www.readlightnovel.org/detailed-search";
const searchUrl = "https://www.readlightnovel.me/detailed-search";
const popularNovels = async (page) => {
let totalPages = 1751;
@@ -64,7 +64,8 @@ const parseNovelAndChapters = async (novelUrl) => {
switch (detailName) {
case "Genre":
genre = detail.replace(/[\t\n]/g, ",");
genre = detail.trim().replace(/\s{2,}/g, ",");
break;
case "Author(s)":
author = detail;
+16 -40
View File
@@ -4,6 +4,7 @@ import { Status } from "../helpers/constants";
const baseUrl = "https://reaperscans.com";
const sourceId = 67;
const sourceName = "ReaperScans";
const popularNovels = async (page) => {
let url = `${baseUrl}/all-series/novels/`;
@@ -17,7 +18,7 @@ const popularNovels = async (page) => {
let novels = [];
$(".page-item-detail").each(function (result) {
const novelName = $(this).find(".item-summary h3").text();
const novelName = $(this).find(".item-summary h3").text().trim();
const novelCover = $(this).find(".img-responsive").attr("data-src");
let novelUrl = $(this).find("div > a").attr("href");
@@ -44,46 +45,31 @@ const parseNovelAndChapters = async (novelUrl) => {
$ = cheerio.load(body);
let novel = {};
let novel = { sourceId, url, sourceName };
$(".post-title > h3 > span").remove();
novel.sourceId = 67;
novel.sourceName = "ReaperScans";
novel.url = url;
novel.novelUrl = novelUrl;
novel.novelName = $(".post-title > h1").text().trim();
novel.novelCover = $(".summary_image > a > img").attr("data-src");
$(".post-content_item").each(function (result) {
detailName = $(this)
.find(".summary-heading > h5")
.text()
.replace(/[\t\n]/g, "")
.trim();
detail = $(this)
.find(".summary-content")
.text()
.replace(/[\t\n]/g, "")
.trim();
$(".post-content_item").each(function () {
const detailName = $(this).find(".summary-heading > h5").text().trim();
const detail = $(this).find(".summary-content").text().trim();
switch (detailName) {
case "Genre(s)":
novel.genre = detail.trim().replace(/[\t\n]/g, ",");
novel.genre = detail.replace(/[\t\n]/g, ",");
break;
case "Author(s)":
novel.author = detail.trim();
novel.author = detail;
break;
case "Status":
novel.status =
detail.trim() === "OnGoing"
? Status.ONGOING
: Status.COMPLETED;
novel.status = detail.includes("OnGoing")
? Status.ONGOING
: Status.COMPLETED;
break;
}
});
@@ -91,25 +77,15 @@ const parseNovelAndChapters = async (novelUrl) => {
$(".description-summary > div.summary__content").find("em").remove();
$(".premium-block").remove();
novel.summary = $(".description-summary > div.summary__content")
.text()
.trim()
.replace(/Description|Summary/g, "");
novel.summary = $("div.summary__content").text().trim();
let novelChapters = [];
$(".wp-manga-chapter").each(function (result) {
const chapterName = $(this)
.find("a")
.text()
.replace(/[\t\n]/g, "")
.trim();
$("i").remove();
const releaseDate = $(this)
.find(".chapter-release-date")
.text()
.replace(/[\t\n]/g, "")
.trim();
const chapterName = $(this).find("a").text().trim();
const releaseDate = null;
const chapterUrl = $(this).find("a").attr("href").replace(url, "");
@@ -155,7 +131,7 @@ const searchNovels = async (searchTerm) => {
let novels = [];
$(".c-tabs-item__content").each(function (result) {
const novelName = $(this).find(".post-title > h3").text();
const novelName = $(this).find(".post-title > h3").text().trim();
const novelCover = $(this).find("div > div > a > img").attr("data-src");
let novelUrl = $(this).find("div > div > a").attr("href");
+156
View File
@@ -0,0 +1,156 @@
import cheerio from "react-native-cheerio";
const sourceId = 76;
const sourceName = "TravisTranslations";
const baseUrl = "https://travistranslations.com/";
const popularNovels = async (page) => {
let url = baseUrl + "all-series/";
if (page > 1) {
url += `page/${page}/`;
}
const totalPages = 19;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".group").each(function () {
const novelName = $(this).find("h3").text();
const novelCover = "https://" + $(this).find("img").attr("data-src");
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").text().trim();
novel.novelCover = "https://" + $("img.object-cover").attr("data-src");
novel.summary = $("div[property=description]").text().trim();
novel.genre = "";
$("div.flex.flex-col.mt-3.text-center > ul > li").each(function () {
novel.genre +=
$(this)
.text()
.replace(/[\t\n]/g, "") + ",";
});
$("div.header-stats > span").each(function () {
if ($(this).find("small").text() === "Status") {
novel.status = $(this).find("strong").text();
}
});
novel.genre = novel.genre.slice(0, -1);
novel.author = $(".author > a > span").text();
let novelChapters = [];
$(".tab_content > ul.grid")
.find("li")
.each(function () {
const chapterName = $(this).find("span").first().text().trim();
const releaseDate = null;
const chapterUrl = $(this).find("a").attr("href");
novelChapters.push({ chapterName, releaseDate, chapterUrl });
});
novel.chapters = novelChapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const result = await fetch(chapterUrl);
let body = await result.text();
$ = cheerio.load(body);
const chapterId = $("input[name=id]").attr("value");
const url =
"https://travistranslations.com/wp-json/wp/v2/posts/" + chapterId;
const data = await fetch(url);
body = await data.json();
let chapterName = body.title.rendered;
let chapterText = body.content.rendered;
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async (searchTerm) => {
let url = baseUrl + "all-series/?search=" + searchTerm;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".group").each(function () {
const novelName = $(this).find("h3").text();
const novelCover = "https://" + $(this).find("img").attr("data-src");
const novelUrl = $(this).find(" a").attr("href");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
};
const TravisTranslationsScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default TravisTranslationsScraper;
+41 -52
View File
@@ -1,10 +1,14 @@
import cheerio from "react-native-cheerio";
import { defaultCoverUri, Status } from "../helpers/constants";
const sourceId = 23;
const sourceName = "TuNovelaLigera";
const baseUrl = "https://tunovelaligera.com/";
const popularNovels = async (page) => {
let totalPages = 1;
let url = baseUrl + "mejores-novelas-originales/";
let totalPages = 62;
let url = baseUrl + "novelas/page/" + page + "/?m_orderby=rating";
const result = await fetch(url);
const body = await result.text();
@@ -13,7 +17,7 @@ const popularNovels = async (page) => {
let novels = [];
$(".page-item-detail").each(function (result) {
$(".page-item-detail").each(function () {
const novelName = $(this).find(".h5 > a").text();
const novelCover = $(this).find("img").attr("src");
@@ -21,7 +25,7 @@ const popularNovels = async (page) => {
novelUrl += "/";
const novel = {
sourceId: 23,
sourceId,
novelName,
novelCover,
novelUrl,
@@ -41,63 +45,48 @@ const parseNovelAndChapters = async (novelUrl) => {
$ = cheerio.load(body);
let novel = {};
novel.sourceId = 23;
novel.sourceName = "Tunovelaligera";
novel.url = url;
let novel = { sourceId, sourceName, url };
novel.novelUrl = novelUrl;
novel.novelName = $("h1.titulo-novela")
.text()
.replace(/[\t\n]/g, "")
.trim();
$(".manga-title-badges").remove();
novel.novelCover = $(".summary_image > a > img").attr("src");
novel.novelName = $(".post-title > h1").text().trim();
$(".post-content_item").each(function (result) {
detailName = $(this)
.find(".summary-heading > h5")
.text()
.replace(/[\t\n]/g, "")
.trim();
detail = $(this)
.find(".summary-content")
.text()
.replace(/[\t\n]/g, "")
.trim();
novelCover = $(".summary_image > a > img");
novel[detailName] = detail;
novel.novelCover =
novelCover.attr("data-src") ||
novelCover.attr("src") ||
defaultCoverUri;
$(".post-content_item").each(function () {
const detailName = $(this).find(".summary-heading > h5").text().trim();
const detail = $(this).find(".summary-content").text().trim();
switch (detailName) {
case "Generos":
novel.genre = detail.replace(/, /g, ",");
break;
case "Autores":
novel.author = detail;
break;
case "Estado":
novel.status =
detail.includes("OnGoing") || detail.includes("Updating")
? Status.ONGOING
: Status.COMPLETED;
break;
}
});
novel.genre = novel.Generos.replace(/, /g, ",");
novel.author = novel.Autores;
novel.status = novel.Estado;
novel.status = null;
novel.artist = novel["Artista(s)"];
delete novel.Genre;
delete novel["Artista(s)"];
delete novel.Autores;
delete novel.Estado;
let novelSummary = "";
$(".summary__content")
.find("p")
.each(function () {
novelSummary += $(this).html();
});
novel.summary = novelSummary;
novel.summary = $("div.summary__content > p").text().trim();
let novelChapters = [];
const novelId = $(".wp-manga-action-button").attr("data-post");
const novelId =
$(".rating-post-id").attr("value") ||
$("#manga-chapters-holder").attr("data-id");
let formData = new FormData();
formData.append("action", "manga_get_chapters");
@@ -151,7 +140,7 @@ const parseChapter = async (novelUrl, chapterUrl) => {
novelUrl = novelUrl + "/";
const chapter = {
sourceId: 23,
sourceId,
novelUrl,
chapterUrl,
chapterName,
@@ -179,7 +168,7 @@ const searchNovels = async (searchTerm) => {
novelUrl = novelUrl.replace(`${baseUrl}novelas/`, "");
const novel = {
sourceId: 23,
sourceId,
novelName,
novelCover,
novelUrl,
+151
View File
@@ -0,0 +1,151 @@
import cheerio from "react-native-cheerio";
const sourceId = 77;
const sourceName = "NovelDeGlace";
const baseUrl = "https://noveldeglace.com/";
const popularNovels = async (page) => {
const totalPages = 1;
let url = baseUrl + "roman";
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$("article").each(function () {
const novelName = $(this).find("h2").text().trim();
const novelCover = $(this).find("img").attr("src");
const novelUrl = $(this).find("h2 > a").attr("href").split("/")[4];
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { novels, totalPages };
};
const parseNovelAndChapters = async (novelUrl) => {
const url = baseUrl + "roman/" + novelUrl;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novel = { sourceId, sourceName, url, novelUrl };
novel.novelName = $(
"div.entry-content > div > strong"
)[0].nextSibling.nodeValue.trim();
novel.novelCover = $(".su-row > div > div > img").attr("src");
novel.summary = $("div[data-title=Synopsis]").text();
author = $(
"div.romans > div.project-large > div.su-row > div.su-column.su-column-size-3-4 > div > div:nth-child(3) > strong"
)[0];
novel.author = author ? author.nextSibling.nodeValue.trim() : null;
novel.genre = $(".genre")
.text()
.replace("Genre : ", "")
.replace(/, /g, ",");
let novelChapters = [];
$(".chpt").each(function () {
const chapterName = $(this).find("a").text().trim();
const releaseDate = null;
const chapterUrl = $(this).find("a").attr("href");
const novel = {
sourceId,
chapterName,
releaseDate,
chapterUrl,
};
novelChapters.push(novel);
});
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 = $("h2.western").text();
let chapterText = $(".chapter-content").html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async (searchTerm) => {
let url = baseUrl + "roman";
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$("article").each(function () {
const novelName = $(this).find("h2").text().trim();
const novelCover = $(this).find("img").attr("src");
const novelUrl = $(this).find("h2 > a").attr("href").split("/")[4];
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
novels = novels.filter((novel) =>
novel.novelName.toLowerCase().includes(searchTerm.toLowerCase())
);
return novels;
};
const NovelDeGlaceScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default NovelDeGlaceScraper;
+11
View File
@@ -1,13 +1,24 @@
export const cleanHtml = (html) => {
let text = html;
if (!text) {
return "Chapter is empty.\n\nReport if it's available in webview.";
}
text = text
/**
* Remove inline styles
*/
.replace(
/style="((?:font-family|line-height|font-size|text-align).*?)"/gi,
""
)
.replace(/class=".*?"/gi, "")
.replace(/id=".*?"/gi, "")
/**
* Remove comments
*/
.replace(/<!--.*?-->/gim, "")
.replace(/<\s*script[^>]*>[\s\S]*?<\/script>/gim, "")
.replace(/<\s*noscript[^>]*>[\s\S]*?<\/noscript>/gim, "")
+3
View File
@@ -1,3 +1,6 @@
export const defaultCoverUri =
"https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true";
export const Status = {
ONGOING: "Ongoing",
COMPLETED: "Completed",
+8 -1
View File
@@ -1,6 +1,10 @@
const htmlToText = (html, options = {}) => {
const { removeLineBreaks = true } = options;
if (!html) {
return "Chapter is empty.\n\nReport if it's available in webview.";
}
html = html.trim();
text = removeLineBreaks && html.replace(/(?:\n|\r\n|\r)/gi, "");
@@ -63,7 +67,10 @@ const htmlToText = (html, options = {}) => {
* <img> -> [img](src)
*/
// .replace(/<\s*img[^>]*src=['"](.*?)['"][^>]*>/gi, "[img]($1)")
.replace(
/<\s*img[^>]*[src|data-src]=['"](.*?)['"][^>]*>/gi,
"[Image]($1)\n"
)
/**
* Remove remaining tags
+16 -7
View File
@@ -21,6 +21,8 @@ const getPath = (extensionId) => {
61: { novels: "novel", novel: "novel", chapter: "novel" },
62: { novels: "novel", novel: "novel", chapter: "novel" },
63: { novels: "novel", novel: "novel", chapter: "novel" },
75: { novels: "series", novel: "series", chapter: "series" },
80: { novels: "manga", novel: "manga", chapter: "manga" },
};
return (
@@ -130,13 +132,6 @@ export const LightNovelsHubScraper = new MadaraScraper(
getPath(58)
);
// export const ArNovelScraper = new MadaraScraper(
// 59,
// "https://arnovel.me/",
// "ArNovel",
// getPath(59)
// );
export const MeionNovelScraper = new MadaraScraper(
60,
"https://meionovel.id/",
@@ -178,3 +173,17 @@ export const WBNovelScraper = new MadaraScraper(
"WBNovel",
getPath(66)
);
export const WuxiaWorldDotSiteScraper = new MadaraScraper(
74,
"https://wuxiaworld.site/",
"WuxiaWorld.Site",
getPath(74)
);
export const MysticalSeriesScraper = new MadaraScraper(
75,
"https://mysticalmerries.com/",
"MysticalSeries",
getPath(75)
);
@@ -9,7 +9,8 @@ class WPMangaStreamScraper {
async popularNovels(page) {
let totalPages = 100;
let url = this.baseUrl + "series/?page=" + page + "?m_orderby=popular";
let url =
this.baseUrl + "series/?page=" + page + "&status=&order=popular";
let sourceId = this.sourceId;
const result = await fetch(url);
+16
View File
@@ -80,6 +80,7 @@ const parseNovelAndChapters = async (novelUrl) => {
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const chapterId = chapterUrl.split("/")[4];
const result = await fetch(url);
const body = await result.text();
@@ -93,6 +94,21 @@ const parseChapter = async (novelUrl, chapterUrl) => {
"body > div.pusher.container_main > div:nth-child(5) > div:nth-child(1)"
).html();
if (chapterText) {
/**
* Convert relative url to absolute
*/
chapterText = chapterText
.replace(/<\s*script[^>]*>[\s\S]*?<\/script>/gim, "")
.replace(
/data-src="(.*)"/g,
`src="https://ranobehub.org/img/ranobe/content/${chapterId}/$1.jpg"`
);
}
console.log(chapterText);
const chapter = {
sourceId,
novelUrl,
+16
View File
@@ -45,6 +45,7 @@ import {
LunarLettersScraper,
MeionNovelScraper,
MostNovelScraper,
MysticalSeriesScraper,
NovelCakeScraper,
NovelMultiverseScraper,
NovelsRockScraper,
@@ -54,6 +55,7 @@ import {
SleepyTranslationsScraper,
WBNovelScraper,
WebNovelLoverScraper,
WuxiaWorldDotSiteScraper,
ZinnNovelScraper,
} from "./multisrc/madara/MadaraGenerator";
import WuxiaBlogScraper from "./en/wuxiablog";
@@ -73,6 +75,12 @@ import ArNovelScraper from "./ar/arnovel";
import DivineDaoLibraryScraper from "./en/divinedaolibrary";
import NovelOnlineFullScraper from "./en/novelonlinefull";
import LightNovelUpdatesScraper from "./en/lightnovelupdates";
import EpikNovelScraper from "./tr/epiknovel";
import TravisTranslationsScraper from "./en/travistranslations";
import NovelDeGlaceScraper from "./fr/noveldeglace";
import RainOfSnowScraper from "./en/rainofsnow";
import ReaperScansBrScraper from "./br/ReaperScansBr";
import ArthurScansScraper from "./br/ArthurScans";
export const getSource = (sourceId) => {
const scrapers = {
@@ -147,6 +155,14 @@ export const getSource = (sourceId) => {
70: DivineDaoLibraryScraper,
71: NovelOnlineFullScraper,
72: LightNovelUpdatesScraper,
73: EpikNovelScraper,
74: WuxiaWorldDotSiteScraper,
75: MysticalSeriesScraper,
76: TravisTranslationsScraper,
77: NovelDeGlaceScraper,
78: RainOfSnowScraper,
79: ReaperScansBrScraper,
80: ArthurScansScraper,
};
return scrapers[sourceId];
+57 -1
View File
@@ -156,7 +156,7 @@
{
"sourceId": 23,
"url": "https://tunovelaligera.com/",
"sourceName": "Tunovelaligera",
"sourceName": "TuNovelaLigera",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/es/tunovelaligera/icon.png?raw=true",
"lang": "Spanish"
},
@@ -495,5 +495,61 @@
"sourceName": "LightNovelUpdates",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/lightnovelupdates/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 73,
"url": "https://www.epiknovel.com/",
"sourceName": "EpikNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/tr/epiknovel/icon.png?raw=true",
"lang": "Turkish"
},
{
"sourceId": 74,
"url": "https://wuxiaworld.site/",
"sourceName": "WuxiaWorld.Site",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/wuxiaworldsite.png?raw=true",
"lang": "English"
},
{
"sourceId": 75,
"url": "https://mysticalmerries.com/",
"sourceName": "MysticalSeries",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/mysticalseries.png?raw=true",
"lang": "English"
},
{
"sourceId": 76,
"url": "https://travistranslations.com/",
"sourceName": "TravisTranslations",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/travistranslations/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 77,
"url": "https://noveldeglace.com/",
"sourceName": "NovelDeGlace",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/fr/noveldeglace/icon.png?raw=true",
"lang": "French"
},
{
"sourceId": 78,
"url": "https://rainofsnow.com/",
"sourceName": "RainOfSnow",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/rainofsnow/icon.png?raw=true",
"lang": "French"
},
{
"sourceId": 79,
"url": "https://reaperscans.com.br/",
"sourceName": "ReaperScans (Br)",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/reaperscans/icon.png?raw=true",
"lang": "Portuguese (Brazil)"
},
{
"sourceId": 80,
"url": "https://arthurscan.xyz/",
"sourceName": "ArthurScans (Br)",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/arthurscans.png?raw=true",
"lang": "Portuguese (Brazil)"
}
]
+170
View File
@@ -0,0 +1,170 @@
import cheerio from "react-native-cheerio";
const sourceId = 73;
const sourceName = "EpikNovel";
const baseUrl = "https://www.epiknovel.com/";
const popularNovels = async (page) => {
let url = baseUrl + "seri-listesi?Sayfa=" + page;
const totalPages = 5;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$("div.col-lg-12.col-md-12").each(function () {
const novelName = $(this).find("h3").text();
const novelCover = $(this).find("img").attr("data-src");
const novelUrl = $(this).find("h3 > a").attr("href");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
console.log(novels);
return { novels, totalPages };
};
const parseNovelAndChapters = async (novelUrl) => {
const url = novelUrl;
console.log(url);
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novel = { sourceId, sourceName, url, novelUrl };
novel.novelName = $("h1#tables").text().trim();
novel.novelCover = $("img.manga-cover").attr("src");
novel.summary = $(
"#wrapper > div.row > div.col-md-9 > div:nth-child(6) > p:nth-child(3)"
)
.text()
.trim();
novel.status = $(
"#wrapper > div.row > div.col-md-9 > div.row > div.col-md-9 > h4:nth-child(3) > a"
)
.text()
.trim();
novel.author = $("#NovelInfo > p:nth-child(4)")
.text()
.replace(/Publisher:|\s/g, "")
.trim();
let novelChapters = [];
$("table").find("tr").first().remove();
$("table")
.find("tr")
.each(function () {
const releaseDate = $(this).find("td:nth-child(3)").text();
let chapterName = $(this).find("td:nth-child(1) > a").text();
if ($(this).find("td:nth-child(1) > span").length > 0) {
chapterName = "🔒 " + chapterName;
}
const chapterUrl = $(this)
.find(" td:nth-child(1) > a")
.attr("href");
novelChapters.push({
chapterName,
chapterUrl,
releaseDate,
});
});
novel.chapters = novelChapters;
console.log(novel);
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
console.log(url);
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let chapterName, chapterText;
if (result.url == "https://www.epiknovel.com/login") {
chapterName = "";
chapterText = "Premium Chapter";
} else {
chapterName = $("#icerik > center > h4 > b").text();
chapterText = $("div#icerik").html();
}
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async (searchTerm) => {
const url = baseUrl + "seri-listesi?q=" + searchTerm;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$("div.col-lg-12.col-md-12").each(function () {
const novelName = $(this).find("h3").text();
const novelCover = $(this).find("img").attr("data-src");
const novelUrl = $(this).find("h3 > a").attr("href");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
};
const EpikNovelScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default EpikNovelScraper;
+11 -1
View File
@@ -8,8 +8,11 @@ const AccentColors = {
hotPink: "#FF3399",
tako: "#F3B375",
yinYang: "#FFFFFF",
yang: "#000000",
springBlossom: "#c43c97",
oceanic: "#009688",
yotsuba: "#FC8C5C",
tako_light: "#66577E",
};
const RippleColors = {
@@ -20,9 +23,12 @@ const RippleColors = {
irisBlue: "rgba(0,173,181,0.12)",
hotPink: "rgba(255,51,153,0.12)",
tako: "rgba(243,179,117,0.12)",
yinYang: "#rgba(255,255,255,0.12)",
yinYang: "rgba(255,255,255,0.12)",
yang: "rgba(0,0,0,0.12)",
springBlossom: "rgba(196,60,151,0.12)",
oceanic: "rgba(0, 150, 136,0.12)",
yotsuba: "rgba(252, 140, 92,0.12)",
tako_light: "rgba(102, 87, 126,0.12)",
};
const StatusBarStyle = {
@@ -76,6 +82,10 @@ const PrimaryColors = {
colorPrimary: "#32424A",
colorPrimaryDark: "#263238",
},
tako_light: {
colorPrimary: "#F7F5FF",
colorPrimaryDark: "#F2EDF7",
},
};
const DividerColors = {
+50 -3
View File
@@ -135,7 +135,7 @@ export const takoTheme = {
filterColor: "#FFEB3B",
dividerColor: DividerColors.DARK,
colorDisabled: "#484861",
colorButtonText: "#000000",
colorButtonText: PrimaryColors.tako.colorPrimaryDark,
statusBar: StatusBarStyle.LIGHT,
...TextColors.dark,
};
@@ -146,7 +146,7 @@ export const takoTheme = {
export const yinYangTheme = {
id: 9,
name: "Yin & Yang",
name: "Yin",
...PrimaryColors.dark,
colorAccent: AccentColors.yinYang,
rippleColor: RippleColors.yinYang,
@@ -160,6 +160,21 @@ export const yinYangTheme = {
...TextColors.dark,
};
export const yangTheme = {
id: 11,
name: "Yang",
...PrimaryColors.light,
colorAccent: AccentColors.yang,
rippleColor: RippleColors.yang,
searchBarColor: "#FFFFFF",
menuColor: "#484861",
filterColor: "#FFEB3B",
dividerColor: DividerColors.LIGHT,
colorDisabled: PrimaryColors.light.colorPrimary,
colorButtonText: "#FFFFFF",
statusBar: StatusBarStyle.DARK,
...TextColors.light,
};
/**
* Yin & Yang
*/
@@ -199,11 +214,43 @@ export const springBlossomTheme = {
statusBar: StatusBarStyle.DARK,
};
export const yotsubaTheme = {
id: 12,
name: "Yotsuba",
...PrimaryColors.light,
...TextColors.light,
colorAccent: AccentColors.yotsuba,
rippleColor: RippleColors.yotsuba,
searchBarColor: PrimaryColors.light.colorPrimary,
menuColor: "#FFFFFF",
dividerColor: DividerColors.LIGHT,
filterColor: "#FFC107",
colorDisabled: PrimaryColors.light.colorPrimary,
colorButtonText: "#FFFFFF",
statusBar: StatusBarStyle.DARK,
};
export const takoLightTheme = {
id: 13,
name: "Tako",
...PrimaryColors.tako_light,
...TextColors.light,
colorAccent: AccentColors.tako_light,
rippleColor: RippleColors.tako_light,
searchBarColor: PrimaryColors.tako_light.colorPrimary,
menuColor: "#FFFFFF",
dividerColor: DividerColors.LIGHT,
filterColor: "#FFC107",
colorDisabled: PrimaryColors.tako_light.colorPrimary,
colorButtonText: "#F3B375",
statusBar: StatusBarStyle.DARK,
};
export const colorsAmoled = {
...PrimaryColors.amoled,
...TextColors.dark,
statusBar: StatusBarStyle.LIGHT,
searchBarColor: "#1F1F1F",
searchBarColor: "#000000",
menuColor: "#242529",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.15)",
+1 -6
View File
@@ -1,5 +1,4 @@
import { StatusBar } from "react-native";
import changeNavigationBarColor from "react-native-navigation-bar-color";
/**
* Sets status and navigation bar color.
@@ -7,8 +6,4 @@ import changeNavigationBarColor from "react-native-navigation-bar-color";
* @param theme
*/
export const setBarColor = (theme) => {
changeNavigationBarColor(theme.colorPrimary);
StatusBar.setBarStyle(theme.statusBar);
};
export const setBarColor = (theme) => StatusBar.setBarStyle(theme.statusBar);