Compare commits
51 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 | |||
| 625acdb585 | |||
| 93617e60bd | |||
| d75c607f67 | |||
| dd087ff485 | |||
| 5da3e05bba | |||
| e628c3cfd6 | |||
| deba021384 | |||
| c6d2ba8442 | |||
| 6a3736dced | |||
| f63ae573ba | |||
| 8f3020a544 | |||
| 04e32a4234 |
@@ -136,7 +136,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.1.2"
|
||||
versionName "1.1.4"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
||||
@@ -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
-19793
File diff suppressed because it is too large
Load Diff
+72
-71
@@ -1,73 +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-fit-image": "^1.5.5",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-navigation-bar-color": "^2.0.1",
|
||||
"react-native-pager-view": "^5.1.10",
|
||||
"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,
|
||||
|
||||
@@ -15,7 +15,13 @@ export const Checkbox = ({ label, status, onPress, theme }) => (
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>{label}</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: status ? theme.colorAccent : theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{status && (
|
||||
<MaterialCommunityIcons
|
||||
name="check"
|
||||
@@ -40,7 +46,13 @@ export const SortItem = ({ label, status, onPress, theme }) => (
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>{label}</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: status ? theme.colorAccent : theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{status && (
|
||||
<MaterialCommunityIcons
|
||||
name={status === "asc" ? "arrow-up" : "arrow-down"}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
@@ -39,7 +39,7 @@ const NovelCover = ({
|
||||
[]
|
||||
);
|
||||
|
||||
const selectNovel = () => onLongPress(item.novelId);
|
||||
const selectNovel = () => onLongPress && onLongPress(item.novelId);
|
||||
|
||||
return displayMode !== 2 ? (
|
||||
<View
|
||||
@@ -174,7 +174,15 @@ const CompactTitle = ({ novelName }) => (
|
||||
>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
|
||||
style={[
|
||||
styles.title,
|
||||
{
|
||||
color: "rgba(255,255,255,1)",
|
||||
textShadowColor: "rgba(0, 0, 0, 0.75)",
|
||||
textShadowOffset: { width: -1, height: 1 },
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{novelName}
|
||||
</Text>
|
||||
@@ -253,9 +261,6 @@ const styles = StyleSheet.create({
|
||||
fontFamily: "pt-sans-bold",
|
||||
fontSize: 14,
|
||||
padding: 8,
|
||||
textShadowColor: "rgba(0, 0, 0, 0.75)",
|
||||
textShadowOffset: { width: -1, height: 1 },
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
linearGradient: {
|
||||
borderRadius: 4,
|
||||
|
||||
@@ -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({});
|
||||
@@ -113,6 +113,19 @@ export const markChapterRead = async (chapterId) => {
|
||||
});
|
||||
};
|
||||
|
||||
const markChapterUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId = ?`;
|
||||
|
||||
export const markChapterUnread = async (chapterId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
markChapterUnreadQuery,
|
||||
[chapterId],
|
||||
(tx, res) => {},
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markAllChaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE novelId = ?`;
|
||||
|
||||
export const markAllChaptersRead = async (novelId) => {
|
||||
|
||||
@@ -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;"));
|
||||
|
||||
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
|
||||
};
|
||||
|
||||
const isNewVersion = (versionTag) => {
|
||||
let currentVersion = "v1.1.2";
|
||||
let currentVersion = "v1.1.4";
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
let newVersion = versionTag.replace(regex, "");
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -266,9 +266,9 @@ export const markPreviousChaptersUnreadAction =
|
||||
};
|
||||
|
||||
export const markChapterUnreadAction =
|
||||
(chapters, novelId) => async (dispatch) => {
|
||||
(chapters, novelId) => async (dispatch, getState) => {
|
||||
const { sort = "ORDER BY chapterId ASC", filter = "" } =
|
||||
store.getState().preferenceReducer.novelSettings[novelId];
|
||||
getState().preferenceReducer.novelSettings[novelId];
|
||||
|
||||
chapters.map((chapter) => markChapterUnread(chapter.chapterId));
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GET_UPDATES } from "./updates.types";
|
||||
import { GET_UPDATES, SET_LAST_UPDATE_TIME } from "./updates.types";
|
||||
|
||||
import { getUpdates } from "../../database/queries/UpdateQueries";
|
||||
import { updateAllNovels } from "../../services/updates";
|
||||
@@ -36,9 +36,12 @@ export const getUpdatesAction = () => async (dispatch) => {
|
||||
export const updateLibraryAction = () => async (dispatch, getState) => {
|
||||
showToast("Updating library");
|
||||
|
||||
const { downloadNewChapters = false } = getState().settingsReducer;
|
||||
dispatch({ type: SET_LAST_UPDATE_TIME, payload: Date.now() });
|
||||
|
||||
await updateAllNovels({ downloadNewChapters });
|
||||
const { downloadNewChapters = false, onlyUpdateOngoingNovels = false } =
|
||||
getState().settingsReducer;
|
||||
|
||||
await updateAllNovels({ downloadNewChapters, onlyUpdateOngoingNovels });
|
||||
|
||||
dispatch(getUpdatesAction());
|
||||
};
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { LOAD_UPDATES, GET_UPDATES } from "./updates.types";
|
||||
import {
|
||||
LOAD_UPDATES,
|
||||
GET_UPDATES,
|
||||
SET_LAST_UPDATE_TIME,
|
||||
SHOW_LAST_UPDATE_TIME,
|
||||
} from "./updates.types";
|
||||
|
||||
const initialState = {
|
||||
updates: [],
|
||||
lastUpdateTime: null,
|
||||
showLastUpdateTime: true,
|
||||
loading: true,
|
||||
};
|
||||
|
||||
@@ -13,6 +20,16 @@ const updateReducer = (state = initialState, action) => {
|
||||
return { ...state, loading: true };
|
||||
case GET_UPDATES:
|
||||
return { ...state, updates: payload, loading: false };
|
||||
case SET_LAST_UPDATE_TIME:
|
||||
return {
|
||||
...state,
|
||||
lastUpdateTime: payload,
|
||||
};
|
||||
case SHOW_LAST_UPDATE_TIME:
|
||||
return {
|
||||
...state,
|
||||
showLastUpdateTime: payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export const GET_UPDATES = "GET_UPDATES";
|
||||
export const LOAD_UPDATES = "LOAD_UPDATES";
|
||||
export const SET_LAST_UPDATE_TIME = "SET_LAST_UPDATE_TIME";
|
||||
export const SHOW_LAST_UPDATE_TIME = "SHOW_LAST_UPDATE_TIME";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -60,69 +63,83 @@ const BrowseSettings = ({ navigation }) => {
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar onBackAction={navigation.goBack} title="Sources" />
|
||||
<View>
|
||||
<List.SubHeader theme={theme}>Global Search</List.SubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"searchAllSources",
|
||||
!searchAllSources
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
Search all sources
|
||||
</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={searchAllSources}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"searchAllSources",
|
||||
!searchAllSources
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Discover</List.SubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() => dispatch(enableDiscover("showMyAnimeList"))}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
MyAnimeList
|
||||
</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={showMyAnimeList}
|
||||
onValueChange={() =>
|
||||
dispatch(enableDiscover("showMyAnimeList"))
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Languages</List.SubHeader>
|
||||
<FlatList
|
||||
data={languages}
|
||||
keyExtractor={(item) => item}
|
||||
renderItem={renderItem}
|
||||
ListHeaderComponent={
|
||||
<View>
|
||||
<List.SubHeader theme={theme}>
|
||||
Global Search
|
||||
</List.SubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"searchAllSources",
|
||||
!searchAllSources
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
Search all sources
|
||||
</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={searchAllSources}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"searchAllSources",
|
||||
!searchAllSources
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>
|
||||
Discover
|
||||
</List.SubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(enableDiscover("showMyAnimeList"))
|
||||
}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
MyAnimeList
|
||||
</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={showMyAnimeList}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
enableDiscover("showMyAnimeList")
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>
|
||||
Languages
|
||||
</List.SubHeader>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
|
||||
+283
-24
@@ -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};
|
||||
|
||||
@@ -419,6 +583,9 @@ const Chapter = ({ route, navigation }) => {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
${reader.customCSS}
|
||||
@font-face {
|
||||
font-family: ${reader.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${
|
||||
@@ -426,7 +593,6 @@ const Chapter = ({ route, navigation }) => {
|
||||
}.ttf");
|
||||
}
|
||||
|
||||
${reader.customCSS}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -442,7 +608,6 @@ const Chapter = ({ route, navigation }) => {
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingVertical: 16,
|
||||
paddingBottom: 32,
|
||||
paddingHorizontal: `${reader.padding}%`,
|
||||
paddingTop: StatusBar.currentHeight,
|
||||
}}
|
||||
@@ -453,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}
|
||||
@@ -467,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 && (
|
||||
@@ -491,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>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
@@ -519,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, { 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,426 +69,525 @@ const ReaderSheet = ({
|
||||
{ value: "right", icon: "format-align-right" },
|
||||
];
|
||||
|
||||
const FirstRoute = () => (
|
||||
<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
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Color
|
||||
</Text>
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<ScrollView horizontal={true}>
|
||||
{presetThemes.map((item, index) => (
|
||||
<ToggleColorButton
|
||||
key={index}
|
||||
selected={reader.theme === item.value}
|
||||
backgroundColor={item.backgroundColor}
|
||||
textColor={item.textColor}
|
||||
theme={theme}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"theme",
|
||||
item.value
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textColor",
|
||||
item.textColor
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</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,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
marginRight: 16,
|
||||
}}
|
||||
>
|
||||
Font style
|
||||
</Text>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
contentContainerStyle={{
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{fonts.map((font) => (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
reader.fontFamily === font.fontFamily && {
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"fontFamily",
|
||||
font.fontFamily
|
||||
)
|
||||
)
|
||||
}
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontFamily: font.fontFamily,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{font.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</Row>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const SecondRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Pressable
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"useWebViewForChapter",
|
||||
!useWebViewForChapter
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
Use WebView
|
||||
</Text>
|
||||
<Switch
|
||||
value={useWebViewForChapter}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"useWebViewForChapter",
|
||||
!useWebViewForChapter
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showScrollPercentage",
|
||||
!showScrollPercentage
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
Show progress percentage
|
||||
</Text>
|
||||
<Switch
|
||||
value={showScrollPercentage}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showScrollPercentage",
|
||||
!showScrollPercentage
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
{!useWebViewForChapter && (
|
||||
<Pressable
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings("textSelectable", !selectText))
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
Select text
|
||||
</Text>
|
||||
<Switch
|
||||
value={selectText}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings("textSelectable", !selectText)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderScene = SceneMap({
|
||||
first: FirstRoute,
|
||||
second: SecondRoute,
|
||||
});
|
||||
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
const [routes] = useState([
|
||||
{ key: "first", title: "Display" },
|
||||
{ key: "second", title: "General" },
|
||||
]);
|
||||
|
||||
const renderTabBar = (props) => (
|
||||
<TabBar
|
||||
{...props}
|
||||
indicatorStyle={{ backgroundColor: theme.colorAccent }}
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
renderLabel={({ route, focused, color }) => (
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.textColorSecondary}
|
||||
activeColor={theme.colorAccent}
|
||||
pressColor={theme.rippleColor}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 400, bottom: 0 }}
|
||||
snappingPoints={[0, 400]}
|
||||
draggableRange={{ top: 390, bottom: 0 }}
|
||||
snappingPoints={[0, 390]}
|
||||
showBackdrop={true}
|
||||
backdropOpacity={0}
|
||||
height={390}
|
||||
>
|
||||
<View
|
||||
<ScrollView
|
||||
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>
|
||||
<BottomSheetHandle theme={theme} />
|
||||
<View style={{ flex: 1, paddingVertical: 16, paddingTop: 24 }}>
|
||||
<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
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Color
|
||||
</Text>
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<ScrollView horizontal={true}>
|
||||
{presetThemes.map((item, index) => (
|
||||
<ToggleColorButton
|
||||
key={index}
|
||||
selected={
|
||||
reader.theme === item.value
|
||||
}
|
||||
backgroundColor={
|
||||
item.backgroundColor
|
||||
}
|
||||
textColor={item.textColor}
|
||||
theme={theme}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"theme",
|
||||
item.value
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textColor",
|
||||
item.textColor
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</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: 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,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"fontFamily",
|
||||
font.fontFamily
|
||||
)
|
||||
)
|
||||
}
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontFamily: font.fontFamily,
|
||||
color:
|
||||
reader.fontFamily ===
|
||||
font.fontFamily
|
||||
? theme.colorAccent
|
||||
: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{font.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</Row>
|
||||
</View>
|
||||
<Pressable
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"useWebViewForChapter",
|
||||
!useWebViewForChapter
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
Use WebView
|
||||
</Text>
|
||||
<Switch
|
||||
value={useWebViewForChapter}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"useWebViewForChapter",
|
||||
!useWebViewForChapter
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"fullScreenMode",
|
||||
!fullScreenMode
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
Fullscreen
|
||||
</Text>
|
||||
<Switch
|
||||
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",
|
||||
!selectText
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<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={[
|
||||
|
||||
@@ -21,14 +21,14 @@ const AboutScreen = ({ navigation }) => {
|
||||
<List.Section>
|
||||
<List.Item
|
||||
title="Version"
|
||||
description="Stable 1.1.2 (17/07/21 08:00 AM)"
|
||||
description="Stable 1.1.4 (15/08/21 9:00 AM)"
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader/releases/tag/v1.1.2"
|
||||
"https://github.com/LNReader/lnreader/releases/tag/v1.1.4"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
|
||||
@@ -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,18 @@
|
||||
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 }) => {
|
||||
const theme = useTheme();
|
||||
@@ -30,11 +25,13 @@ const GenralSettings = ({ navigation }) => {
|
||||
const {
|
||||
updateLibraryOnLaunch = false,
|
||||
downloadNewChapters = false,
|
||||
showHistoryTab = true,
|
||||
showUpdatesTab = true,
|
||||
showLabelsInNav = false,
|
||||
onlyUpdateOngoingNovels = false,
|
||||
} = useSettings();
|
||||
|
||||
const { showLastUpdateTime = true } = useSelector(
|
||||
(state) => state.updatesReducer
|
||||
);
|
||||
|
||||
const displayModeLabel = (displayMode) => {
|
||||
const label = {
|
||||
0: "Compact Grid",
|
||||
@@ -53,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
|
||||
*/
|
||||
@@ -67,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
|
||||
@@ -166,6 +92,32 @@ 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}
|
||||
onPress={() =>
|
||||
dispatch({
|
||||
type: SHOW_LAST_UPDATE_TIME,
|
||||
payload: !showLastUpdateTime,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Auto-download</List.SubHeader>
|
||||
<SwitchSetting
|
||||
label="Download new chapters"
|
||||
@@ -180,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
|
||||
@@ -231,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
|
||||
|
||||
+175
-103
@@ -1,15 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import {
|
||||
Dimensions,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Button } from "react-native-paper";
|
||||
import WebView from "react-native-webview";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import ColorPickerModal from "../../components/ColorPickerModal";
|
||||
@@ -20,19 +23,42 @@ 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,
|
||||
readerTextColor,
|
||||
} from "../Chapter/readerStyleController";
|
||||
|
||||
const ReaderSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const FirstRoute = React.memo(({ theme, dispatch, reader }) => {
|
||||
const presetThemes = [
|
||||
{
|
||||
value: 1,
|
||||
backgroundColor: "#000000",
|
||||
textColor: "rgba(255,255,255,0.7)",
|
||||
},
|
||||
{ value: 2, backgroundColor: "#FFFFFF", textColor: "#111111" },
|
||||
{ value: 3, backgroundColor: "#F7DFC6", textColor: "#593100" },
|
||||
{ value: 4, backgroundColor: "#292832", textColor: "#CCCCCC" },
|
||||
{ value: 5, backgroundColor: "#2B2C30", textColor: "#CCCCCC" },
|
||||
];
|
||||
|
||||
const reader = useReaderSettings();
|
||||
const textAlignments = [
|
||||
{ value: "left", icon: "format-align-left" },
|
||||
{ value: "center", icon: "format-align-center" },
|
||||
{ value: "justify", icon: "format-align-justify" },
|
||||
{ value: "right", icon: "format-align-right" },
|
||||
];
|
||||
|
||||
const { autoScrollInterval = 10 } = useSettings();
|
||||
|
||||
/**
|
||||
* Reader Background Color Modal
|
||||
@@ -59,100 +85,10 @@ const ReaderSettings = ({ navigation }) => {
|
||||
dispatch(setReaderSettings("textColor", val ?? reader.textColor));
|
||||
};
|
||||
|
||||
const readerStyles = [
|
||||
{
|
||||
paddingVertical: 16,
|
||||
paddingBottom: 32,
|
||||
fontSize: reader.textSize,
|
||||
color: reader.textColor,
|
||||
lineHeight: readerLineHeight(reader.textSize, reader.lineHeight),
|
||||
textAlign: reader.textAlign,
|
||||
paddingHorizontal: `${reader.padding}%`,
|
||||
},
|
||||
reader.fontFamily && {
|
||||
fontFamily: reader.fontFamily,
|
||||
},
|
||||
];
|
||||
|
||||
const presetThemes = [
|
||||
{
|
||||
value: 1,
|
||||
backgroundColor: "#000000",
|
||||
textColor: "rgba(255,255,255,0.7)",
|
||||
},
|
||||
{ value: 2, backgroundColor: "#FFFFFF", textColor: "#111111" },
|
||||
{ value: 3, backgroundColor: "#F7DFC6", textColor: "#593100" },
|
||||
{ value: 4, backgroundColor: "#292832", textColor: "#CCCCCC" },
|
||||
{ value: 5, backgroundColor: "#2B2C30", textColor: "#CCCCCC" },
|
||||
];
|
||||
|
||||
const textAlignments = [
|
||||
{ value: "left", icon: "format-align-left" },
|
||||
{ value: "center", icon: "format-align-center" },
|
||||
{ value: "justify", icon: "format-align-justify" },
|
||||
{ value: "right", icon: "format-align-right" },
|
||||
];
|
||||
|
||||
const [customHtmlCSS, setcustomHtmlCSS] = useState(reader.customCSS);
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar title="Reader" onBackAction={navigation.goBack} />
|
||||
<View style={{ height: 360 }}>
|
||||
<WebView
|
||||
originWhitelist={["*"]}
|
||||
style={{
|
||||
backgroundColor: readerBackground(reader.theme),
|
||||
}}
|
||||
source={{
|
||||
html: `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: ${reader.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${reader.fontFamily}.ttf");
|
||||
}
|
||||
body {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: ${reader.padding}%;
|
||||
padding-right: ${reader.padding}%;
|
||||
font-family: ${reader.fontFamily};
|
||||
text-align: ${reader.textAlign};
|
||||
line-height: ${reader.lineHeight};
|
||||
font-size: ${reader.textSize}px;
|
||||
color: ${reader.textColor};
|
||||
}
|
||||
${reader.customCSS}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Ut lobortis nunc id nisl tempor, sed tristique sem
|
||||
sagittis. Mauris consequat orci egestas volutpat
|
||||
vestibulum. Nullam convallis, leo in porttitor
|
||||
fringilla, ex mi finibus felis, non molestie ligula
|
||||
ligula facilisis ligula. Nam eu est a turpis porttitor
|
||||
aliquet sed vel risus. Ut ac scelerisque diam. Nulla
|
||||
fringilla enim non ex rhoncus faucibus. Integer eget
|
||||
lacus justo.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Ut lobortis nunc id nisl tempor, sed tristique sem
|
||||
sagittis. Mauris consequat orci egestas volutpat
|
||||
vestibulum. Nullam convallis, leo in porttitor
|
||||
fringilla, ex mi finibus felis, non molestie ligula
|
||||
ligula facilisis ligula.
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView>
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Preset</List.SubHeader>
|
||||
@@ -168,6 +104,7 @@ const ReaderSettings = ({ navigation }) => {
|
||||
<ScrollView
|
||||
style={{ marginLeft: 16 }}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
<Row>
|
||||
{presetThemes.map((theme) => (
|
||||
@@ -219,10 +156,7 @@ const ReaderSettings = ({ navigation }) => {
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Text</List.SubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressableListItem}
|
||||
>
|
||||
<Pressable style={styles.pressableListItem}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
@@ -305,9 +239,30 @@ const ReaderSettings = ({ navigation }) => {
|
||||
</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
|
||||
title="Reader background color"
|
||||
modalVisible={readerBackgroundColorModal}
|
||||
@@ -324,6 +279,123 @@ const ReaderSettings = ({ navigation }) => {
|
||||
theme={theme}
|
||||
onSubmit={setReaderTextColor}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const SecondRoute = React.memo(({ reader }) => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<WebView
|
||||
originWhitelist={["*"]}
|
||||
style={{
|
||||
backgroundColor: readerBackground(reader.theme),
|
||||
}}
|
||||
source={{
|
||||
html: `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
color: ${reader.textColor};
|
||||
text-align: ${reader.textAlign};
|
||||
line-height: ${reader.lineHeight};
|
||||
font-size: ${reader.textSize}px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: ${reader.padding}%;
|
||||
padding-right: ${reader.padding}%;
|
||||
font-family: ${reader.fontFamily};
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
|
||||
${reader.customCSS}
|
||||
|
||||
@font-face {
|
||||
font-family: ${reader.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${reader.fontFamily}.ttf");
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean lobortis, diam sed malesuada bibendum, nulla libero scelerisque sapien, nec interdum nisl ipsum ac ipsum. In lacinia eros ut quam commodo, in finibus augue ultricies. Vestibulum ex purus, condimentum eget sem at, molestie semper mi. Mauris ac feugiat quam. Pellentesque sagittis bibendum nibh eu lacinia. Aenean rhoncus, velit sit amet mollis egestas, diam turpis ornare velit, a dictum elit velit in erat. Quisque luctus in sem a vulputate.
|
||||
</p>
|
||||
<p> Pellentesque id tempus orci, non finibus tortor. Suspendisse in neque non eros eleifend hendrerit vitae a lacus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc interdum magna nulla, eget pharetra arcu dapibus eu. Pellentesque sed lectus sit amet sem porta lacinia. Curabitur consectetur dolor in nibh varius, ac placerat erat pellentesque. Proin in justo condimentum, fermentum lacus eget, ultrices ante. Fusce tempor blandit erat nec mollis. Vivamus in leo et nunc consectetur elementum nec ac lectus.
|
||||
</p>
|
||||
<p> In consectetur libero tempor metus interdum, sit amet viverra leo dapibus. Nullam tincidunt justo hendrerit lorem maximus, eu pretium erat fringilla. Nulla scelerisque leo at enim convallis, a elementum velit laoreet. Aliquam quis vestibulum quam. Morbi ut sollicitudin mi, et laoreet elit. Vivamus mattis, nulla at blandit sodales, lectus lacus luctus metus, non euismod neque urna vel nisl. Quisque vitae ante vitae orci tempus egestas quis eu nibh. Phasellus eget nulla non velit tincidunt fringilla eget nec libero. Morbi tempor erat quis rutrum condimentum.
|
||||
</p>
|
||||
<p> Cras sem magna, tempus sed urna eget, scelerisque tincidunt tortor. Cras euismod turpis libero, ut condimentum nulla ultrices id. Phasellus faucibus, elit ut imperdiet euismod, enim dui volutpat turpis, in tincidunt ex ligula non felis. Praesent erat erat, ultrices ut imperdiet sed, congue eu leo. Aliquam viverra fringilla ultrices. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Praesent ac malesuada massa. Cras eu augue in dui venenatis laoreet sit amet eget sem. Nullam malesuada sapien felis, et varius neque auctor eu.
|
||||
</p>
|
||||
<p> Sed vestibulum facilisis libero, in aliquet mauris lobortis et. In vitae lectus id risus cursus dictum at ac mi. Praesent ac lectus eget nunc elementum pellentesque. Ut at dui magna. Aliquam mattis posuere gravida. Morbi volutpat, dolor quis varius tincidunt, quam nulla vulputate libero, eu sagittis dui odio et magna. Sed luctus lectus ante, sit amet ultrices ligula convallis vitae. Sed efficitur libero nec quam porta, et fringilla turpis egestas. Nulla facilisi. Duis leo tellus, porta non enim eu, tincidunt cursus lorem.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
));
|
||||
|
||||
const ReaderSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const reader = useReaderSettings();
|
||||
|
||||
const renderScene = ({ route }) => {
|
||||
switch (route.key) {
|
||||
case "first":
|
||||
return (
|
||||
<FirstRoute
|
||||
dispatch={dispatch}
|
||||
theme={theme}
|
||||
reader={reader}
|
||||
/>
|
||||
);
|
||||
case "second":
|
||||
return <SecondRoute reader={reader} />;
|
||||
}
|
||||
};
|
||||
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
const [routes] = useState([
|
||||
{ key: "first", title: "Settings" },
|
||||
{ key: "second", title: "Preview" },
|
||||
]);
|
||||
|
||||
const renderTabBar = (props) => (
|
||||
<TabBar
|
||||
{...props}
|
||||
indicatorStyle={{ backgroundColor: theme.colorAccent }}
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
renderLabel={({ route, focused, color }) => (
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.textColorSecondary}
|
||||
activeColor={theme.colorAccent}
|
||||
pressColor={theme.rippleColor}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar
|
||||
title="Reader"
|
||||
onBackAction={navigation.goBack}
|
||||
style={{ elevation: 0 }}
|
||||
/>
|
||||
|
||||
<TabView
|
||||
navigationState={{ index, routes }}
|
||||
renderTabBar={renderTabBar}
|
||||
renderScene={renderScene}
|
||||
onIndexChange={setIndex}
|
||||
initialLayout={{ width: layout.width }}
|
||||
swipeEnabled={false}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ const ChapterItem = ({
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Row style={{ flex: 1, overflow: "hidden" }}>
|
||||
{bookmark === 1 && <ChapterBookmarkButton theme={theme} />}
|
||||
{!!bookmark && <ChapterBookmarkButton theme={theme} />}
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -28,7 +28,12 @@ import {
|
||||
const Updates = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { updates, loading } = useSelector((state) => state.updatesReducer);
|
||||
const {
|
||||
updates,
|
||||
lastUpdateTime = null,
|
||||
showLastUpdateTime = true,
|
||||
loading,
|
||||
} = useSelector((state) => state.updatesReducer);
|
||||
const { downloadQueue } = useSelector((state) => state.downloadsReducer);
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
@@ -171,6 +176,19 @@ const Updates = ({ navigation }) => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{showLastUpdateTime && lastUpdateTime && (
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 4,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Library last updated: {moment(lastUpdateTime).fromNow()}
|
||||
</Text>
|
||||
)}
|
||||
<FlatList
|
||||
contentContainerStyle={styles.flatList}
|
||||
data={searchText ? searchResults : updates}
|
||||
|
||||
+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;
|
||||
@@ -110,12 +110,11 @@ const parseNovelAndChapters = async (novelUrl) => {
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
let firstChapterNo, totalChapters;
|
||||
let totalChapters;
|
||||
|
||||
totalChapters = $(".header-stats > span").first().text().match(/\d+/)[0];
|
||||
firstChapterNo = $(".chapter-no").first().text().match(/\d+/)[0];
|
||||
|
||||
for (let i = firstChapterNo; i <= totalChapters; i++) {
|
||||
for (let i = 1; i <= totalChapters; i++) {
|
||||
const chapterName = "Chapter " + i;
|
||||
|
||||
const releaseDate = null;
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import cheerio from "react-native-cheerio";
|
||||
import NovelUpdatesScraper from "./novelupdates";
|
||||
|
||||
const sourceId = 72;
|
||||
const sourceName = "LightNovelUpdates";
|
||||
const baseUrl = "https://www.lightnovelupdates.com/";
|
||||
|
||||
const popularNovels = async (page) => {
|
||||
let url = `${baseUrl}novel/page/${page}/?m_orderby=rating`;
|
||||
let totalPages = 500;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
$(".page-item-detail").each(function () {
|
||||
const novelName = $(this).find(".h5 > a").text();
|
||||
const novelCover = $(this).find("img").attr("src");
|
||||
const novelUrl = $(this).find(".h5 > a").attr("href");
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return { totalPages, novels };
|
||||
};
|
||||
|
||||
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, novelUrl, url };
|
||||
|
||||
novel.novelName = $(".post-title > h1").text().trim();
|
||||
novel.novelCover =
|
||||
$(".summary_image > a > img").attr("src") ||
|
||||
"https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true";
|
||||
|
||||
$(".post-content_item").each(function (result) {
|
||||
detailName = $(this).find(".summary-heading > h5").text().trim();
|
||||
detail = $(this).find(".summary-content").text().trim();
|
||||
|
||||
switch (detailName) {
|
||||
case "Genre(s)":
|
||||
novel.genre = detail.trim().replace(/[\t\n]/g, ",");
|
||||
break;
|
||||
case "Author(s)":
|
||||
novel.author = detail.trim();
|
||||
break;
|
||||
case "Artist(s)":
|
||||
novel.status = detail.trim();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
novel.summary = $("div.summary__content").text();
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
const novelId = $(".rating-post-id").attr("value");
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("action", "manga_get_chapters");
|
||||
formData.append("manga", novelId);
|
||||
|
||||
const data = await fetch(
|
||||
"https://www.lightnovelupdates.com/wp-admin/admin-ajax.php",
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
const text = await data.text();
|
||||
|
||||
$ = cheerio.load(text);
|
||||
|
||||
$(".wp-manga-chapter").each(function () {
|
||||
const chapterName = $(this).find("a").text().trim();
|
||||
const releaseDate = $(this).find("span").text().trim();
|
||||
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 chapterName = "";
|
||||
const chapterText = (
|
||||
await NovelUpdatesScraper.parseChapter(novelUrl, chapterUrl)
|
||||
).chapterText;
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async (searchTerm) => {
|
||||
const url = `${baseUrl}?s=${searchTerm}&post_type=wp-manga&m_orderby=rating`;
|
||||
|
||||
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(".h4 > a").text();
|
||||
const novelCover = $(this).find("img").attr("src");
|
||||
const novelUrl = $(this).find(".h4 > a").attr("href");
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const LightNovelUpdatesScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default LightNovelUpdatesScraper;
|
||||
@@ -0,0 +1,158 @@
|
||||
import cheerio from "react-native-cheerio";
|
||||
import { Status } from "../helpers/constants";
|
||||
|
||||
const sourceId = 71;
|
||||
const sourceName = "NovelOnlineFull.com";
|
||||
const baseUrl = "https://novelonlinefull.com/";
|
||||
|
||||
const popularNovels = async (page) => {
|
||||
let url =
|
||||
baseUrl + "novel_list?type=topview&category=all&state=all&page=" + page;
|
||||
const totalPages = 2155;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
$(".update_item.list_category").each(function () {
|
||||
const novelName = $(this).find("h3").text().trim();
|
||||
const novelCover = $(this).find("img").attr("src");
|
||||
|
||||
let novelUrl = $(this).find("h3 > a").attr("href");
|
||||
novelUrl = novelUrl.replace(baseUrl + "novel/", "");
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return { novels, totalPages };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async (novelUrl) => {
|
||||
const url = baseUrl + "novel/" + novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novel = { sourceId, sourceName, url, novelUrl };
|
||||
|
||||
novel.novelName = $("h1").text().trim();
|
||||
|
||||
novel.novelCover = $(".info_image > img").attr("src");
|
||||
|
||||
novel.summary = $("#noidungm").text().trim();
|
||||
|
||||
novel.genre = $("div.truyen_if_wrap > ul > li:nth-child(3)")
|
||||
.text()
|
||||
.replace("GENRES: ", "")
|
||||
.replace(/ - /g, ",")
|
||||
.trim();
|
||||
|
||||
console.log(novel.genre);
|
||||
|
||||
novel.status =
|
||||
$(" div.truyen_if_wrap > ul > li:nth-child(4) > a")
|
||||
.text()
|
||||
.replace("STATUS", "")
|
||||
.trim() === "ONGOING"
|
||||
? Status.ONGOING
|
||||
: Status.COMPLETED;
|
||||
|
||||
novel.author = $("div.truyen_if_wrap > ul > li:nth-child(2)")
|
||||
.text()
|
||||
.replace("Author(s):", "")
|
||||
.trim();
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
$(".chapter-list")
|
||||
.find(".row")
|
||||
.each(function () {
|
||||
const releaseDate = $(this).find("span:nth-child(2)").text();
|
||||
|
||||
const chapterName = $(this).find("span:nth-child(1) > a").text();
|
||||
const chapterUrl = $(this).find("a").attr("href");
|
||||
|
||||
novelChapters.push({
|
||||
chapterName,
|
||||
chapterUrl,
|
||||
releaseDate,
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
let chapterName = $(".name_chapter").text();
|
||||
let chapterText = $("#vung_doc").html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async (searchTerm) => {
|
||||
const url = baseUrl + "search_novels/" + searchTerm;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
$(".update_item.list_category").each(function () {
|
||||
const novelName = $(this).find("h3").text();
|
||||
const novelCover = $(this).find("img").attr("src");
|
||||
|
||||
let novelUrl = $(this).find("h3 > a").attr("href");
|
||||
novelUrl = novelUrl.replace(baseUrl + "novel/", "");
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const NovelOnlineFullScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default NovelOnlineFullScraper;
|
||||
@@ -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;
|
||||
|
||||
@@ -11,9 +11,16 @@ const popularNovels = async (page) => {
|
||||
let url = `${baseUrl}list/all/all-onclick-${page - 1}.html`;
|
||||
const totalPages = 281;
|
||||
|
||||
const result = await fetch(url);
|
||||
const headers = new Headers({
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
|
||||
});
|
||||
|
||||
const result = await fetch(url, { method: "GET", headers });
|
||||
const body = await result.text();
|
||||
|
||||
console.log(body);
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
@@ -40,7 +47,12 @@ const popularNovels = async (page) => {
|
||||
const parseNovelAndChapters = async (novelUrl) => {
|
||||
const url = `${baseUrl}novel/${novelUrl}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const headers = new Headers({
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
|
||||
});
|
||||
|
||||
const result = await fetch(url, { method: "GET", headers });
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
@@ -104,7 +116,12 @@ const parseNovelAndChapters = async (novelUrl) => {
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = `${baseUrl}novel/${chapterUrl}.html`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const headers = new Headers({
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
|
||||
});
|
||||
|
||||
const result = await fetch(url, { method: "GET", headers });
|
||||
const body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
@@ -131,6 +148,8 @@ const searchNovels = async (searchTerm) => {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Referer: "https://www.readwn.com/search.html",
|
||||
Origin: "https://www.readwn.com",
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
|
||||
},
|
||||
method: "POST",
|
||||
body: QueryString.stringify({
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
import cheerio from "react-native-cheerio";
|
||||
import { Status } from "../helpers/constants";
|
||||
import NovelUpdatesScraper from "./novelupdates";
|
||||
|
||||
const sourceId = 62;
|
||||
|
||||
@@ -118,118 +119,10 @@ const parseNovelAndChapters = async (novelUrl) => {
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = chapterUrl;
|
||||
let result, body;
|
||||
|
||||
let chapterName = "";
|
||||
|
||||
try {
|
||||
result = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: headers,
|
||||
});
|
||||
body = await result.text();
|
||||
|
||||
$ = cheerio.load(body);
|
||||
|
||||
let isWuxiaWorld = result.url.toLowerCase().includes("wuxiaworld");
|
||||
|
||||
let isBlogspot = result.url.toLowerCase().includes("blogspot");
|
||||
|
||||
let isTumblr = result.url.toLowerCase().includes("tumblr");
|
||||
|
||||
/**
|
||||
* Checks if its a wwordpress site
|
||||
*/
|
||||
let isWordPress =
|
||||
$('meta[name="generator"]').attr("content") || $("footer").text();
|
||||
|
||||
if (isWordPress) {
|
||||
isWordPress =
|
||||
isWordPress.toLowerCase().includes("wordpress") ||
|
||||
isWordPress.includes("Site Kit by Google") ||
|
||||
$(".powered-by").text().toLowerCase().includes("wordpress") ||
|
||||
$(".entry-content").html() ||
|
||||
$("section.content").html()
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
|
||||
let isRainOfSnow = result.url.toLowerCase().includes("rainofsnow");
|
||||
let isRoyalRoad = result.url.toLowerCase().includes("royalroad");
|
||||
|
||||
let isWebNovel = result.url.toLowerCase().includes("webnovel");
|
||||
|
||||
let isHostedNovel = result.url.toLowerCase().includes("hostednovel");
|
||||
|
||||
let isScribbleHub = result.url.toLowerCase().includes("scribblehub");
|
||||
|
||||
if (isWuxiaWorld) {
|
||||
chapterText = $("#chapter-content").html();
|
||||
} else if (isRainOfSnow) {
|
||||
chapterText = $("div.content").html();
|
||||
} else if (isTumblr) {
|
||||
chapterText = $(".post").html();
|
||||
} else if (isBlogspot) {
|
||||
chapterText = $(".entry-content").html();
|
||||
} else if (isHostedNovel) {
|
||||
chapterText = $(".chapter").html();
|
||||
} else if (isScribbleHub) {
|
||||
chapterText = $("div.chp_raw").html();
|
||||
} else if (isRoyalRoad) {
|
||||
chapterText = $("div.chapter-content").html();
|
||||
} else if (isWordPress) {
|
||||
/**
|
||||
* Remove wordpress bloat tags
|
||||
*/
|
||||
|
||||
const bloatClasses = [
|
||||
".c-ads",
|
||||
"#madara-comments",
|
||||
"#comments",
|
||||
".sharedaddy",
|
||||
".wp-dark-mode-switcher",
|
||||
".wp-next-post-navi",
|
||||
".wp-block-buttons",
|
||||
".wp-block-columns",
|
||||
".post-cats",
|
||||
".sidebar",
|
||||
".author-avatar",
|
||||
];
|
||||
|
||||
bloatClasses.map((tag) => $(tag).remove());
|
||||
|
||||
chapterText =
|
||||
$(".entry-content").html() ||
|
||||
$(".single_post").html() ||
|
||||
$(".post-entry").html() ||
|
||||
$("article.post").html() ||
|
||||
$(".content").html() ||
|
||||
$("#content").html();
|
||||
} else if (isWebNovel) {
|
||||
chapterText = $(".cha-words").html();
|
||||
|
||||
if (!chapterText) {
|
||||
chapterText = $("._content").html();
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Remove unnecessary tags
|
||||
*/
|
||||
const tags = ["nav", "header", "footer", ".hidden"];
|
||||
|
||||
tags.map((tag) => $(tag).remove());
|
||||
|
||||
chapterText = $("body").html();
|
||||
}
|
||||
|
||||
if (!chapterText) {
|
||||
chapterText =
|
||||
"Chapter not available.\n\nReport if it's available in webview.";
|
||||
}
|
||||
} catch (error) {
|
||||
chapterText = `Chapter not available (Error: ${error.message}).\n\nReport if it's available in webview.`;
|
||||
}
|
||||
const chapterName = "";
|
||||
const chapterText = (
|
||||
await NovelUpdatesScraper.parseChapter(novelUrl, chapterUrl)
|
||||
).chapterText;
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
|
||||
@@ -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="((?:color|font-family|line-height|font-size|text-align).*?)"/gi,
|
||||
/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";
|
||||
@@ -71,6 +73,14 @@ import ReadwnScraper from "./en/readwn";
|
||||
import RanobeHubScraper from "./ru/ranobehub";
|
||||
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 = {
|
||||
@@ -143,6 +153,16 @@ export const getSource = (sourceId) => {
|
||||
68: ReadwnScraper,
|
||||
69: RanobeHubScraper,
|
||||
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"
|
||||
},
|
||||
@@ -481,5 +481,75 @@
|
||||
"sourceName": "Divine Dao Library",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/divinedaolibrary/icon.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 71,
|
||||
"url": "https://novelonlinefull.com/",
|
||||
"sourceName": "NovelOnlineFull.com",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelonlinefull/icon.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 72,
|
||||
"url": "https://www.lightnovelupdates.com/",
|
||||
"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