Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8084cdbffb | |||
| dc9529c322 | |||
| 91168a335b | |||
| a6b3eb9350 | |||
| 0ce4e6b985 | |||
| 6684848a42 | |||
| 87d12cabb3 | |||
| e01fc21a49 | |||
| f0cfd9066b | |||
| c2cc6ea96b | |||
| eeeea6b4e0 | |||
| ddce153b53 | |||
| 38e45ca097 | |||
| a6a60607a7 | |||
| d3e772f0dd | |||
| 095d46681f | |||
| 3e4e967197 | |||
| c5b4e7b902 | |||
| e9421a5fd0 | |||
| afef0d63ce | |||
| e68a5c5f82 | |||
| ff1635b817 | |||
| f1ca221b91 | |||
| 5a1106fc68 | |||
| 3b49a1e516 | |||
| 85acbc8c4a | |||
| 66e0be5e84 | |||
| 2234006272 | |||
| eca00ddcfa | |||
| 2328e55105 | |||
| b8a25b9172 | |||
| 9af8d55985 | |||
| 4777f453a4 | |||
| 4a0195762d | |||
| e8785c2905 | |||
| 1302cbde16 | |||
| 43d0adced8 | |||
| 0c8b30cd72 | |||
| cc86fc7da4 |
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
||||
+1
@@ -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(),
|
||||
|
||||
Generated
+19798
-19776
File diff suppressed because it is too large
Load Diff
+72
-70
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
@@ -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({});
|
||||
@@ -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 = ?",
|
||||
|
||||
@@ -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;"));
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -30,7 +30,10 @@ const BrowseSettings = ({ navigation }) => {
|
||||
"Arabic",
|
||||
"Indonesian",
|
||||
"Russian",
|
||||
];
|
||||
"Turkish",
|
||||
"French",
|
||||
"Portuguese (Brazil)",
|
||||
].sort();
|
||||
|
||||
const { searchAllSources = false } = useSettings();
|
||||
|
||||
|
||||
+280
-23
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const HistoryItem = ({
|
||||
} = history;
|
||||
|
||||
const getChapterNumber = () =>
|
||||
`Ch. ${parseChapterNumber(chapterName)} - ${moment(historyTimeRead)
|
||||
`Chapter ${parseChapterNumber(chapterName)} • ${moment(historyTimeRead)
|
||||
.format("h:mm a")
|
||||
.toUpperCase()}`;
|
||||
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -55,7 +55,6 @@ const DownloadQueue = ({ navigation }) => {
|
||||
if (downloadQueue.length > 0) {
|
||||
dispatch(cancelDownload());
|
||||
showToast("Downloads cancelled.");
|
||||
BackgroundService.stop();
|
||||
}
|
||||
closeMenu();
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
+104
-37
@@ -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",
|
||||
},
|
||||
+75
-49
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -182,6 +182,7 @@ const Updates = ({ navigation }) => {
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 4,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
|
||||
+34
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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, "")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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);
|
||||
|
||||
Reference in New Issue
Block a user