Compare commits
98 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 | |||
| 9b3d65dded | |||
| aa3a5c2d0c | |||
| 2aab5621a6 | |||
| b4b7e6770d | |||
| 0ed65d64e4 | |||
| a7aaa7d230 | |||
| e358b3753f | |||
| 780a67e1a6 | |||
| 5b57e78486 | |||
| 6430c11cae | |||
| 322bfc224d | |||
| a14d35f031 | |||
| f03b1703e2 | |||
| 40d2052f16 | |||
| 01814e92d0 | |||
| dffc9c3b99 | |||
| 8bb3ffe82c | |||
| c746081dee | |||
| d25fe85a64 | |||
| 92d9d53da7 | |||
| 17fcd96f16 | |||
| 31bada1c1a | |||
| 605bd6a514 | |||
| 01bac76bed | |||
| b5ed62d9f8 | |||
| fd21cc6e85 | |||
| 40f1495cf1 | |||
| 983b069dba | |||
| e21101ca28 | |||
| d381e878bc | |||
| bcf5632668 | |||
| ac8bca7f03 | |||
| 62a5e27b1e | |||
| 7c5a9a407f | |||
| d8d231cb90 | |||
| a5f90eaf8b | |||
| ec50189ea4 | |||
| ccdccdb6bb | |||
| 059f440713 | |||
| b695f57ef4 | |||
| db86b707e7 | |||
| 7db4e4b872 | |||
| a2c6e9fe58 | |||
| 6c27993454 | |||
| 63de0a3794 | |||
| 93fbcf1246 | |||
| 50357f7d6f |
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -6,6 +6,7 @@ import { Provider as PaperProvider } from "react-native-paper";
|
||||
import { Provider } from "react-redux";
|
||||
import { persistor, store } from "./src/redux/store";
|
||||
import { PersistGate } from "redux-persist/lib/integration/react";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
|
||||
import Main from "./src/navigators/Main";
|
||||
|
||||
@@ -16,6 +17,7 @@ const App = () => {
|
||||
const [loaded] = useFonts(fonts);
|
||||
|
||||
useEffect(() => {
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
createDB();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
<h1 align="center">
|
||||
<br>
|
||||
LNReader
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<p align="center">LNReader is a Tachiyomi-like, free and open source light novel reader for Android.</p>
|
||||
|
||||
---
|
||||
|
||||
## Screenshots:
|
||||
<p align="center">
|
||||
<img src="./.github/readme-images/screenshots.png" align="center" height="350" />
|
||||
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
|
||||
<p>
|
||||
<h1 align="center">
|
||||
LNReader
|
||||
</h1>
|
||||
<p align="center">
|
||||
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
|
||||
</p>
|
||||
<div align="center">
|
||||
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/LNReader/lnreader/CI">
|
||||
</a>
|
||||
<a href="https://discord.gg/QdcWN4MD63">
|
||||
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader/releases/latest">
|
||||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<img src="./.github/readme-images/screenshots.png" align="center" />
|
||||
</p>
|
||||
|
||||
## Download
|
||||
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
|
||||
|
||||
## Sources
|
||||
LNReader does not have any affiliation with the content providers available.
|
||||
|
||||
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
|
||||
|
||||
## License
|
||||
[MIT ](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
|
||||
|
||||
@@ -136,7 +136,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.1.1"
|
||||
versionName "1.1.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>
|
||||
Binary file not shown.
Binary file not shown.
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(),
|
||||
|
||||
Binary file not shown.
Binary file not shown.
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-eject",
|
||||
"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({
|
||||
|
||||
+5
-123
@@ -1,17 +1,11 @@
|
||||
import React, { useRef } from "react";
|
||||
import { View, TextInput, StyleSheet } from "react-native";
|
||||
import {
|
||||
TouchableRipple,
|
||||
IconButton,
|
||||
Appbar as MaterialAppbar,
|
||||
} from "react-native-paper";
|
||||
import Constants from "expo-constants";
|
||||
import React from "react";
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Appbar as MaterialAppbar } from "react-native-paper";
|
||||
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
|
||||
export const Appbar = ({ title, onBackAction, style, children }) => {
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<MaterialAppbar.Header
|
||||
@@ -28,115 +22,3 @@ export const Appbar = ({ title, onBackAction, style, children }) => {
|
||||
</MaterialAppbar.Header>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchAppbar = ({
|
||||
placeholder,
|
||||
getSearchResults,
|
||||
setSearchText,
|
||||
searchText,
|
||||
getNovels,
|
||||
setLoading,
|
||||
screen,
|
||||
openWebView,
|
||||
}) => {
|
||||
const searchRef = useRef(null);
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
borderless
|
||||
onPress={() => searchRef.current.focus()}
|
||||
style={[
|
||||
styles.searchAppbarContainer,
|
||||
{ backgroundColor: theme.searchBarColor },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, flexDirection: "row" }}>
|
||||
{screen === "Library" ? (
|
||||
<IconButton
|
||||
icon="magnify"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginLeft: 0 }}
|
||||
size={23}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginLeft: 0 }}
|
||||
size={23}
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
onChangeText={(text) => {
|
||||
setSearchText(text);
|
||||
if (screen === "Library") {
|
||||
getSearchResults(text);
|
||||
}
|
||||
}}
|
||||
onSubmitEditing={() => {
|
||||
if (screen === "Extension") {
|
||||
getSearchResults(searchText);
|
||||
}
|
||||
}}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</View>
|
||||
{searchText !== "" && (
|
||||
<IconButton
|
||||
icon="close"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={() => {
|
||||
setLoading?.(true);
|
||||
getNovels();
|
||||
setSearchText("");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{openWebView && (
|
||||
<IconButton
|
||||
icon="earth"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={openWebView}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchAppbarContainer: {
|
||||
marginTop: Constants.statusBarHeight + 8,
|
||||
height: 48,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 12,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,11 +4,11 @@ import { View } from "react-native";
|
||||
const BottomSheetHandle = ({ theme }) => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.textColorHint,
|
||||
height: 5,
|
||||
width: 30,
|
||||
backgroundColor: `${theme.textColorPrimary}66`,
|
||||
height: 4,
|
||||
width: 40,
|
||||
borderRadius: 50,
|
||||
top: 10,
|
||||
top: 8,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
export const Checkbox = ({ label, status, onPress, theme }) => (
|
||||
<View
|
||||
style={[
|
||||
styles.checkboxContainer,
|
||||
status && { backgroundColor: theme.rippleColor },
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: status ? theme.colorAccent : theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{status && (
|
||||
<MaterialCommunityIcons
|
||||
name="check"
|
||||
color={theme.colorAccent}
|
||||
size={21}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export const SortItem = ({ label, status, onPress, theme }) => (
|
||||
<View
|
||||
style={[
|
||||
styles.checkboxContainer,
|
||||
status && { backgroundColor: theme.rippleColor },
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: status ? theme.colorAccent : theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{status && (
|
||||
<MaterialCommunityIcons
|
||||
name={status === "asc" ? "arrow-up" : "arrow-down"}
|
||||
color={theme.colorAccent}
|
||||
size={21}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
checkboxContainer: {
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginVertical: 4,
|
||||
},
|
||||
pressable: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
icon: {
|
||||
position: "absolute",
|
||||
right: 16,
|
||||
alignSelf: "center",
|
||||
},
|
||||
});
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginHorizontal: 4,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 10,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={
|
||||
selected
|
||||
? theme.colorAccent
|
||||
: color
|
||||
? color
|
||||
: theme.textColorPrimary
|
||||
}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export const ToggleColorButton = ({
|
||||
selected,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
onPress,
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 50,
|
||||
overflow: "hidden",
|
||||
marginHorizontal: 6,
|
||||
height: 44,
|
||||
width: 44,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: textColor }}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
backgroundColor: backgroundColor,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
{selected && (
|
||||
<MaterialCommunityIcons
|
||||
name="check"
|
||||
color={textColor}
|
||||
size={24}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import { IconButton } from "react-native-paper";
|
||||
|
||||
const icons = ["(・o・;)", "Σ(ಠ_ಠ)", "ಥ_ಥ", "(˘・_・˘)", "(; ̄Д ̄)", "(・Д・。"];
|
||||
|
||||
export const ErrorView = ({ errorName, actions, theme }) => (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<Text style={[styles.emptyViewIcon, { color: theme.textColorHint }]}>
|
||||
{/* {icons[Math.floor(Math.random() * 5)]} */}
|
||||
ಥ_ಥ
|
||||
</Text>
|
||||
<Text style={[styles.emptyViewText, { color: theme.textColorHint }]}>
|
||||
{errorName}
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
{actions.map((action) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
margin: 16,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
borderless: false,
|
||||
}}
|
||||
onPress={action.onPress}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: 20,
|
||||
minWidth: 100,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
icon={action.icon}
|
||||
color={theme.textColorHint}
|
||||
size={24}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: theme.textColorHint,
|
||||
}}
|
||||
>
|
||||
{action.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
emptyViewContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
emptyViewIcon: {
|
||||
fontSize: 45,
|
||||
},
|
||||
emptyViewText: {
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
textAlign: "center",
|
||||
paddingHorizontal: 30,
|
||||
},
|
||||
});
|
||||
|
||||
+57
-10
@@ -1,19 +1,19 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { List, Divider as PaperDivider } from "react-native-paper";
|
||||
import { List as PaperList, Divider as PaperDivider } from "react-native-paper";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
const ListSection = ({ children }) => (
|
||||
<List.Section style={styles.listSection}>{children}</List.Section>
|
||||
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
|
||||
);
|
||||
|
||||
const ListSubHeader = ({ children, theme }) => (
|
||||
<List.Subheader
|
||||
<PaperList.Subheader
|
||||
style={[styles.listSubHeader, { color: theme.colorAccent }]}
|
||||
>
|
||||
{children}
|
||||
</List.Subheader>
|
||||
</PaperList.Subheader>
|
||||
);
|
||||
|
||||
const ListItem = ({
|
||||
@@ -27,7 +27,7 @@ const ListItem = ({
|
||||
titleStyle,
|
||||
style,
|
||||
}) => (
|
||||
<List.Item
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
style={style}
|
||||
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
|
||||
@@ -37,7 +37,7 @@ const ListItem = ({
|
||||
left={() => (
|
||||
<View style={{ justifyContent: "center" }}>
|
||||
{icon && (
|
||||
<List.Icon
|
||||
<PaperList.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{
|
||||
@@ -67,13 +67,13 @@ const Divider = ({ theme }) => (
|
||||
);
|
||||
|
||||
const InfoItem = ({ title, icon, theme }) => (
|
||||
<List.Item
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
|
||||
titleNumberOfLines={5}
|
||||
left={() =>
|
||||
icon && (
|
||||
<List.Icon
|
||||
<PaperList.Icon
|
||||
color={theme.textColorSecondary}
|
||||
icon={icon}
|
||||
style={{ marginVertical: 0 }}
|
||||
@@ -83,7 +83,54 @@ const InfoItem = ({ title, icon, theme }) => (
|
||||
/>
|
||||
);
|
||||
|
||||
export { ListSection, ListSubHeader, ListItem, Divider, InfoItem };
|
||||
const ColorItem = ({ title, description, theme, onPress }) => (
|
||||
<Pressable
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View>
|
||||
<Text style={{ color: theme.textColorPrimary, fontSize: 16 }}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: description,
|
||||
height: 24,
|
||||
width: 24,
|
||||
borderRadius: 50,
|
||||
marginRight: 16,
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const Icon = ({ icon, theme }) => (
|
||||
<PaperList.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const List = {
|
||||
Section: ListSection,
|
||||
SubHeader: ListSubHeader,
|
||||
Item: ListItem,
|
||||
Divider,
|
||||
InfoItem,
|
||||
ColorItem,
|
||||
Icon,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listSection: {
|
||||
|
||||
+35
-31
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import { StyleSheet, View, Text, Pressable } from "react-native";
|
||||
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
|
||||
const ListView = ({
|
||||
item,
|
||||
@@ -10,39 +10,43 @@ const ListView = ({
|
||||
inLibraryBadge,
|
||||
theme,
|
||||
onPress,
|
||||
isSelected,
|
||||
onLongPress,
|
||||
}) => {
|
||||
return (
|
||||
<TouchableRipple
|
||||
borderless
|
||||
centered
|
||||
rippleColor={theme.rippleColor}
|
||||
style={styles.listView}
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={[
|
||||
styles.listView,
|
||||
isSelected && { backgroundColor: theme.rippleColor },
|
||||
]}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
>
|
||||
<>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={[
|
||||
styles.extensionIcon,
|
||||
inLibraryBadge && { opacity: 0.5 },
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary },
|
||||
styles.novelName,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
<View style={styles.badgeContainer}>
|
||||
{downloadBadge}
|
||||
{unreadBadge}
|
||||
{inLibraryBadge}
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
<FastImage
|
||||
source={{
|
||||
uri:
|
||||
item.novelCover && !item.novelCover.startsWith("/")
|
||||
? item.novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
style={[
|
||||
styles.extensionIcon,
|
||||
inLibraryBadge && { opacity: 0.5 },
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[{ color: theme.textColorPrimary }, styles.novelName]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
<View style={styles.badgeContainer}>
|
||||
{downloadBadge}
|
||||
{unreadBadge}
|
||||
{inLibraryBadge}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,15 @@ import ListView from "./ListView";
|
||||
import { getDeviceOrientation } from "../services/utils/helpers";
|
||||
import { useSettings } from "../hooks/reduxHooks";
|
||||
|
||||
const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
|
||||
const NovelCover = ({
|
||||
item,
|
||||
onPress,
|
||||
libraryStatus,
|
||||
theme,
|
||||
isSelected,
|
||||
onLongPress,
|
||||
selectedNovels,
|
||||
}) => {
|
||||
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
|
||||
useSettings();
|
||||
|
||||
@@ -31,18 +39,32 @@ const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
|
||||
[]
|
||||
);
|
||||
|
||||
const selectNovel = () => onLongPress && onLongPress(item.novelId);
|
||||
|
||||
return displayMode !== 2 ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1 / getNovelsPerRow(),
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
flex: 1 / getNovelsPerRow(),
|
||||
borderRadius: 6,
|
||||
overflow: "hidden",
|
||||
margin: 2,
|
||||
},
|
||||
isSelected && {
|
||||
backgroundColor: theme.colorAccent,
|
||||
opacity: 0.8,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
style={styles.opac}
|
||||
onPress={onPress}
|
||||
onPress={
|
||||
selectedNovels && selectedNovels.length > 0
|
||||
? selectNovel
|
||||
: onPress
|
||||
}
|
||||
onLongPress={selectNovel}
|
||||
>
|
||||
<>
|
||||
<View style={styles.badgeContainer}>
|
||||
@@ -64,7 +86,13 @@ const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
|
||||
)}
|
||||
</View>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
source={{
|
||||
uri:
|
||||
item.novelCover &&
|
||||
!item.novelCover.startsWith("/")
|
||||
? item.novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
style={[
|
||||
{ height: getHeight(), borderRadius: 4 },
|
||||
libraryStatus && { opacity: 0.5 },
|
||||
@@ -110,7 +138,13 @@ const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
|
||||
}
|
||||
inLibraryBadge={libraryStatus && <InLibraryBadge theme={theme} />}
|
||||
theme={theme}
|
||||
onPress={onPress}
|
||||
onPress={
|
||||
selectedNovels && selectedNovels.length > 0
|
||||
? selectNovel
|
||||
: onPress
|
||||
}
|
||||
onLongPress={selectNovel}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -140,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>
|
||||
@@ -221,12 +263,10 @@ const styles = StyleSheet.create({
|
||||
padding: 8,
|
||||
},
|
||||
linearGradient: {
|
||||
bottom: -1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
opac: {
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 4,
|
||||
padding: 4.8,
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
},
|
||||
@@ -245,7 +285,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
|
||||
downloadBadge: {
|
||||
backgroundColor: "#47a84a",
|
||||
backgroundColor: "#2979FF",
|
||||
color: "#FFFFFF",
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
|
||||
@@ -33,7 +33,10 @@ const NovelList = ({
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.flatListCont}
|
||||
contentContainerStyle={[
|
||||
styles.flatListCont,
|
||||
displayMode !== 2 && { paddingHorizontal: 4 },
|
||||
]}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={[orientation, getNovelsPerRow()]}
|
||||
data={data}
|
||||
@@ -54,6 +57,5 @@ const styles = StyleSheet.create({
|
||||
flatListCont: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
@@ -78,20 +78,20 @@ export const Searchbar = ({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchbarContainer: {
|
||||
marginTop: StatusBar.currentHeight + 4,
|
||||
height: 48,
|
||||
marginTop: StatusBar.currentHeight + 8,
|
||||
height: 46,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 6,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 12,
|
||||
elevation: 2,
|
||||
backgroundColor: "white",
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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({});
|
||||
@@ -126,6 +126,32 @@ export const markChapterUnread = async (chapterId) => {
|
||||
});
|
||||
};
|
||||
|
||||
const markAllChaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE novelId = ?`;
|
||||
|
||||
export const markAllChaptersRead = async (novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
markAllChaptersReadQuery,
|
||||
[novelId],
|
||||
(tx, res) => {},
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markAllChaptersUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE novelId = ?`;
|
||||
|
||||
export const markAllChaptersUnread = async (novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
markAllChaptersUnreadQuery,
|
||||
[novelId],
|
||||
(tx, res) => {},
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const isChapterDownloadedQuery = `SELECT * FROM downloads WHERE downloadChapterId=?`;
|
||||
|
||||
export const isChapterDownloaded = async (chapterId) => {
|
||||
@@ -147,7 +173,7 @@ export const isChapterDownloaded = async (chapterId) => {
|
||||
);
|
||||
};
|
||||
|
||||
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?)`;
|
||||
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)`;
|
||||
|
||||
export const downloadChapter = async (
|
||||
sourceId,
|
||||
@@ -166,15 +192,10 @@ export const downloadChapter = async (
|
||||
);
|
||||
tx.executeSql(
|
||||
downloadChapterQuery,
|
||||
[
|
||||
chapterId,
|
||||
chapter.chapterName,
|
||||
chapter.chapterText,
|
||||
chapter.prevChapter,
|
||||
chapter.nextChapter,
|
||||
],
|
||||
(tx, res) =>
|
||||
console.log(`Downloaded Chapter ${chapter.chapterUrl}`),
|
||||
[chapterId, chapter.chapterName, chapter.chapterText],
|
||||
(tx, res) => {
|
||||
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
@@ -261,7 +282,7 @@ export const markPreviousChaptersUnread = async (chapterId, novelId) => {
|
||||
};
|
||||
|
||||
const getDownloadedChaptersQuery = `
|
||||
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover
|
||||
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover, novels.novelUrl
|
||||
FROM chapters
|
||||
JOIN novels
|
||||
ON chapters.novelId = novels.novelId
|
||||
|
||||
@@ -13,7 +13,7 @@ const getHistoryQuery = `
|
||||
ORDER BY history.historyTimeRead DESC
|
||||
`;
|
||||
|
||||
export const getHistory = async () => {
|
||||
export const getHistoryFromDb = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
import BackgroundService from "react-native-background-actions";
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
|
||||
import { fetchChapters, fetchNovel } from "../../services/Source/source";
|
||||
import { insertChapters } from "./ChapterQueries";
|
||||
@@ -46,6 +47,17 @@ export const followNovel = async (followed, novelId) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const unfollowNovel = async (novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE novels SET followed = 0 WHERE novelId = ?",
|
||||
[novelId],
|
||||
(tx, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const checkNovelInCacheQuery = `SELECT * FROM novels WHERE novelUrl=? LIMIT 1`;
|
||||
|
||||
export const checkNovelInCache = (novelUrl) => {
|
||||
@@ -188,8 +200,42 @@ export const migrateNovel = async (sourceId, novelUrl) => {
|
||||
await BackgroundService.updateNotification({
|
||||
progressBar: { value: 1, indeterminate: false },
|
||||
});
|
||||
await BackgroundService.stop();
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateNovelInfo = async (info, novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE novels SET novelName = ?, novelSummary = ?, author = ?, genre = ?, status = ? WHERE novelId = ?",
|
||||
[
|
||||
info.novelName,
|
||||
info.novelSummary,
|
||||
info.author,
|
||||
info.genre,
|
||||
info.status,
|
||||
novelId,
|
||||
],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const pickCustomNovelCover = async (novelId) => {
|
||||
const image = await DocumentPicker.getDocumentAsync({ type: "image/*" });
|
||||
|
||||
if (image.type === "success" && image.uri) {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE novels SET novelCover = ? WHERE novelId = ?",
|
||||
[image.uri, novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
|
||||
return image.uri;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ const getUpdatesQuery = `
|
||||
ON updates.chapterId = chapters.chapterId
|
||||
JOIN novels
|
||||
ON updates.novelId = novels.novelId
|
||||
WHERE date(updates.updateTime) > date('now','-3 months')
|
||||
ORDER BY updates.updateTime DESC
|
||||
`;
|
||||
|
||||
@@ -36,3 +37,6 @@ export const deleteNovelUpdates = (novelId) => {
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const clearUpdates = () =>
|
||||
db.transaction((tx) => tx.executeSql("DELETE FROM updates; VACCUM;"));
|
||||
|
||||
@@ -4,8 +4,6 @@ export const createDownloadTableQuery = `
|
||||
downloadChapterId INTEGER NOT NULL,
|
||||
chapterName TEXT,
|
||||
chapterText TEXT,
|
||||
nextChapter TEXT,
|
||||
prevChapter TEXT,
|
||||
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
|
||||
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
|
||||
};
|
||||
|
||||
const isNewVersion = (versionTag) => {
|
||||
let currentVersion = "v1.1.1";
|
||||
let currentVersion = "v1.1.4";
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
let newVersion = versionTag.replace(regex, "");
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { shallowEqual, useSelector } from "react-redux";
|
||||
import { showChapterTitlesAction } from "../redux/novel/novel.actions";
|
||||
import { useSelector } from "react-redux";
|
||||
import { darkTheme } from "../theme/theme";
|
||||
|
||||
const useTheme = () => {
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
|
||||
return theme;
|
||||
return theme || darkTheme;
|
||||
};
|
||||
|
||||
const useReaderSettings = () => {
|
||||
|
||||
@@ -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";
|
||||
@@ -9,17 +10,28 @@ import History from "../screens/History/History";
|
||||
import Browse from "../screens/Browse/Browse";
|
||||
import More from "../screens/More/More";
|
||||
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
import { useSettings, useTheme } from "../hooks/reduxHooks";
|
||||
|
||||
const Tab = createMaterialBottomTabNavigator();
|
||||
|
||||
const BottomNavigator = () => {
|
||||
const theme = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const {
|
||||
showHistoryTab = true,
|
||||
showUpdatesTab = true,
|
||||
showLabelsInNav = false,
|
||||
} = useSettings();
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
barStyle={{ backgroundColor: theme.colorPrimary }}
|
||||
barStyle={{
|
||||
backgroundColor: theme.colorPrimary,
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
activeColor={theme.colorAccent}
|
||||
shifting={!showLabelsInNav}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Library"
|
||||
@@ -34,32 +46,36 @@ const BottomNavigator = () => {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Updates"
|
||||
component={Updates}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="alert-decagram-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="History"
|
||||
component={History}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="history"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{showUpdatesTab && (
|
||||
<Tab.Screen
|
||||
name="Updates"
|
||||
component={Updates}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="alert-decagram-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showHistoryTab && (
|
||||
<Tab.Screen
|
||||
name="History"
|
||||
component={History}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="history"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Tab.Screen
|
||||
name="Browse"
|
||||
component={Browse}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createStackNavigator } from "@react-navigation/stack";
|
||||
import { setBarColor } from "../theme/utils/setBarColor";
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
import { githubUpdateChecker } from "../hooks/githubUpdateChecker";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
|
||||
// Navigators
|
||||
import BottomNavigator from "./BottomNavigator";
|
||||
@@ -30,7 +31,10 @@ const MainNavigator = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setBarColor(theme);
|
||||
setTimeout(async () => {
|
||||
await SplashScreen.hideAsync();
|
||||
setBarColor(theme);
|
||||
}, 500);
|
||||
}, [theme]);
|
||||
|
||||
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,7 @@ export const resumeDownloads = (chapters) => async (dispatch) => {
|
||||
parameters: {
|
||||
delay: 1000,
|
||||
},
|
||||
linkingURI: "lnreader://downloads",
|
||||
progressBar: {
|
||||
max: chapters.length,
|
||||
value: 0,
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import {
|
||||
CLEAR_NOVEL_HISTORY,
|
||||
GET_HISTORY,
|
||||
LOAD_HISTORY,
|
||||
CLEAR_HISTORY,
|
||||
} from "./history.types";
|
||||
import {
|
||||
getHistory,
|
||||
insertHistory,
|
||||
deleteHistory,
|
||||
deleteAllHistory,
|
||||
} from "../../database/queries/HistoryQueries";
|
||||
import { SET_LAST_READ } from "../preferences/preference.types";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
export const getHistoryAction = () => async (dispatch) => {
|
||||
dispatch({ type: LOAD_HISTORY });
|
||||
|
||||
const history = await getHistory();
|
||||
|
||||
const groups = history.reduce((groups, update) => {
|
||||
var dateParts = update.historyTimeRead.split("-");
|
||||
var jsDate = new Date(
|
||||
dateParts[0],
|
||||
dateParts[1] - 1,
|
||||
dateParts[2].substr(0, 2)
|
||||
);
|
||||
const date = jsDate.toISOString();
|
||||
if (!groups[date]) {
|
||||
groups[date] = [];
|
||||
}
|
||||
groups[date].push(update);
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
const groupedHistory = Object.keys(groups).map((date) => {
|
||||
return {
|
||||
date,
|
||||
novels: groups[date],
|
||||
};
|
||||
});
|
||||
|
||||
// console.log(groupedHistory);
|
||||
|
||||
dispatch({ type: GET_HISTORY, payload: groupedHistory });
|
||||
};
|
||||
|
||||
export const insertHistoryAction = (novelId, chapterId) => async (dispatch) => {
|
||||
await insertHistory(novelId, chapterId);
|
||||
|
||||
dispatch({
|
||||
type: SET_LAST_READ,
|
||||
payload: { novelId, chapterId },
|
||||
});
|
||||
|
||||
dispatch(getHistoryAction());
|
||||
};
|
||||
|
||||
export const deleteHistoryAction = (historyId) => async (dispatch) => {
|
||||
await deleteHistory(historyId);
|
||||
console.log("Delete History Action Called");
|
||||
dispatch(getHistoryAction());
|
||||
};
|
||||
|
||||
export const clearAllHistoryAction = () => async (dispatch) => {
|
||||
try {
|
||||
await deleteAllHistory();
|
||||
|
||||
showToast("History deleted");
|
||||
|
||||
dispatch({ type: CLEAR_HISTORY });
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import {
|
||||
CLEAR_HISTORY,
|
||||
CLEAR_NOVEL_HISTORY,
|
||||
GET_HISTORY,
|
||||
LOAD_HISTORY,
|
||||
} from "./history.types";
|
||||
|
||||
const initialState = {
|
||||
history: [],
|
||||
loading: true,
|
||||
};
|
||||
|
||||
const historyReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case LOAD_HISTORY:
|
||||
return { ...state, loading: true };
|
||||
case GET_HISTORY:
|
||||
return { ...state, history: payload, loading: false };
|
||||
// case CLEAR_NOVEL_HISTORY:
|
||||
// return {
|
||||
// ...state,
|
||||
// history: state.history.filter(
|
||||
// (item) => item.novelId !== payload.novelId
|
||||
// ),
|
||||
// };
|
||||
case CLEAR_HISTORY:
|
||||
return { ...state, loading: false, history: [] };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default historyReducer;
|
||||
@@ -1,5 +0,0 @@
|
||||
export const LOAD_HISTORY = "LOAD_HISTORY";
|
||||
export const GET_HISTORY = "GET_HISTORY";
|
||||
export const UPDATE_NOVEL_HISTORY = "UPDATE_NOVEL_HISTORY";
|
||||
export const CLEAR_NOVEL_HISTORY = "CLEAR_NOVEL_HISTORY";
|
||||
export const CLEAR_HISTORY = "CLEAR_HISTORY";
|
||||
@@ -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,83 +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",
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -8,25 +8,36 @@ import {
|
||||
SET_AMOLED_MODE,
|
||||
} from "./settings.types";
|
||||
import {
|
||||
amoledDarkTheme,
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
midnightDuskTheme,
|
||||
greenAppleTheme,
|
||||
irisBlueTheme,
|
||||
hotPinkTheme,
|
||||
strawberryDaiquiri,
|
||||
takoTheme,
|
||||
yinYangTheme,
|
||||
springBlossomTheme,
|
||||
colorsAmoled,
|
||||
oceanicTheme,
|
||||
yangTheme,
|
||||
yotsubaTheme,
|
||||
takoLightTheme,
|
||||
} from "../../theme/theme";
|
||||
|
||||
const themes = {
|
||||
0: amoledDarkTheme,
|
||||
1: lightTheme,
|
||||
2: darkTheme,
|
||||
3: midnightDuskTheme,
|
||||
4: greenAppleTheme,
|
||||
5: irisBlueTheme,
|
||||
6: hotPinkTheme,
|
||||
6: oceanicTheme,
|
||||
7: strawberryDaiquiri,
|
||||
8: takoTheme,
|
||||
9: yinYangTheme,
|
||||
10: springBlossomTheme,
|
||||
11: yangTheme,
|
||||
12: yotsubaTheme,
|
||||
13: takoLightTheme,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -35,20 +46,70 @@ const themes = {
|
||||
* 0 -> Compact
|
||||
* 1 -> Comfortable
|
||||
* 2 -> List
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reader Theme
|
||||
*
|
||||
* 1 -> Dark
|
||||
* 2 -> White
|
||||
* 3 -> Sepia
|
||||
* 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)",
|
||||
@@ -57,17 +118,8 @@ const initialState = {
|
||||
padding: 5,
|
||||
fontFamily: "",
|
||||
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,
|
||||
};
|
||||
|
||||
const settingsReducer = (state = initialState, action) => {
|
||||
@@ -102,9 +154,10 @@ const settingsReducer = (state = initialState, action) => {
|
||||
...state,
|
||||
theme: payload.val
|
||||
? {
|
||||
...themes[0],
|
||||
...colorsAmoled,
|
||||
colorAccent: state.theme.colorAccent,
|
||||
rippleColor: state.theme.rippleColor,
|
||||
colorButtonText: state.theme.colorButtonText,
|
||||
id: state.theme.id,
|
||||
name: state.theme.name,
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import settingsReducer from "./settings/settings.reducer";
|
||||
import libraryReducer from "./library/library.reducer";
|
||||
import sourceReducer from "./source/source.reducers";
|
||||
import novelReducer from "./novel/novel.reducer";
|
||||
import historyReducer from "./history/history.reducer";
|
||||
import updatesReducer from "./updates/updates.reducer";
|
||||
import trackerReducer from "./tracker/tracker.reducer";
|
||||
import preferenceReducer from "./preferences/preference.reducer";
|
||||
@@ -26,7 +25,6 @@ const persistedReducer = persistReducer(
|
||||
libraryReducer,
|
||||
sourceReducer,
|
||||
novelReducer,
|
||||
historyReducer,
|
||||
updatesReducer,
|
||||
trackerReducer,
|
||||
preferenceReducer,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { GET_UPDATES } from "./updates.types";
|
||||
|
||||
import store from "../store";
|
||||
import { GET_UPDATES, SET_LAST_UPDATE_TIME } from "./updates.types";
|
||||
|
||||
import { getUpdates } from "../../database/queries/UpdateQueries";
|
||||
import { updateAllNovels } from "../../services/updates";
|
||||
@@ -38,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));
|
||||
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
|
||||
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
import NovelCover from "../../components/NovelCover";
|
||||
import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
import ErrorView from "../../components/ErrorView";
|
||||
|
||||
import { useTheme, useSettings } from "../../hooks/reduxHooks";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
import { getDeviceOrientation } from "../../services/utils/helpers";
|
||||
import { scrapeSearchResults } from "./discover/MyAnimeListScraper";
|
||||
|
||||
const BrowseMalScreen = ({ navigation, route }) => {
|
||||
const theme = useTheme();
|
||||
const settings = useSettings();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [novels, setNovels] = useState();
|
||||
const [error, setError] = useState();
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const baseUrl = "https://api.jikan.moe/v3/";
|
||||
const malUrl = "https://myanimelist.net/topmanga.php?type=lightnovels";
|
||||
|
||||
const getNovels = async () => {
|
||||
try {
|
||||
const url = `${baseUrl}top/manga/1/novels`;
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
|
||||
setNovels(data.top);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
setNovels([]);
|
||||
setLoading(false);
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const clearSearchbar = () => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setSearchText("");
|
||||
};
|
||||
|
||||
const getSearchResults = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const data = await scrapeSearchResults(searchText);
|
||||
|
||||
setNovels(data);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
setNovels([]);
|
||||
setLoading(false);
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getNovels();
|
||||
}, []);
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<NovelCover
|
||||
item={{ novelName: item.title, novelCover: item.image_url }}
|
||||
onPress={() =>
|
||||
navigation.navigate("GlobalSearch", {
|
||||
novelName: item.title,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
const { displayMode, novelsPerRow } = settings;
|
||||
|
||||
const orientation = getDeviceOrientation();
|
||||
|
||||
const getNovelsPerRow = () => {
|
||||
if (displayMode === 2) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (orientation === "landscape") {
|
||||
return 6;
|
||||
} else {
|
||||
return novelsPerRow;
|
||||
}
|
||||
};
|
||||
|
||||
const ListEmptyComponent = () => (
|
||||
<ErrorView
|
||||
error={error || "No results found"}
|
||||
onRetry={() => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setError();
|
||||
}}
|
||||
openWebView={() => WebBrowser.openBrowserAsync(topNovelsUrl)}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<Searchbar
|
||||
theme={theme}
|
||||
placeholder="Search MyAnimeList"
|
||||
backAction="arrow-left"
|
||||
onBackAction={() => navigation.goBack()}
|
||||
searchText={searchText}
|
||||
onChangeText={(text) => setSearchText(text)}
|
||||
onSubmitEditing={getSearchResults}
|
||||
clearSearchbar={clearSearchbar}
|
||||
actions={[
|
||||
{
|
||||
icon: "earth",
|
||||
onPress: () => WebBrowser.openBrowserAsync(malUrl),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator size={60} color={theme.colorAccent} />
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.novelsContainer}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={getNovelsPerRow()}
|
||||
data={novels}
|
||||
keyExtractor={(item) => item.mal_id.toString()}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowseMalScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
novelsContainer: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { FlatList, Pressable, Text, View } from "react-native";
|
||||
|
||||
import { Switch } from "react-native-paper";
|
||||
@@ -6,7 +6,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { ListSubHeader } from "../../components/List";
|
||||
import { List } from "../../components/List";
|
||||
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import { setAppSettings } from "../../redux/settings/settings.actions";
|
||||
@@ -18,10 +18,23 @@ import {
|
||||
const BrowseSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { filters = [], showMyAnimeList = true } = useSelector(
|
||||
(state) => state.sourceReducer
|
||||
);
|
||||
const languages = ["English", "Spanish", "Japanese", "Chinese", "Arabic"];
|
||||
const languages = [
|
||||
"English",
|
||||
"Spanish",
|
||||
"Japanese",
|
||||
"Chinese",
|
||||
"Arabic",
|
||||
"Indonesian",
|
||||
"Russian",
|
||||
"Turkish",
|
||||
"French",
|
||||
"Portuguese (Brazil)",
|
||||
].sort();
|
||||
|
||||
const { searchAllSources = false } = useSettings();
|
||||
|
||||
const renderItem = ({ item }) => {
|
||||
@@ -50,68 +63,83 @@ const BrowseSettings = ({ navigation }) => {
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar onBackAction={navigation.goBack} title="Sources" />
|
||||
<View>
|
||||
<ListSubHeader theme={theme}>Global Search</ListSubHeader>
|
||||
<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>
|
||||
<ListSubHeader theme={theme}>Discover</ListSubHeader>
|
||||
|
||||
<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>
|
||||
<ListSubHeader theme={theme}>Languages</ListSubHeader>
|
||||
<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>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
import { Searchbar } from "../../../components/Searchbar/Searchbar";
|
||||
import ErrorView from "../../../components/ErrorView";
|
||||
import { ErrorView } from "../../../components/ErrorView/ErrorView";
|
||||
|
||||
import { useTheme, useSettings } from "../../../hooks/reduxHooks";
|
||||
import { showToast } from "../../../hooks/showToast";
|
||||
@@ -106,13 +106,18 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
|
||||
const ListEmptyComponent = () => (
|
||||
<ErrorView
|
||||
error={error || "No results found"}
|
||||
onRetry={() => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setError();
|
||||
}}
|
||||
openWebView={() => WebBrowser.openBrowserAsync(topNovelsUrl)}
|
||||
errorName={error || "No results found"}
|
||||
actions={[
|
||||
{
|
||||
name: "Retry",
|
||||
onPress: () => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setError();
|
||||
},
|
||||
icon: "reload",
|
||||
},
|
||||
]}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -74,9 +74,6 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
setProgress(
|
||||
(progress) => progress + 1 / globalSearchSources.length
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(
|
||||
globalSearchSources[i].sourceName + ": " + error.message
|
||||
@@ -92,6 +89,9 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
setProgress(
|
||||
(progress) => progress + 1 / globalSearchSources.length
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
import { StyleSheet, View, Text, Image, Pressable } from "react-native";
|
||||
|
||||
const GlobalSearchNovelCover = ({
|
||||
novel,
|
||||
@@ -13,18 +11,21 @@ const GlobalSearchNovelCover = ({
|
||||
const { novelName, novelCover } = novel;
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<TouchableRipple
|
||||
borderless
|
||||
centered
|
||||
rippleColor={theme.rippleColor}
|
||||
<View style={{ flex: 1, borderRadius: 6, overflow: "hidden" }}>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.novelContainer}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{ uri: novelCover }}
|
||||
source={{
|
||||
uri:
|
||||
novelCover && !novelCover.startsWith("/")
|
||||
? novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
style={[
|
||||
styles.novelCover,
|
||||
inLibrary && { opacity: 0.5 },
|
||||
@@ -41,7 +42,7 @@ const GlobalSearchNovelCover = ({
|
||||
{novelName}
|
||||
</Text>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,11 +9,7 @@ const GlobalSearchNovelList = ({ data, theme, library, navigation }) => {
|
||||
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
|
||||
);
|
||||
|
||||
const navigateToNovel = (item) =>
|
||||
navigation.navigate("Novel", {
|
||||
...item,
|
||||
sourceId: item.sourceId,
|
||||
});
|
||||
const navigateToNovel = (item) => navigation.push("Novel", item);
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<GlobalSearchNovelCover
|
||||
|
||||
@@ -61,9 +61,6 @@ const MigrationNovels = ({ navigation, route }) => {
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
setProgress(
|
||||
(progress) => progress + 1 / migrationSources.length
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
|
||||
@@ -78,6 +75,7 @@ const MigrationNovels = ({ navigation, route }) => {
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
setProgress((progress) => progress + 1 / migrationSources.length);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+490
-127
@@ -5,15 +5,19 @@ import {
|
||||
Text,
|
||||
StatusBar,
|
||||
ScrollView,
|
||||
TouchableWithoutFeedback,
|
||||
Dimensions,
|
||||
Pressable,
|
||||
} from "react-native";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
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";
|
||||
import WebView from "react-native-webview";
|
||||
|
||||
import {
|
||||
getChapterFromDB,
|
||||
@@ -30,9 +34,7 @@ import {
|
||||
useTrackingStatus,
|
||||
} from "../../hooks/reduxHooks";
|
||||
import { updateChaptersRead } from "../../redux/tracker/tracker.actions";
|
||||
import { insertHistoryAction } from "../../redux/history/history.actions";
|
||||
import {
|
||||
errorTextColor,
|
||||
readerBackground,
|
||||
readerLineHeight,
|
||||
readerTextColor,
|
||||
@@ -48,8 +50,15 @@ import EmptyView from "../../components/EmptyView";
|
||||
import ChapterFooter from "./components/ChapterFooter";
|
||||
import VerticalScrollbar from "./components/VerticalScrollbar";
|
||||
import GestureRecognizer from "react-native-swipe-gestures";
|
||||
import TextToComponents from "./TextToComponent";
|
||||
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
|
||||
import { insertHistory } from "../../database/queries/HistoryQueries";
|
||||
import { SET_LAST_READ } from "../../redux/preferences/preference.types";
|
||||
import { htmlToText } from "../../sources/helpers/htmlToText";
|
||||
import { setAppSettings } from "../../redux/settings/settings.actions";
|
||||
import { cleanHtml } from "../../sources/helpers/cleanHtml";
|
||||
import { useBatteryLevel } from "react-native-device-info";
|
||||
import moment from "moment";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const Chapter = ({ route, navigation }) => {
|
||||
const {
|
||||
@@ -69,15 +78,22 @@ const Chapter = ({ route, navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const reader = useReaderSettings();
|
||||
const showScrollPercentage = useSelector(
|
||||
(state) => state.settingsReducer.showScrollPercentage
|
||||
);
|
||||
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();
|
||||
@@ -94,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) {
|
||||
@@ -111,6 +224,7 @@ const Chapter = ({ route, navigation }) => {
|
||||
}
|
||||
} else {
|
||||
const res = await fetchChapter(sourceId, novelUrl, chapterUrl);
|
||||
|
||||
setChapter(res);
|
||||
}
|
||||
|
||||
@@ -134,18 +248,58 @@ const Chapter = ({ route, navigation }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setImmersiveMode();
|
||||
getChapter(chapterId);
|
||||
!incognitoMode && dispatch(insertHistoryAction(novelId, chapterId));
|
||||
return () => {
|
||||
|
||||
if (!incognitoMode) {
|
||||
insertHistory(novelId, chapterId);
|
||||
dispatch({
|
||||
type: SET_LAST_READ,
|
||||
payload: { novelId, chapterId },
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
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;
|
||||
@@ -196,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({
|
||||
@@ -214,7 +372,14 @@ const Chapter = ({ route, navigation }) => {
|
||||
}, []);
|
||||
|
||||
const hideHeader = () => {
|
||||
!hidden && setImmersiveMode();
|
||||
if (!hidden) {
|
||||
setImmersiveMode();
|
||||
} else {
|
||||
if (fullScreenMode) {
|
||||
StatusBar.setHidden(false);
|
||||
showNavigationBar();
|
||||
}
|
||||
}
|
||||
setHidden(!hidden);
|
||||
};
|
||||
|
||||
@@ -263,13 +428,19 @@ const Chapter = ({ route, navigation }) => {
|
||||
})
|
||||
: showToast("There's no next chapter");
|
||||
|
||||
const onPressLink = async (link) => {
|
||||
setLoading(true);
|
||||
const res = await fetchChapter(sourceId, novelUrl, link);
|
||||
setChapter(res);
|
||||
setLoading(false);
|
||||
const enableSwipeGestures = () => {
|
||||
dispatch(setAppSettings("swipeGestures", !swipeGestures));
|
||||
showToast(
|
||||
swipeGestures ? "Swipe gestures disabled" : "Swipe gestured enabled"
|
||||
);
|
||||
};
|
||||
|
||||
const enableAutoScroll = () =>
|
||||
dispatch(setAppSettings("autoScroll", !autoScroll));
|
||||
|
||||
const enableWebView = () =>
|
||||
dispatch(setAppSettings("useWebViewForChapter", !useWebViewForChapter));
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
@@ -278,129 +449,323 @@ 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}
|
||||
/>
|
||||
<ScrollView
|
||||
ref={(ref) => (scrollViewRef.current = ref)}
|
||||
contentContainerStyle={[
|
||||
styles.screenContainer,
|
||||
{ backgroundColor: readerBackground(reader.theme) },
|
||||
]}
|
||||
onScroll={onScroll}
|
||||
onContentSizeChange={(x, y) => setContentSize(y)}
|
||||
showsVerticalScrollIndicator={false}
|
||||
<GestureRecognizer
|
||||
onSwipeRight={swipeGestures && navigateToPrevChapter}
|
||||
onSwipeLeft={swipeGestures && navigateToNextChapter}
|
||||
config={config}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{error ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<EmptyView
|
||||
icon="Σ(ಠ_ಠ)"
|
||||
description={error}
|
||||
style={{ color: errorTextColor(reader.theme) }}
|
||||
>
|
||||
<IconButton
|
||||
icon="reload"
|
||||
size={25}
|
||||
style={{ margin: 0, marginTop: 16 }}
|
||||
color={errorTextColor(reader.theme)}
|
||||
onPress={() => {
|
||||
getChapter(chapterId);
|
||||
setLoading(true);
|
||||
setError();
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
<ScrollView
|
||||
ref={(ref) => (scrollViewRef.current = ref)}
|
||||
contentContainerStyle={[
|
||||
styles.screenContainer,
|
||||
{ backgroundColor: readerBackground(reader.theme) },
|
||||
]}
|
||||
onScroll={onScroll}
|
||||
onContentSizeChange={(x, y) => setContentSize(y)}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{error ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<EmptyView
|
||||
icon="Σ(ಠ_ಠ)"
|
||||
description={error}
|
||||
style={{
|
||||
color: errorTextColor(reader.theme),
|
||||
color:
|
||||
reader.textColor ||
|
||||
readerTextColor(reader.theme),
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
</EmptyView>
|
||||
</View>
|
||||
) : loading ? (
|
||||
<LoadingScreen theme={theme} />
|
||||
) : (
|
||||
<GestureRecognizer
|
||||
onSwipeRight={
|
||||
swipeGestures && navigateToPrevChapter
|
||||
}
|
||||
onSwipeLeft={swipeGestures && navigateToNextChapter}
|
||||
config={config}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingVertical: 16,
|
||||
paddingBottom: 32,
|
||||
paddingHorizontal: `${reader.padding}%`,
|
||||
}}
|
||||
onLayout={scrollToSavedProgress}
|
||||
<IconButton
|
||||
icon="reload"
|
||||
size={25}
|
||||
style={{ margin: 0, marginTop: 16 }}
|
||||
color={
|
||||
reader.textColor ||
|
||||
readerTextColor(reader.theme)
|
||||
}
|
||||
onPress={() => {
|
||||
getChapter(chapterId);
|
||||
setLoading(true);
|
||||
setError();
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color:
|
||||
reader.textColor ||
|
||||
readerTextColor(reader.theme),
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
</EmptyView>
|
||||
</View>
|
||||
) : loading ? (
|
||||
<LoadingScreen theme={theme} />
|
||||
) : (
|
||||
<TouchableWithoutFeedback
|
||||
style={{ flex: 1 }}
|
||||
onPress={hideHeader}
|
||||
onLayout={scrollToSavedProgress}
|
||||
>
|
||||
<TextToComponents
|
||||
text={chapter.chapterText}
|
||||
textStyle={readerStyles}
|
||||
textSize={reader.textSize}
|
||||
textColor={
|
||||
reader.textColor ||
|
||||
readerTextColor(reader.theme)
|
||||
}
|
||||
textSelectable={textSelectable}
|
||||
onPressLink={onPressLink}
|
||||
theme={theme}
|
||||
/>
|
||||
</Pressable>
|
||||
</GestureRecognizer>
|
||||
)}
|
||||
<Portal>
|
||||
<ReaderSheet
|
||||
theme={theme}
|
||||
reader={reader}
|
||||
dispatch={dispatch}
|
||||
navigation={navigation}
|
||||
bottomSheetRef={readerSheetRef}
|
||||
selectText={textSelectable}
|
||||
showScrollPercentage={showScrollPercentage}
|
||||
/>
|
||||
</Portal>
|
||||
</ScrollView>
|
||||
<VerticalScrollbar
|
||||
theme={theme}
|
||||
hide={hidden}
|
||||
setLoading={setLoading}
|
||||
contentSize={contentSize}
|
||||
scrollViewRef={scrollViewRef}
|
||||
scrollPercentage={scrollPercentage}
|
||||
setScrollPercentage={setScrollPercentage}
|
||||
/>
|
||||
{useWebViewForChapter ? (
|
||||
<WebView
|
||||
style={{
|
||||
backgroundColor: readerBackground(
|
||||
reader.theme
|
||||
),
|
||||
}}
|
||||
originWhitelist={["*"]}
|
||||
scalesPageToFit={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onScroll={onScroll}
|
||||
onNavigationStateChange={async ({
|
||||
url,
|
||||
}) => {
|
||||
if (
|
||||
(sourceId === 50 ||
|
||||
sourceId == 62) &&
|
||||
url !== "about:blank"
|
||||
) {
|
||||
setLoading(true);
|
||||
const res = await fetchChapter(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
url
|
||||
);
|
||||
setChapter(res);
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
source={{
|
||||
html: `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<style>
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
padding-top: ${StatusBar.currentHeight};
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
padding-left: ${reader.padding}%;
|
||||
padding-right: ${reader.padding}%;
|
||||
padding-bottom: 30px;
|
||||
font-size: ${reader.textSize}px;
|
||||
color: ${reader.textColor};
|
||||
|
||||
text-align: ${reader.textAlign};
|
||||
line-height: ${reader.lineHeight};
|
||||
font-family: ${reader.fontFamily};
|
||||
}
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
a {
|
||||
color: ${theme.colorAccent};
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
${reader.customCSS}
|
||||
@font-face {
|
||||
font-family: ${reader.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${
|
||||
reader.fontFamily
|
||||
}.ttf");
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${cleanHtml(chapter.chapterText)}
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: `${reader.padding}%`,
|
||||
paddingTop: StatusBar.currentHeight,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={readerStyles}
|
||||
selectable={textSelectable}
|
||||
>
|
||||
{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}
|
||||
reader={reader}
|
||||
dispatch={dispatch}
|
||||
navigation={navigation}
|
||||
bottomSheetRef={readerSheetRef}
|
||||
selectText={textSelectable}
|
||||
autoScroll={autoScroll}
|
||||
useWebViewForChapter={useWebViewForChapter}
|
||||
showScrollPercentage={showScrollPercentage}
|
||||
showBatteryAndTime={showBatteryAndTime}
|
||||
fullScreenMode={fullScreenMode}
|
||||
swipeGestures={swipeGestures}
|
||||
enableSwipeGestures={enableSwipeGestures}
|
||||
/>
|
||||
</Portal>
|
||||
{!useWebViewForChapter && (
|
||||
<VerticalScrollbar
|
||||
theme={theme}
|
||||
hide={hidden}
|
||||
setLoading={setLoading}
|
||||
contentSize={contentSize}
|
||||
scrollViewRef={scrollViewRef}
|
||||
scrollPercentage={scrollPercentage}
|
||||
setScrollPercentage={setScrollPercentage}
|
||||
/>
|
||||
)}
|
||||
<ChapterFooter
|
||||
theme={theme}
|
||||
swipeGestures={swipeGestures}
|
||||
enableSwipeGestures={enableSwipeGestures}
|
||||
enableWebView={enableWebView}
|
||||
dispatch={dispatch}
|
||||
nextChapter={nextChapter}
|
||||
prevChapter={prevChapter}
|
||||
hide={hidden}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
@@ -410,20 +775,18 @@ const Chapter = ({ route, navigation }) => {
|
||||
export default Chapter;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screenContainer: {
|
||||
flexGrow: 1,
|
||||
paddingTop: StatusBar.currentHeight,
|
||||
},
|
||||
scrollPercentage: {
|
||||
screenContainer: { flexGrow: 1 },
|
||||
scrollPercentageContainer: {
|
||||
width: "100%",
|
||||
position: "absolute",
|
||||
paddingVertical: 4,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: 30,
|
||||
textAlign: "center",
|
||||
textAlignVertical: "center",
|
||||
alignSelf: "center",
|
||||
fontFamily: "noto-sans",
|
||||
fontWeight: "bold",
|
||||
zIndex: 1,
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 32,
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
import React, { memo, useCallback, useEffect } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import FitImage from "react-native-fit-image";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
const getHeadingSize = (size) => {
|
||||
const headings = {
|
||||
h1: 6,
|
||||
h2: 5,
|
||||
h3: 4,
|
||||
h4: 3,
|
||||
h5: 2,
|
||||
h6: 1,
|
||||
strong: 0,
|
||||
};
|
||||
|
||||
return headings[size];
|
||||
};
|
||||
|
||||
const Header = ({ size, text, textStyle, textSelectable, textSize }) => (
|
||||
<Text
|
||||
style={[
|
||||
textStyle,
|
||||
{
|
||||
fontSize: textSize + getHeadingSize(size),
|
||||
fontWeight: "bold",
|
||||
},
|
||||
]}
|
||||
selectable={textSelectable}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const DefaultText = ({ text, textStyle, textSelectable }) => (
|
||||
<Text style={textStyle} selectable={textSelectable}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const ItalicText = ({ text, textStyle, textSelectable }) => (
|
||||
<Text
|
||||
style={[textStyle, { fontStyle: "italic" }]}
|
||||
selectable={textSelectable}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const HorizontalRule = ({ textColor }) => (
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
flex: 1,
|
||||
borderBottomColor: textColor,
|
||||
marginVertical: 16,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const Link = ({
|
||||
link,
|
||||
text,
|
||||
textStyle,
|
||||
textSelectable,
|
||||
onPressLink,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
textStyle,
|
||||
{
|
||||
alignSelf: "flex-start",
|
||||
color: theme.colorAccent,
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
]}
|
||||
selectable={textSelectable}
|
||||
onPress={() => onPressLink(link)}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const TextToComponents = ({
|
||||
text,
|
||||
textStyle,
|
||||
textSelectable,
|
||||
textSize,
|
||||
textColor,
|
||||
onPressLink,
|
||||
theme,
|
||||
}) => {
|
||||
try {
|
||||
regex = /(\[(?:\n|.)*?\]\(.*?\))/;
|
||||
|
||||
let parseText = text.trim().split(regex);
|
||||
return parseText.map((part, index) => {
|
||||
if (part) {
|
||||
let match = part.match(/\[((?:\n|.)*?)\]\((.*?)\)/);
|
||||
|
||||
if (match) {
|
||||
tag = match[1];
|
||||
text = match[2];
|
||||
|
||||
if (tag.startsWith("Anchor:")) {
|
||||
link = tag.replace("Anchor: ", "");
|
||||
tag = "a";
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
case "h1":
|
||||
return (
|
||||
<Header
|
||||
size="h1"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h2":
|
||||
return (
|
||||
<Header
|
||||
size="h2"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h3":
|
||||
return (
|
||||
<Header
|
||||
size="h3"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h4":
|
||||
return (
|
||||
<Header
|
||||
size="h4"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h5":
|
||||
return (
|
||||
<Header
|
||||
size="h5"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h6":
|
||||
return (
|
||||
<Header
|
||||
size="h6"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "strong":
|
||||
return (
|
||||
<Header
|
||||
size="strong"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "em":
|
||||
return (
|
||||
<ItalicText
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
|
||||
case "a":
|
||||
return (
|
||||
<Link
|
||||
link={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
text={link}
|
||||
onPressLink={onPressLink}
|
||||
theme={theme}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
case "img":
|
||||
return (
|
||||
<FitImage source={{ uri: text }} key={index} />
|
||||
);
|
||||
case "hr":
|
||||
return (
|
||||
<HorizontalRule
|
||||
key={index}
|
||||
textColor={textColor}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<DefaultText
|
||||
text={part}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<DefaultText
|
||||
text={part}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return (
|
||||
<DefaultText
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(TextToComponents);
|
||||
@@ -1,11 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { Appbar } from "react-native-paper";
|
||||
import {
|
||||
getNextChapterFromDB,
|
||||
getPrevChapterFromDB,
|
||||
} from "../../../database/queries/ChapterQueries";
|
||||
import FadeView from "../../../components/Common/CrossFadeView";
|
||||
import { bookmarkChapterAction } from "../../../redux/novel/novel.actions";
|
||||
|
||||
const ChapterAppbar = ({
|
||||
@@ -16,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.6)",
|
||||
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,25 +1,24 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Dimensions, Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, View } from "react-native";
|
||||
import { IconButton } from "react-native-paper";
|
||||
import {
|
||||
getNextChapterFromDB,
|
||||
getPrevChapterFromDB,
|
||||
} from "../../../database/queries/ChapterQueries";
|
||||
import { useSettings } from "../../../hooks/reduxHooks";
|
||||
import { showToast } from "../../../hooks/showToast";
|
||||
import { setAppSettings } from "../../../redux/settings/settings.actions";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import FadeView from "../../../components/Common/CrossFadeView";
|
||||
|
||||
const ChapterFooter = ({
|
||||
hide,
|
||||
theme,
|
||||
swipeGestures,
|
||||
dispatch,
|
||||
readerSheetRef,
|
||||
scrollViewRef,
|
||||
navigateToNextChapter,
|
||||
navigateToPrevChapter,
|
||||
nextChapter,
|
||||
prevChapter,
|
||||
useWebViewForChapter,
|
||||
enableSwipeGestures,
|
||||
autoScroll,
|
||||
enableWebView,
|
||||
enableAutoScroll,
|
||||
}) => {
|
||||
const rippleConfig = {
|
||||
color: theme.rippleColor,
|
||||
@@ -27,25 +26,24 @@ const ChapterFooter = ({
|
||||
radius: 50,
|
||||
};
|
||||
|
||||
const enableSwipeGestures = () => {
|
||||
dispatch(setAppSettings("swipeGestures", !swipeGestures));
|
||||
showToast(
|
||||
swipeGestures ? "Swipe gestures disabled" : "Swipe gestured enabled"
|
||||
);
|
||||
};
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
if (hide) {
|
||||
return null;
|
||||
} else {
|
||||
return (
|
||||
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.6)",
|
||||
backgroundColor: `${theme.colorPrimary}E6`,
|
||||
flexDirection: "row",
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
@@ -57,7 +55,7 @@ const ChapterFooter = ({
|
||||
icon="chevron-left"
|
||||
size={26}
|
||||
disabled={!prevChapter}
|
||||
color="#FFFFFF"
|
||||
color={theme.textColorPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
@@ -69,26 +67,58 @@ const ChapterFooter = ({
|
||||
icon="gesture-swipe"
|
||||
disabled={!swipeGestures}
|
||||
size={26}
|
||||
color="#FFFFFF"
|
||||
color={theme.textColorPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
{!useWebViewForChapter && (
|
||||
<>
|
||||
<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}
|
||||
style={styles.buttonStyles}
|
||||
onPress={() => scrollViewRef.current.scrollTo({})}
|
||||
onPress={enableWebView}
|
||||
>
|
||||
<IconButton
|
||||
icon="format-vertical-align-top"
|
||||
icon="language-html5"
|
||||
disabled={!useWebViewForChapter}
|
||||
size={26}
|
||||
color="#FFFFFF"
|
||||
color={theme.textColorPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
</Pressable> */}
|
||||
<Pressable
|
||||
android_ripple={rippleConfig}
|
||||
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}
|
||||
@@ -99,12 +129,12 @@ const ChapterFooter = ({
|
||||
icon="chevron-right"
|
||||
size={26}
|
||||
disabled={!nextChapter}
|
||||
color="#FFFFFF"
|
||||
color={theme.textColorPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
</FadeView>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChapterFooter;
|
||||
@@ -115,5 +145,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingVertical: 8,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text, Dimensions, Pressable } from "react-native";
|
||||
import React, { memo, useCallback, useState } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
Dimensions,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
useWindowDimensions,
|
||||
Animated,
|
||||
} from "react-native";
|
||||
|
||||
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
|
||||
import Slider from "@react-native-community/slider";
|
||||
import { ToggleButton, IconButton, Menu, Switch } from "react-native-paper";
|
||||
import { IconButton, Menu, Switch } from "react-native-paper";
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
|
||||
import BottomSheetHandle from "../../../components/BottomSheetHandle";
|
||||
@@ -14,407 +24,486 @@ import {
|
||||
setAppSettings,
|
||||
setReaderSettings,
|
||||
} from "../../../redux/settings/settings.actions";
|
||||
import {
|
||||
ToggleButton,
|
||||
ToggleColorButton,
|
||||
} from "../../../components/Common/ToggleButton";
|
||||
|
||||
const ReaderSettingTitle = ({ title }) => (
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
);
|
||||
|
||||
const ReaderSheet = ({
|
||||
theme,
|
||||
reader,
|
||||
dispatch,
|
||||
navigation,
|
||||
bottomSheetRef,
|
||||
showBatteryAndTime,
|
||||
selectText,
|
||||
autoScroll,
|
||||
useWebViewForChapter,
|
||||
showScrollPercentage,
|
||||
swipeGestures,
|
||||
enableSwipeGestures,
|
||||
fullScreenMode,
|
||||
}) => {
|
||||
const [fontMenu, setFontMenu] = useState(false);
|
||||
const openFontMenu = () => setFontMenu(true);
|
||||
const closeFontMenu = () => setFontMenu(false);
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
|
||||
const ReaderSettingTitle = ({ title }) => (
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
);
|
||||
const presetThemes = [
|
||||
{ value: 2, backgroundColor: "#f5f5fa", textColor: "#111111" },
|
||||
{ value: 3, backgroundColor: "#F7DFC6", textColor: "#593100" },
|
||||
{ value: 6, backgroundColor: "#dce5e2", textColor: "#000000" },
|
||||
{ value: 4, backgroundColor: "#292832", textColor: "#CCCCCC" },
|
||||
{
|
||||
value: 1,
|
||||
backgroundColor: "#000000",
|
||||
textColor: "rgba(255,255,255,0.7)",
|
||||
},
|
||||
];
|
||||
|
||||
const textColor = {
|
||||
1: "rgba(255,255,255,0.7)",
|
||||
2: "#111111",
|
||||
3: "#593100",
|
||||
4: "#CCCCCC",
|
||||
5: "#CCCCCC",
|
||||
};
|
||||
const textAlignments = [
|
||||
{ value: "left", icon: "format-align-left" },
|
||||
{ value: "center", icon: "format-align-center" },
|
||||
{ value: "justify", icon: "format-align-justify" },
|
||||
{ value: "right", icon: "format-align-right" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 470, bottom: 0 }}
|
||||
snappingPoints={[0, 470]}
|
||||
draggableRange={{ top: 390, bottom: 0 }}
|
||||
snappingPoints={[0, 390]}
|
||||
showBackdrop={true}
|
||||
backdropOpacity={0}
|
||||
height={390}
|
||||
>
|
||||
<View style={styles.contentContainer}>
|
||||
<ScrollView
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<BottomSheetHandle theme={theme} />
|
||||
{/* <IconButton
|
||||
icon="cog-outline"
|
||||
color="#FFFFFF"
|
||||
size={24}
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "ReaderSettings",
|
||||
})
|
||||
}
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
}}
|
||||
/> */}
|
||||
<View style={styles.readerSettingsContainer}>
|
||||
<ReaderSettingTitle title="Text Size" />
|
||||
<Slider
|
||||
style={{
|
||||
width: Dimensions.get("window").width,
|
||||
height: 40,
|
||||
}}
|
||||
value={reader.textSize}
|
||||
minimumValue={12}
|
||||
maximumValue={20}
|
||||
step={1}
|
||||
minimumTrackTintColor={theme.colorAccent}
|
||||
maximumTrackTintColor="#000000"
|
||||
thumbTintColor={theme.colorAccent}
|
||||
onSlidingComplete={(value) =>
|
||||
dispatch(setReaderSettings("textSize", value))
|
||||
}
|
||||
/>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Reader Theme" />
|
||||
<Row>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) => {
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"theme",
|
||||
value ?? reader.theme
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textColor",
|
||||
textColor[value] ?? reader.textColor
|
||||
)
|
||||
);
|
||||
}}
|
||||
value={reader.theme}
|
||||
>
|
||||
<ToggleButton
|
||||
icon={reader.theme === 1 && "check"}
|
||||
color="rgba(255,255,255,0.7)"
|
||||
value={1}
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
borderBottomRightRadius: 50,
|
||||
}}
|
||||
/>
|
||||
{/* <ToggleButton
|
||||
icon={reader.theme === 5 && "check"}
|
||||
color="#CCCCCC"
|
||||
value={5}
|
||||
style={{
|
||||
backgroundColor: "#2B2C30",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
borderBottomRightRadius: 50,
|
||||
}}
|
||||
/> */}
|
||||
<ToggleButton
|
||||
icon={reader.theme === 4 && "check"}
|
||||
color="#CCCCCC"
|
||||
value={4}
|
||||
style={{
|
||||
backgroundColor: "#292832",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
borderBottomRightRadius: 50,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon={reader.theme === 2 && "check"}
|
||||
color="#111111"
|
||||
value={2}
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
borderRadius: 50,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon={reader.theme === 3 && "check"}
|
||||
color="#593100"
|
||||
value={3}
|
||||
style={{
|
||||
backgroundColor: "#F7DFC6",
|
||||
marginHorizontal: 10,
|
||||
borderRadius: 50,
|
||||
borderTopLeftRadius: 50,
|
||||
borderBottomLeftRadius: 50,
|
||||
}}
|
||||
/>
|
||||
</ToggleButton.Row>
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Text Align" />
|
||||
<Row>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textAlign",
|
||||
value ?? reader.textAlign
|
||||
)
|
||||
)
|
||||
}
|
||||
value={reader.textAlign}
|
||||
>
|
||||
<ToggleButton
|
||||
icon="format-align-left"
|
||||
color={
|
||||
reader.textAlign === "left"
|
||||
? theme.colorAccent
|
||||
: "#FFFFFF"
|
||||
}
|
||||
value="left"
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-align-justify"
|
||||
color={
|
||||
reader.textAlign === "justify"
|
||||
? theme.colorAccent
|
||||
: "#FFFFFF"
|
||||
}
|
||||
value="justify"
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-align-right"
|
||||
color={
|
||||
reader.textAlign === "right"
|
||||
? theme.colorAccent
|
||||
: "#FFFFFF"
|
||||
}
|
||||
value="right"
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
}}
|
||||
/>
|
||||
</ToggleButton.Row>
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Padding" />
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.padding <= 0 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"padding",
|
||||
reader.padding - 1
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{`${reader.padding}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.padding >= 10 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"padding",
|
||||
reader.padding + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Line Height" />
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={
|
||||
reader.lineHeight <= 1.3 ? true : false
|
||||
}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"lineHeight",
|
||||
reader.lineHeight - 0.1
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{`${Math.round(reader.lineHeight * 10) / 10}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.lineHeight >= 2 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"lineHeight",
|
||||
reader.lineHeight + 0.1
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Pressable
|
||||
<View style={{ flex: 1, paddingVertical: 16, paddingTop: 24 }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
alignItems: "center",
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
onPress={openFontMenu}
|
||||
>
|
||||
<ReaderSettingTitle title="Font Style" />
|
||||
<Menu
|
||||
visible={fontMenu}
|
||||
onDismiss={closeFontMenu}
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
anchor={
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontFamily: reader.fontFamily,
|
||||
paddingHorizontal: 16,
|
||||
}}
|
||||
>
|
||||
{fonts.find(
|
||||
(font) =>
|
||||
font.fontFamily ===
|
||||
reader.fontFamily
|
||||
).name || "Default"}
|
||||
</Text>
|
||||
}
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{fonts.map((font) => (
|
||||
<Menu.Item
|
||||
key={font.fontFamily}
|
||||
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(
|
||||
"fontFamily",
|
||||
font.fontFamily
|
||||
"padding",
|
||||
reader.padding - 1
|
||||
)
|
||||
)
|
||||
}
|
||||
icon={
|
||||
font.fontFamily === reader.fontFamily &&
|
||||
"check"
|
||||
}
|
||||
title={font.name}
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
<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={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
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(
|
||||
@@ -424,7 +513,13 @@ const ReaderSheet = ({
|
||||
)
|
||||
}
|
||||
>
|
||||
<ReaderSettingTitle title="Show Progress Percentage" />
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
Show progress percentage
|
||||
</Text>
|
||||
<Switch
|
||||
value={showScrollPercentage}
|
||||
onValueChange={() =>
|
||||
@@ -439,35 +534,28 @@ const ReaderSheet = ({
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings("textSelectable", !selectText)
|
||||
)
|
||||
}
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={enableSwipeGestures}
|
||||
>
|
||||
<View>
|
||||
<ReaderSettingTitle title="Select Text " />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
(Justify does not work with selectable text)
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
Swipe gestures
|
||||
</Text>
|
||||
<Switch
|
||||
value={selectText}
|
||||
onValueChange={() =>
|
||||
value={swipeGestures}
|
||||
onValueChange={enableSwipeGestures}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
{!useWebViewForChapter && (
|
||||
<Pressable
|
||||
style={styles.switchStyle}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"textSelectable",
|
||||
@@ -475,16 +563,31 @@ const ReaderSheet = ({
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Select text
|
||||
</Text>
|
||||
<Switch
|
||||
value={selectText}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"textSelectable",
|
||||
!selectText
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Bottomsheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReaderSheet;
|
||||
export default memo(ReaderSheet);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
@@ -502,4 +605,12 @@ const styles = StyleSheet.create({
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
switchStyle: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import WebView from "react-native-webview";
|
||||
|
||||
export const WebViewReader = ({ style, css, text, onScroll }) => (
|
||||
<WebView
|
||||
style={style}
|
||||
originWhitelist={["*"]}
|
||||
scalesPageToFit={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onScroll={onScroll}
|
||||
source={{
|
||||
html: `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<style>
|
||||
${css}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${text}
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -5,6 +5,7 @@ export const readerBackground = (val) => {
|
||||
3: "#F7DFC6",
|
||||
4: "#292832",
|
||||
5: "#2B2C30",
|
||||
6: "#dce5e2",
|
||||
};
|
||||
|
||||
return backgroundColor[val] || val;
|
||||
@@ -17,6 +18,7 @@ export const readerTextColor = (val) => {
|
||||
3: "#593100",
|
||||
4: "#CCCCCC",
|
||||
5: "#CCCCCC",
|
||||
6: "#000000",
|
||||
};
|
||||
|
||||
return textColor[val] || val;
|
||||
@@ -29,9 +31,3 @@ export const readerLineHeight = (fontSize, lineHeightMultiplier) => {
|
||||
|
||||
return lineHeight;
|
||||
};
|
||||
|
||||
export const errorTextColor = (val) => {
|
||||
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.54)";
|
||||
|
||||
return textColor;
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
@@ -13,7 +13,7 @@ import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
|
||||
import NovelCover from "../../components/NovelCover";
|
||||
import NovelList from "../../components/NovelList";
|
||||
import ErrorView from "../../components/ErrorView";
|
||||
import { ErrorView } from "../../components/ErrorView/ErrorView";
|
||||
|
||||
const Extension = ({ navigation, route }) => {
|
||||
const { sourceId, sourceName, url } = route.params;
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -126,13 +134,18 @@ const Extension = ({ navigation, route }) => {
|
||||
|
||||
const listEmptyComponent = () => (
|
||||
<ErrorView
|
||||
error={error || "No results found"}
|
||||
onRetry={() => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setError();
|
||||
}}
|
||||
openWebView={() => WebBrowser.openBrowserAsync(url)}
|
||||
errorName={error || "No results found"}
|
||||
actions={[
|
||||
{
|
||||
name: "Retry",
|
||||
onPress: () => {
|
||||
getNovels();
|
||||
setLoading(true);
|
||||
setError();
|
||||
},
|
||||
icon: "reload",
|
||||
},
|
||||
]}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
@@ -193,7 +206,8 @@ const Extension = ({ navigation, route }) => {
|
||||
data={novels}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={listEmptyComponent()}
|
||||
onScroll={onScroll}
|
||||
// onScroll={onScroll}
|
||||
onEndReached={getNextPage}
|
||||
ListFooterComponent={
|
||||
!searchText &&
|
||||
page < totalPages &&
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { StyleSheet, FlatList, ActivityIndicator, Text } from "react-native";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
import moment from "moment";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
import HistoryItem from "./HistoryItem";
|
||||
@@ -10,18 +17,52 @@ import RemoveHistoryDialog from "./RemoveHistoryDialog";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
|
||||
import { getHistoryAction } from "../../redux/history/history.actions";
|
||||
import { dateFormat } from "../../services/utils/constants";
|
||||
import { useTheme } from "../../hooks/reduxHooks";
|
||||
import {
|
||||
deleteHistory,
|
||||
getHistoryFromDb,
|
||||
} from "../../database/queries/HistoryQueries";
|
||||
|
||||
const History = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { history, loading } = useSelector((state) => state.historyReducer);
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
|
||||
const [history, setHistory] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const getHistory = async () => {
|
||||
const history = await getHistoryFromDb();
|
||||
|
||||
const groups = history.reduce((groups, update) => {
|
||||
var dateParts = update.historyTimeRead.split("-");
|
||||
var jsDate = new Date(
|
||||
dateParts[0],
|
||||
dateParts[1] - 1,
|
||||
dateParts[2].substr(0, 2)
|
||||
);
|
||||
const date = jsDate.toISOString();
|
||||
if (!groups[date]) {
|
||||
groups[date] = [];
|
||||
}
|
||||
groups[date].push(update);
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
const groupedHistory = Object.keys(groups).map((date) => {
|
||||
return {
|
||||
date,
|
||||
novels: groups[date],
|
||||
};
|
||||
});
|
||||
|
||||
setHistory(groupedHistory);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Confirm Clear History Dialog
|
||||
*/
|
||||
@@ -29,9 +70,16 @@ const History = ({ navigation }) => {
|
||||
const showDialog = () => setVisible(true);
|
||||
const hideDialog = () => setVisible(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getHistoryAction());
|
||||
}, [getHistoryAction]);
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
getHistory();
|
||||
}, [visible])
|
||||
);
|
||||
|
||||
const deleteChapterHistory = (historyId) => {
|
||||
deleteHistory(historyId);
|
||||
getHistory();
|
||||
};
|
||||
|
||||
const renderHistoryCard = ({ item }) => (
|
||||
<HistoryItem
|
||||
@@ -39,6 +87,7 @@ const History = ({ navigation }) => {
|
||||
history={item}
|
||||
theme={theme}
|
||||
navigation={navigation}
|
||||
deleteHistory={deleteChapterHistory}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -70,7 +119,7 @@ const History = ({ navigation }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Searchbar
|
||||
placeholder="Search History"
|
||||
searchText={searchText}
|
||||
@@ -81,18 +130,17 @@ 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, index) => item.date}
|
||||
keyExtractor={(item) => item.date}
|
||||
renderItem={({ item }) => (
|
||||
<FlatList
|
||||
keyExtractor={(item, index) => item.novelId.toString()}
|
||||
keyExtractor={(item) => item.novelId.toString()}
|
||||
data={item.novels}
|
||||
renderItem={renderHistoryCard}
|
||||
ListHeaderComponent={
|
||||
@@ -114,23 +162,25 @@ const History = ({ navigation }) => {
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={theme.colorAccent}
|
||||
style={{ margin: 16 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
ListEmptyComponent={
|
||||
<EmptyView
|
||||
icon="(˘・_・˘)"
|
||||
description="Nothing read recently"
|
||||
/>
|
||||
!loading && (
|
||||
<EmptyView
|
||||
icon="(˘・_・˘)"
|
||||
description="Nothing read recently"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<RemoveHistoryDialog
|
||||
dialogVisible={visible}
|
||||
hideDialog={hideDialog}
|
||||
dispatch={dispatch}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,10 +5,15 @@ import moment from "moment";
|
||||
import { IconButton } from "react-native-paper";
|
||||
|
||||
import { parseChapterNumber } from "../../services/utils/helpers";
|
||||
import { deleteHistoryAction } from "../../redux/history/history.actions";
|
||||
import { setNovel } from "../../redux/novel/novel.actions";
|
||||
|
||||
const HistoryItem = ({ history, theme, dispatch, navigation }) => {
|
||||
const HistoryItem = ({
|
||||
history,
|
||||
theme,
|
||||
dispatch,
|
||||
navigation,
|
||||
deleteHistory,
|
||||
}) => {
|
||||
const {
|
||||
historyId,
|
||||
historyTimeRead,
|
||||
@@ -24,7 +29,7 @@ const HistoryItem = ({ history, theme, dispatch, navigation }) => {
|
||||
} = history;
|
||||
|
||||
const getChapterNumber = () =>
|
||||
`Ch. ${parseChapterNumber(chapterName)} - ${moment(historyTimeRead)
|
||||
`Chapter ${parseChapterNumber(chapterName)} • ${moment(historyTimeRead)
|
||||
.format("h:mm a")
|
||||
.toUpperCase()}`;
|
||||
|
||||
@@ -61,7 +66,12 @@ const HistoryItem = ({ history, theme, dispatch, navigation }) => {
|
||||
>
|
||||
<View>
|
||||
<Image
|
||||
source={{ uri: novelCover }}
|
||||
source={{
|
||||
uri:
|
||||
novelCover && !novelCover.startsWith("/")
|
||||
? novelCover
|
||||
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
|
||||
}}
|
||||
style={styles.historyItemCover}
|
||||
/>
|
||||
</View>
|
||||
@@ -97,7 +107,7 @@ const HistoryItem = ({ history, theme, dispatch, navigation }) => {
|
||||
icon="delete-outline"
|
||||
size={22}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={() => dispatch(deleteHistoryAction(historyId))}
|
||||
onPress={() => deleteHistory(historyId)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="play"
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
import { Button, Dialog, Portal } from "react-native-paper";
|
||||
import { clearAllHistoryAction } from "../../redux/history/history.actions";
|
||||
import { deleteAllHistory } from "../../database/queries/HistoryQueries";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
const RemoveHistoryDialog = ({
|
||||
dialogVisible,
|
||||
hideDialog,
|
||||
dispatch,
|
||||
theme,
|
||||
}) => {
|
||||
const RemoveHistoryDialog = ({ dialogVisible, hideDialog, theme }) => {
|
||||
const onPress = () => {
|
||||
dispatch(clearAllHistoryAction());
|
||||
deleteAllHistory();
|
||||
showToast("History deleted.");
|
||||
hideDialog();
|
||||
};
|
||||
|
||||
|
||||
+152
-31
@@ -5,9 +5,13 @@ import {
|
||||
View,
|
||||
RefreshControl,
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
} from "react-native";
|
||||
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import * as Haptics from "expo-haptics";
|
||||
|
||||
import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
import NovelList from "../../components/NovelList";
|
||||
@@ -21,8 +25,14 @@ import {
|
||||
import { updateLibraryAction } from "../../redux/updates/updates.actions";
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import { setNovel } from "../../redux/novel/novel.actions";
|
||||
import { Portal, TouchableRipple } from "react-native-paper";
|
||||
import { Portal } from "react-native-paper";
|
||||
import LibraryFilterSheet from "./components/LibraryFilterSheet";
|
||||
import { Actionbar } from "../../components/Actionbar/Actionbar";
|
||||
import {
|
||||
markAllChaptersRead,
|
||||
markAllChaptersUnread,
|
||||
} from "../../database/queries/ChapterQueries";
|
||||
import { unfollowNovel } from "../../database/queries/NovelQueries";
|
||||
|
||||
const LibraryScreen = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -32,6 +42,7 @@ const LibraryScreen = ({ navigation }) => {
|
||||
showNumberOfNovels,
|
||||
incognitoMode = false,
|
||||
updateLibraryOnLaunch = false,
|
||||
downloadedOnlyMode = false,
|
||||
} = settings;
|
||||
|
||||
const libraryFilterSheetRef = useRef(null);
|
||||
@@ -72,18 +83,37 @@ const LibraryScreen = ({ navigation }) => {
|
||||
dispatch(searchLibraryAction(text, sort, filter));
|
||||
};
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() => {
|
||||
navigation.navigate("Novel", item);
|
||||
dispatch(setNovel(item));
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
/**
|
||||
* 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}
|
||||
onPress={() => {
|
||||
navigation.navigate("Novel", item);
|
||||
dispatch(setNovel(item));
|
||||
}}
|
||||
theme={theme}
|
||||
onLongPress={selectNovel}
|
||||
isSelected={
|
||||
selectedNovels.indexOf(item.novelId) === -1 ? false : true
|
||||
}
|
||||
selectedNovels={selectedNovels}
|
||||
/>
|
||||
);
|
||||
|
||||
const refreshControl = () => (
|
||||
@@ -95,10 +125,6 @@ const LibraryScreen = ({ navigation }) => {
|
||||
/>
|
||||
);
|
||||
|
||||
// if (!loading) {
|
||||
// console.log(novels);
|
||||
// }
|
||||
|
||||
const listEmptyComponent = () =>
|
||||
searchText === "" && (
|
||||
<EmptyView
|
||||
@@ -116,20 +142,30 @@ const LibraryScreen = ({ navigation }) => {
|
||||
]}
|
||||
>
|
||||
<Searchbar
|
||||
placeholder={`Search Library ${
|
||||
showNumberOfNovels ? "(" + novels.length + ")" : ""
|
||||
}`}
|
||||
placeholder={
|
||||
selectedNovels.length === 0
|
||||
? `Search Library ${
|
||||
showNumberOfNovels
|
||||
? "(" + novels.length + ")"
|
||||
: ""
|
||||
}`
|
||||
: `${selectedNovels.length} selected`
|
||||
}
|
||||
searchText={searchText}
|
||||
clearSearchbar={clearSearchbar}
|
||||
onChangeText={onChangeText}
|
||||
backAction="magnify"
|
||||
backAction={selectedNovels.length > 0 ? "close" : "magnify"}
|
||||
onBackAction={() =>
|
||||
selectedNovels.length > 0 && setSelectedNovels([])
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
icon: "filter-variant",
|
||||
onPress: () => libraryFilterSheetRef.current.show(),
|
||||
color: filter
|
||||
? theme.filterColor
|
||||
: theme.textColorSecondary,
|
||||
color:
|
||||
filter || downloadedOnlyMode
|
||||
? theme.filterColor
|
||||
: theme.textColorPrimary,
|
||||
},
|
||||
]}
|
||||
theme={theme}
|
||||
@@ -137,12 +173,55 @@ const LibraryScreen = ({ navigation }) => {
|
||||
{incognitoMode && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#424242",
|
||||
paddingVertical: 4,
|
||||
backgroundColor: theme.searchBarColor,
|
||||
paddingVertical: 6,
|
||||
alignItems: "center",
|
||||
marginVertical: 8,
|
||||
elevation: 2,
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#FFFFFF" }}>Incognito Mode</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="incognito"
|
||||
color={theme.textColorSecondary}
|
||||
size={18}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
marginLeft: 8,
|
||||
}}
|
||||
>
|
||||
Incognito mode
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{loading ? (
|
||||
@@ -150,20 +229,19 @@ const LibraryScreen = ({ navigation }) => {
|
||||
) : (
|
||||
<>
|
||||
{searchText !== "" && (
|
||||
<TouchableRipple
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
navigation.navigate("GlobalSearch", {
|
||||
novelName: searchText,
|
||||
})
|
||||
}
|
||||
borderless
|
||||
rippleColor={theme.colorAccent}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
style={styles.emptySearch}
|
||||
>
|
||||
<Text style={{ color: theme.colorAccent }}>
|
||||
{`Search for "${searchText}" globally`}
|
||||
</Text>
|
||||
</TouchableRipple>
|
||||
</Pressable>
|
||||
)}
|
||||
<NovelList
|
||||
data={novels}
|
||||
@@ -171,6 +249,48 @@ const LibraryScreen = ({ navigation }) => {
|
||||
refreshControl={refreshControl()}
|
||||
ListEmptyComponent={listEmptyComponent}
|
||||
/>
|
||||
<Actionbar
|
||||
active={selectedNovels.length > 0}
|
||||
theme={theme}
|
||||
actions={[
|
||||
{
|
||||
icon: "check",
|
||||
onPress: () => {
|
||||
selectedNovels.map((id) =>
|
||||
markAllChaptersRead(id)
|
||||
),
|
||||
setSelectedNovels([]);
|
||||
dispatch(
|
||||
getLibraryAction(sort, filter)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "check-outline",
|
||||
onPress: () => {
|
||||
selectedNovels.map((id) =>
|
||||
markAllChaptersUnread(id)
|
||||
);
|
||||
setSelectedNovels([]);
|
||||
dispatch(
|
||||
getLibraryAction(sort, filter)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "delete-outline",
|
||||
onPress: () => {
|
||||
selectedNovels.map((id) =>
|
||||
unfollowNovel(id)
|
||||
);
|
||||
setSelectedNovels([]);
|
||||
dispatch(
|
||||
getLibraryAction(sort, filter)
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Portal>
|
||||
<LibraryFilterSheet
|
||||
bottomSheetRef={libraryFilterSheetRef}
|
||||
@@ -178,6 +298,7 @@ const LibraryScreen = ({ navigation }) => {
|
||||
dispatch={dispatch}
|
||||
sort={sort}
|
||||
filter={filter}
|
||||
downloadedOnlyMode={downloadedOnlyMode}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
|
||||
@@ -6,76 +6,58 @@ import {
|
||||
Animated,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
|
||||
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
|
||||
|
||||
import { Checkbox } from "react-native-paper";
|
||||
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
import { ListItem } from "../../../components/List";
|
||||
import { filterLibrary } from "../../../redux/library/library.actions";
|
||||
import { useSettings } from "../../../hooks/reduxHooks";
|
||||
import { setAppSettings } from "../../../redux/settings/settings.actions";
|
||||
import { Checkbox, SortItem } from "../../../components/Checkbox/Checkbox";
|
||||
|
||||
const LibraryFilterSheet = ({
|
||||
bottomSheetRef,
|
||||
dispatch,
|
||||
sort,
|
||||
filter,
|
||||
downloadedOnlyMode,
|
||||
theme,
|
||||
}) => {
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
|
||||
const FirstRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Checkbox.Item
|
||||
<View style={{ flex: 1, margin: 8 }}>
|
||||
<Checkbox
|
||||
label="Downloaded"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorHint}
|
||||
theme={theme}
|
||||
status={
|
||||
filter === "chaptersDownloaded > 0"
|
||||
? "checked"
|
||||
: "unchecked"
|
||||
filter === "chaptersDownloaded > 0" ||
|
||||
downloadedOnlyMode === true
|
||||
}
|
||||
onPress={() =>
|
||||
filter === "chaptersDownloaded > 0"
|
||||
? dispatch(filterLibrary(sort, ""))
|
||||
: dispatch(
|
||||
filterLibrary(sort, "chaptersDownloaded > 0")
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
label="Unread"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
color: theme.textColorPrimary,
|
||||
onPress={() => {
|
||||
if (!downloadedOnlyMode) {
|
||||
filter === "chaptersDownloaded > 0"
|
||||
? dispatch(filterLibrary(sort, ""))
|
||||
: dispatch(
|
||||
filterLibrary(sort, "chaptersDownloaded > 0")
|
||||
);
|
||||
}
|
||||
}}
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorHint}
|
||||
status={filter === "unread = 1" ? "checked" : "unchecked"}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Unread"
|
||||
theme={theme}
|
||||
status={filter === "unread = 1"}
|
||||
onPress={() =>
|
||||
filter === "unread = 1"
|
||||
? dispatch(filterLibrary(sort, ""))
|
||||
: dispatch(filterLibrary(sort, "unread = 1"))
|
||||
}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
label="Completed"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorHint}
|
||||
status={
|
||||
filter === "chaptersUnread IS NULL"
|
||||
? "checked"
|
||||
: "unchecked"
|
||||
}
|
||||
theme={theme}
|
||||
status={filter === "chaptersUnread IS NULL"}
|
||||
onPress={() =>
|
||||
filter === "chaptersUnread IS NULL"
|
||||
? dispatch(filterLibrary(sort, ""))
|
||||
@@ -87,117 +69,62 @@ const LibraryFilterSheet = ({
|
||||
</View>
|
||||
);
|
||||
|
||||
const sortOrders = [
|
||||
{
|
||||
label: "Date Added",
|
||||
ASC: "novels.novelId ASC",
|
||||
DESC: "novels.novelId DESC",
|
||||
},
|
||||
{
|
||||
label: "Aplhabetically",
|
||||
ASC: "novels.novelName ASC",
|
||||
DESC: "novels.novelName DESC",
|
||||
},
|
||||
{
|
||||
label: "Unread",
|
||||
ASC: "novels.unread ASC",
|
||||
DESC: "novels.unread DESC",
|
||||
},
|
||||
{
|
||||
label: "Downloaded",
|
||||
ASC: "chaptersDownloaded ASC",
|
||||
DESC: "chaptersDownloaded DESC",
|
||||
},
|
||||
{
|
||||
label: "Total Chapters",
|
||||
ASC: "chaptersUnread ASC",
|
||||
DESC: "chaptersUnread DESC",
|
||||
},
|
||||
];
|
||||
|
||||
const SecondRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ListItem
|
||||
style={{ paddingVertical: 10 }}
|
||||
title="Date Added"
|
||||
titleStyle={{ fontSize: 14 }}
|
||||
theme={theme}
|
||||
right={
|
||||
sort === "novels.novelId ASC"
|
||||
? "arrow-up"
|
||||
: sort === "novels.novelId DESC"
|
||||
? "arrow-down"
|
||||
: null
|
||||
}
|
||||
iconColor={theme.colorAccent}
|
||||
onPress={() =>
|
||||
sort === "novels.novelId ASC"
|
||||
? dispatch(filterLibrary("novels.novelId DESC", filter))
|
||||
: dispatch(filterLibrary("novels.novelId ASC", filter))
|
||||
}
|
||||
/>
|
||||
<ListItem
|
||||
style={{ paddingVertical: 10 }}
|
||||
title="Alphabetically"
|
||||
titleStyle={{ fontSize: 14 }}
|
||||
theme={theme}
|
||||
right={
|
||||
sort === "novels.novelName ASC"
|
||||
? "arrow-up"
|
||||
: sort === "novels.novelName DESC"
|
||||
? "arrow-down"
|
||||
: null
|
||||
}
|
||||
iconColor={theme.colorAccent}
|
||||
onPress={() =>
|
||||
sort === "novels.novelName ASC"
|
||||
? dispatch(
|
||||
filterLibrary("novels.novelName DESC", filter)
|
||||
)
|
||||
: dispatch(
|
||||
filterLibrary("novels.novelName ASC", filter)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ListItem
|
||||
style={{ paddingVertical: 10 }}
|
||||
title="Unread"
|
||||
titleStyle={{ fontSize: 14 }}
|
||||
theme={theme}
|
||||
right={
|
||||
sort === "novels.unread ASC"
|
||||
? "arrow-up"
|
||||
: sort === "novels.unread DESC"
|
||||
? "arrow-down"
|
||||
: null
|
||||
}
|
||||
iconColor={theme.colorAccent}
|
||||
onPress={() =>
|
||||
sort === "novels.unread ASC"
|
||||
? dispatch(filterLibrary("novels.unread DESC", filter))
|
||||
: dispatch(filterLibrary("novels.unread ASC", filter))
|
||||
}
|
||||
/>
|
||||
<ListItem
|
||||
style={{ paddingVertical: 10 }}
|
||||
title="Downloaded"
|
||||
titleStyle={{ fontSize: 14 }}
|
||||
theme={theme}
|
||||
right={
|
||||
sort === "chaptersDownloaded ASC"
|
||||
? "arrow-up"
|
||||
: sort === "chaptersDownloaded DESC"
|
||||
? "arrow-down"
|
||||
: null
|
||||
}
|
||||
iconColor={theme.colorAccent}
|
||||
onPress={() =>
|
||||
sort === "chaptersDownloaded ASC"
|
||||
? dispatch(
|
||||
filterLibrary("chaptersDownloaded DESC", filter)
|
||||
)
|
||||
: dispatch(
|
||||
filterLibrary("chaptersDownloaded ASC", filter)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ListItem
|
||||
style={{ paddingVertical: 10 }}
|
||||
title="Total Chapters"
|
||||
titleStyle={{ fontSize: 14 }}
|
||||
theme={theme}
|
||||
right={
|
||||
sort === "chaptersUnread ASC"
|
||||
? "arrow-up"
|
||||
: sort === "chaptersUnread DESC"
|
||||
? "arrow-down"
|
||||
: null
|
||||
}
|
||||
iconColor={theme.colorAccent}
|
||||
onPress={() =>
|
||||
sort === "chaptersUnread ASC"
|
||||
? dispatch(filterLibrary("chaptersUnread DESC", filter))
|
||||
: dispatch(filterLibrary("chaptersUnread ASC", filter))
|
||||
}
|
||||
/>
|
||||
<View style={{ flex: 1, margin: 8 }}>
|
||||
{sortOrders.map((item, index) => (
|
||||
<SortItem
|
||||
key={index}
|
||||
label={item.label}
|
||||
theme={theme}
|
||||
status={
|
||||
sort === item.ASC
|
||||
? "asc"
|
||||
: sort === item.DESC
|
||||
? "desc"
|
||||
: null
|
||||
}
|
||||
onPress={() =>
|
||||
sort === item.ASC
|
||||
? dispatch(filterLibrary(item.DESC, filter))
|
||||
: dispatch(filterLibrary(item.ASC, filter))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
|
||||
const displayModes = [
|
||||
{ displayMode: 0, label: "Compact Grid" },
|
||||
{ displayMode: 1, label: "Comfortable Grid" },
|
||||
{ displayMode: 3, label: "No Title Grid" },
|
||||
{ displayMode: 2, label: "List" },
|
||||
];
|
||||
|
||||
@@ -210,30 +137,25 @@ const LibraryFilterSheet = ({
|
||||
|
||||
const renderCheckboxes = () => {
|
||||
return displayModes.map((mode) => (
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
key={mode.displayMode}
|
||||
label={mode.label}
|
||||
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
|
||||
status={
|
||||
displayMode === mode.displayMode ? "checked" : "unchecked"
|
||||
}
|
||||
// mode="ios"
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccent}
|
||||
status={displayMode === mode.displayMode}
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings("displayMode", mode.displayMode))
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const ThirdRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ flex: 1, margin: 8 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
paddingVertical: 8,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
@@ -245,18 +167,15 @@ const LibraryFilterSheet = ({
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
paddingVertical: 8,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
Badges
|
||||
</Text>
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
label="Download Badges"
|
||||
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
|
||||
status={showDownloadBadges ? "checked" : "unchecked"}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccent}
|
||||
status={showDownloadBadges}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
@@ -265,25 +184,21 @@ const LibraryFilterSheet = ({
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
label="Unread Badges"
|
||||
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
|
||||
status={showUnreadBadges ? "checked" : "unchecked"}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccent}
|
||||
status={showUnreadBadges}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings("showUnreadBadges", !showUnreadBadges)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<Checkbox
|
||||
label="Show number of items"
|
||||
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
|
||||
status={showNumberOfNovels ? "checked" : "unchecked"}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccent}
|
||||
status={showNumberOfNovels}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
@@ -292,6 +207,7 @@ const LibraryFilterSheet = ({
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -317,7 +233,7 @@ const LibraryFilterSheet = ({
|
||||
indicatorStyle={{ backgroundColor: theme.colorAccent }}
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
renderLabel={({ route, focused, color }) => (
|
||||
<Text style={{ color, margin: 8 }}>{route.title}</Text>
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.textColorSecondary}
|
||||
activeColor={theme.colorAccent}
|
||||
@@ -329,8 +245,8 @@ const LibraryFilterSheet = ({
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 470, bottom: 0 }}
|
||||
snappingPoints={[0, 470]}
|
||||
draggableRange={{ top: 500, bottom: 0 }}
|
||||
snappingPoints={[0, 500]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
|
||||
+56
-60
@@ -1,71 +1,67 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import * as Linking from "expo-linking";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { Divider, ListItem, ListSection } from "../../components/List";
|
||||
|
||||
import { List } from "../../components/List";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { MoreHeader } from "./components/MoreHeader";
|
||||
|
||||
const AboutScreen = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar title="About" onBackAction={() => navigation.goBack()} />
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<ListSection>
|
||||
<ListItem
|
||||
title="Version"
|
||||
description="Stable 1.1.1"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Build Time"
|
||||
description="04-07-21 21:00 PM"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader/commits/main"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<Divider theme={theme} />
|
||||
<ListItem
|
||||
title="Discord"
|
||||
description="https://discord.gg/QdcWN4MD63"
|
||||
onPress={() =>
|
||||
Linking.openURL("https://discord.gg/QdcWN4MD63")
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Github"
|
||||
description="https://github.com/LNReader/lnreader"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Sources"
|
||||
description="https://github.com/LNReader/lnreader-sources"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader-sources"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</View>
|
||||
</>
|
||||
<ScreenContainer theme={theme}>
|
||||
<MoreHeader
|
||||
title="About"
|
||||
navigation={navigation}
|
||||
theme={theme}
|
||||
goBack={true}
|
||||
/>
|
||||
<List.Section>
|
||||
<List.Item
|
||||
title="Version"
|
||||
description="Stable 1.1.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.4"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title="Discord"
|
||||
description="https://discord.gg/QdcWN4MD63"
|
||||
onPress={() =>
|
||||
Linking.openURL("https://discord.gg/QdcWN4MD63")
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Github"
|
||||
description="https://github.com/LNReader/lnreader"
|
||||
onPress={() =>
|
||||
Linking.openURL("https://github.com/LNReader/lnreader")
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Sources"
|
||||
description="https://github.com/LNReader/lnreader-sources"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/LNReader/lnreader-sources"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import React, { useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { Button, Dialog, Portal } from "react-native-paper";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { ListItem, ListSection, ListSubHeader } from "../../components/List";
|
||||
import { List } from "../../components/List";
|
||||
|
||||
import { useTheme } from "../../hooks/reduxHooks";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
import { clearAllHistoryAction } from "../../redux/history/history.actions";
|
||||
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();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const showDialog = () => setVisible(true);
|
||||
const hideDialog = () => setVisible(false);
|
||||
|
||||
/**
|
||||
* Confirm Clear Database Dialog
|
||||
@@ -29,59 +25,25 @@ const AdvancedSettings = ({ navigation }) => {
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar title="Advanced" onBackAction={() => navigation.goBack()} />
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Data</ListSubHeader>
|
||||
<ListItem
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Data</List.SubHeader>
|
||||
<List.Item
|
||||
title="Clear database"
|
||||
description="Delete history for novels not in your library"
|
||||
onPress={showClearDatabaseDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Clear history"
|
||||
description="Delete reading history for all novels"
|
||||
onPress={showDialog}
|
||||
<List.Item
|
||||
title="Clear updates tab"
|
||||
description="Clears chapter entries in updates tab"
|
||||
onPress={() => {
|
||||
clearUpdates();
|
||||
showToast("Updates cleared.");
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</List.Section>
|
||||
<Portal>
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onDismiss={hideDialog}
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
}}
|
||||
>
|
||||
<Dialog.Title
|
||||
style={{
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
Are you sure? All history will be lost.
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<Button
|
||||
uppercase={false}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={hideDialog}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
uppercase={false}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={() => {
|
||||
dispatch(clearAllHistoryAction());
|
||||
hideDialog();
|
||||
}}
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
visible={clearDatabaseDialog}
|
||||
onDismiss={hideClearDatabaseDialog}
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
import React, { useState } from "react";
|
||||
import { ScrollView, Text } from "react-native";
|
||||
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { List } from "../../components/List";
|
||||
import { ThemePicker } from "../../components/ThemePicker/ThemePicker";
|
||||
import SwitchSetting from "../../components/Switch/Switch";
|
||||
import ColorPickerModal from "../../components/ColorPickerModal";
|
||||
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import {
|
||||
setAccentColor,
|
||||
setAmoledMode,
|
||||
setAppSettings,
|
||||
setAppTheme,
|
||||
setRippleColor,
|
||||
} from "../../redux/settings/settings.actions";
|
||||
|
||||
import {
|
||||
darkTheme,
|
||||
greenAppleTheme,
|
||||
irisBlueTheme,
|
||||
lightTheme,
|
||||
midnightDuskTheme,
|
||||
oceanicTheme,
|
||||
springBlossomTheme,
|
||||
strawberryDaiquiri,
|
||||
takoLightTheme,
|
||||
takoTheme,
|
||||
yangTheme,
|
||||
yinYangTheme,
|
||||
yotsubaTheme,
|
||||
} from "../../theme/theme";
|
||||
|
||||
const lightThemes = [
|
||||
lightTheme,
|
||||
springBlossomTheme,
|
||||
takoLightTheme,
|
||||
yangTheme,
|
||||
yotsubaTheme,
|
||||
];
|
||||
const darkThemes = [
|
||||
darkTheme,
|
||||
midnightDuskTheme,
|
||||
strawberryDaiquiri,
|
||||
takoTheme,
|
||||
greenAppleTheme,
|
||||
yinYangTheme,
|
||||
irisBlueTheme,
|
||||
oceanicTheme,
|
||||
];
|
||||
|
||||
const AppearanceSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
showHistoryTab = true,
|
||||
showUpdatesTab = true,
|
||||
showLabelsInNav = false,
|
||||
hideBackdrop = false,
|
||||
useFabForContinueReading = false,
|
||||
} = useSettings();
|
||||
|
||||
/**
|
||||
* Accent Color Modal
|
||||
*/
|
||||
const [accentColorModal, setAccentColorModal] = useState(false);
|
||||
const showAccentColorModal = () => setAccentColorModal(true);
|
||||
const hideAccentColorModal = () => setAccentColorModal(false);
|
||||
|
||||
const hexToRgb = (hex) => {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
const onSubmit = (val) => {
|
||||
dispatch(setAccentColor(val));
|
||||
const rgb = hexToRgb(val);
|
||||
const rgbaColor = `rgba(${rgb.r},${rgb.g},${rgb.b},0.12)`;
|
||||
dispatch(setRippleColor(rgbaColor));
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar title="Appearance" onBackAction={navigation.goBack} />
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>App theme</List.SubHeader>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
Light Theme
|
||||
</Text>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
flexDirection: "row",
|
||||
}}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
{lightThemes.map((item) => (
|
||||
<ThemePicker
|
||||
key={item.id}
|
||||
currentTheme={theme}
|
||||
theme={item}
|
||||
onPress={() => dispatch(setAppTheme(item.id))}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
Dark Theme
|
||||
</Text>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
flexDirection: "row",
|
||||
}}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
{darkThemes.map((item) => (
|
||||
<ThemePicker
|
||||
key={item.id}
|
||||
currentTheme={theme}
|
||||
theme={item}
|
||||
onPress={() => dispatch(setAppTheme(item.id))}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
{theme.statusBar === "light-content" && (
|
||||
<SwitchSetting
|
||||
label="Pure black dark mode"
|
||||
value={
|
||||
theme.colorPrimary === "#000000" &&
|
||||
theme.colorPrimaryDark === "#000000"
|
||||
}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAmoledMode(
|
||||
theme.id,
|
||||
!(
|
||||
theme.colorPrimary === "#000000" &&
|
||||
theme.colorPrimaryDark === "#000000"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
<List.ColorItem
|
||||
title="Accent Color"
|
||||
description={theme.colorAccent.toUpperCase()}
|
||||
onPress={showAccentColorModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Novel info</List.SubHeader>
|
||||
<SwitchSetting
|
||||
label="Hide backdrop"
|
||||
value={hideBackdrop}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings("hideBackdrop", !hideBackdrop)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label="Use FAB instead of button"
|
||||
value={useFabForContinueReading}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"useFabForContinueReading",
|
||||
!useFabForContinueReading
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Navbar</List.SubHeader>
|
||||
<SwitchSetting
|
||||
label="Show updates in the nav"
|
||||
value={showUpdatesTab}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showUpdatesTab",
|
||||
!showUpdatesTab
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label="Show history in the nav"
|
||||
value={showHistoryTab}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showHistoryTab",
|
||||
!showHistoryTab
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label="Always show nav labels"
|
||||
value={showLabelsInNav}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showLabelsInNav",
|
||||
!showLabelsInNav
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScrollView>
|
||||
|
||||
<ColorPickerModal
|
||||
title="Accent color"
|
||||
modalVisible={accentColorModal}
|
||||
hideModal={hideAccentColorModal}
|
||||
color={theme.colorAccent}
|
||||
onSubmit={onSubmit}
|
||||
theme={theme}
|
||||
showAccentColors={true}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppearanceSettings;
|
||||
@@ -2,17 +2,15 @@ import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import {
|
||||
InfoItem,
|
||||
ListItem,
|
||||
ListSection,
|
||||
ListSubHeader,
|
||||
} from "../../components/List";
|
||||
import { List } from "../../components/List";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
|
||||
import { useTheme } from "../../hooks/reduxHooks";
|
||||
import { restoreLibraryAction } from "../../redux/library/library.actions";
|
||||
import { createBackup } from "../../services/backup";
|
||||
import {
|
||||
createFullBackup,
|
||||
restoreFullBackup,
|
||||
} from "../../services/backup/backup";
|
||||
|
||||
const BackupSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -25,27 +23,34 @@ const BackupSettings = ({ navigation }) => {
|
||||
title="Backup"
|
||||
onBackAction={() => navigation.goBack()}
|
||||
/>
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Backup</ListSubHeader>
|
||||
<ListItem
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Backup</List.SubHeader>
|
||||
<List.Item
|
||||
title="Create backup"
|
||||
description="Can be used to restore current library"
|
||||
onPress={createBackup}
|
||||
onPress={createFullBackup}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="Restore backup"
|
||||
description="Restore library from backup file"
|
||||
onPress={() => dispatch(restoreLibraryAction())}
|
||||
onPress={restoreFullBackup}
|
||||
theme={theme}
|
||||
/>
|
||||
<InfoItem
|
||||
{/* <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"
|
||||
description="Restore library from backup file"
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</List.Section>
|
||||
</ScreenContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -55,7 +55,6 @@ const DownloadQueue = ({ navigation }) => {
|
||||
if (downloadQueue.length > 0) {
|
||||
dispatch(cancelDownload());
|
||||
showToast("Downloads cancelled.");
|
||||
BackgroundService.stop();
|
||||
}
|
||||
closeMenu();
|
||||
}}
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import { ScrollView } from "react-native";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import {
|
||||
Divider,
|
||||
ListItem,
|
||||
ListSection,
|
||||
ListSubHeader,
|
||||
} from "../../components/List";
|
||||
import { List } from "../../components/List";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
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 { 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();
|
||||
@@ -31,14 +22,22 @@ const GenralSettings = ({ navigation }) => {
|
||||
(state) => state.settingsReducer
|
||||
);
|
||||
|
||||
const { updateLibraryOnLaunch = false, downloadNewChapters = false } =
|
||||
useSettings();
|
||||
const {
|
||||
updateLibraryOnLaunch = false,
|
||||
downloadNewChapters = false,
|
||||
onlyUpdateOngoingNovels = false,
|
||||
} = useSettings();
|
||||
|
||||
const { showLastUpdateTime = true } = useSelector(
|
||||
(state) => state.updatesReducer
|
||||
);
|
||||
|
||||
const displayModeLabel = (displayMode) => {
|
||||
const label = {
|
||||
0: "Compact Grid",
|
||||
1: "Comfortable Grid",
|
||||
2: "List",
|
||||
3: "No Title Grid",
|
||||
};
|
||||
|
||||
return label[displayMode] ?? "Compact Grid";
|
||||
@@ -51,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
|
||||
*/
|
||||
@@ -65,114 +57,83 @@ 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} />
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Display</ListSubHeader>
|
||||
<ListItem
|
||||
title="Display Mode"
|
||||
description={displayModeLabel(displayMode)}
|
||||
onPress={showDisplayModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Items per row in library"
|
||||
description={`${novelsPerRow} items per row`}
|
||||
onPress={showGridSizeModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<Divider theme={theme} />
|
||||
<ListSubHeader theme={theme}>Library</ListSubHeader>
|
||||
<SwitchSetting
|
||||
label="Update library on launch"
|
||||
value={updateLibraryOnLaunch}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"updateLibraryOnLaunch",
|
||||
!updateLibraryOnLaunch
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListSubHeader theme={theme}>Auto-download</ListSubHeader>
|
||||
<SwitchSetting
|
||||
label="Download new chapters"
|
||||
value={downloadNewChapters}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"downloadNewChapters",
|
||||
!downloadNewChapters
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListSubHeader theme={theme}>Theme</ListSubHeader>
|
||||
<ListItem
|
||||
title="Theme"
|
||||
description={theme.name}
|
||||
onPress={showthemeModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Accent Color"
|
||||
description={theme.colorAccent.toUpperCase()}
|
||||
onPress={showAccentColorModal}
|
||||
theme={theme}
|
||||
iconColor={theme.colorAccent}
|
||||
right="circle"
|
||||
/>
|
||||
{theme.statusBar === "light-content" && (
|
||||
<ScrollView contentContainerStyle={{ paddingBottom: 32 }}>
|
||||
<List.Section>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Display</List.SubHeader>
|
||||
<List.Item
|
||||
title="Display Mode"
|
||||
description={displayModeLabel(displayMode)}
|
||||
onPress={showDisplayModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Items per row in library"
|
||||
description={`${novelsPerRow} items per row`}
|
||||
onPress={showGridSizeModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Library</List.SubHeader>
|
||||
<SwitchSetting
|
||||
label="Pure black dark mode"
|
||||
value={
|
||||
theme.colorPrimary === "#000000" &&
|
||||
theme.colorPrimaryDark === "#000000"
|
||||
}
|
||||
label="Update library on launch"
|
||||
value={updateLibraryOnLaunch}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAmoledMode(
|
||||
theme.id,
|
||||
!(
|
||||
theme.colorPrimary === "#000000" &&
|
||||
theme.colorPrimaryDark === "#000000"
|
||||
)
|
||||
setAppSettings(
|
||||
"updateLibraryOnLaunch",
|
||||
!updateLibraryOnLaunch
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</ListSection>
|
||||
<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"
|
||||
value={downloadNewChapters}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"downloadNewChapters",
|
||||
!downloadNewChapters
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScrollView>
|
||||
<DisplayModeModal
|
||||
displayMode={displayMode}
|
||||
displayModalVisible={displayModalVisible}
|
||||
@@ -187,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>
|
||||
);
|
||||
};
|
||||
|
||||
+125
-114
@@ -1,138 +1,149 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Image, Pressable, Text } from "react-native";
|
||||
import { List, Switch } from "react-native-paper";
|
||||
import { Switch } from "react-native-paper";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { Divider, ListItem, ListSection } from "../../components/List";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { List } from "../../components/List";
|
||||
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import { setAppSettings } from "../../redux/settings/settings.actions";
|
||||
import { MoreHeader } from "./components/MoreHeader";
|
||||
|
||||
const MoreScreen = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
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 (
|
||||
<>
|
||||
<Appbar title="More" style={{ elevation: 0 }} />
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
|
||||
<View
|
||||
style={{
|
||||
paddingTop: 10,
|
||||
paddingBottom: 25,
|
||||
alignItems: "center",
|
||||
backgroundColor: theme.colorPrimary,
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={require("../../../assets/logo.png")}
|
||||
style={{
|
||||
height: 80,
|
||||
width: 80,
|
||||
tintColor: theme.textColorPrimary,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<ListSection>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={enableIncognitoMode}
|
||||
>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<List.Icon
|
||||
color={theme.colorAccent}
|
||||
icon="incognito"
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
Incognito Mode
|
||||
</Text>
|
||||
<Text
|
||||
style={{ color: theme.textColorSecondary }}
|
||||
>
|
||||
Pauses reading history
|
||||
</Text>
|
||||
</View>
|
||||
<ScreenContainer theme={theme}>
|
||||
<MoreHeader title="More" navigation={navigation} theme={theme} />
|
||||
<List.Section>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 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>
|
||||
<Switch
|
||||
value={incognitoMode}
|
||||
onValueChange={enableIncognitoMode}
|
||||
color={theme.colorAccent}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
</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",
|
||||
}}
|
||||
onPress={enableIncognitoMode}
|
||||
>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<List.Icon theme={theme} icon="incognito" />
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
Incognito mode
|
||||
</Text>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Pauses reading history
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={incognitoMode}
|
||||
onValueChange={enableIncognitoMode}
|
||||
color={theme.colorAccent}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title="Download queue"
|
||||
description={
|
||||
downloadQueue.length > 0 &&
|
||||
downloadQueue.length + " remaining"
|
||||
}
|
||||
icon="progress-download"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "DownloadQueue",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Downloads"
|
||||
icon="folder-download"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "Downloads",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title="Settings"
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "SettingsStack",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
title="Download queue"
|
||||
description={
|
||||
downloadQueue.length > 0 &&
|
||||
downloadQueue.length + " remaining"
|
||||
}
|
||||
icon="progress-download"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "DownloadQueue",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Downloads"
|
||||
icon="folder-download"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "Downloads",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
title="Settings"
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "SettingsStack",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
title="About"
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "About",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</View>
|
||||
</>
|
||||
<List.Item
|
||||
title="About"
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "About",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+289
-147
@@ -1,33 +1,64 @@
|
||||
import React, { useState } from "react";
|
||||
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
import { ToggleButton } from "react-native-paper";
|
||||
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";
|
||||
import { Row, ScreenContainer } from "../../components/Common";
|
||||
import { ListItem, ListSection, ListSubHeader } from "../../components/List";
|
||||
import {
|
||||
ToggleButton,
|
||||
ToggleColorButton,
|
||||
} from "../../components/Common/ToggleButton";
|
||||
import { List } from "../../components/List";
|
||||
|
||||
import { useReaderSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import { setReaderSettings } from "../../redux/settings/settings.actions";
|
||||
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 textColor = {
|
||||
1: "rgba(255,255,255,0.7)",
|
||||
2: "#000000",
|
||||
3: "#000000",
|
||||
4: "#CCCCCC",
|
||||
};
|
||||
const { autoScrollInterval = 10 } = useSettings();
|
||||
|
||||
/**
|
||||
* Reader Background Color Modal
|
||||
@@ -54,81 +85,14 @@ 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 onValueChange = (value) => {
|
||||
dispatch(setReaderSettings("theme", value ?? reader.theme));
|
||||
dispatch(
|
||||
setReaderSettings("textColor", textColor[value] ?? reader.textColor)
|
||||
);
|
||||
};
|
||||
|
||||
const textAlignments = [
|
||||
{ value: "left", icon: "format-align-left" },
|
||||
{ value: "justify", icon: "format-align-justify" },
|
||||
{ value: "right", icon: "format-align-right" },
|
||||
];
|
||||
|
||||
const onValueChangeTextAlign = (value) => {
|
||||
dispatch(setReaderSettings("textAlign", value ?? reader.textAlign));
|
||||
};
|
||||
const [customHtmlCSS, setcustomHtmlCSS] = useState(reader.customCSS);
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar title="Reader" onBackAction={navigation.goBack} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView>
|
||||
<View
|
||||
style={{ backgroundColor: readerBackground(reader.theme) }}
|
||||
>
|
||||
<Text style={readerStyles}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Ut lobortis nunc id nisl tempor, sed tristique sem
|
||||
sagittis. Mauris consequat orci egestas volutpat
|
||||
vestibulum. Nullam convallis, leo in porttitor
|
||||
fringilla, ex mi finibus felis, non molestie ligula
|
||||
ligula facilisis ligula. Nam eu est a turpis porttitor
|
||||
aliquet sed vel risus. Ut ac scelerisque diam. Nulla
|
||||
fringilla enim non ex rhoncus faucibus. Integer eget
|
||||
lacus justo. Lorem ipsum dolor sit amet, consectetur
|
||||
adipiscing elit. Ut vitae tempor metus. Morbi nec nisl
|
||||
cursus, gravida ex vitae, laoreet leo. Pellentesque in
|
||||
lectus ante. Aliquam porta suscipit pretium. Sed ut
|
||||
ligula at dolor pretium egestas quis vitae urna. In
|
||||
ullamcorper venenatis orci, eu pulvinar nibh venenatis
|
||||
sit amet.
|
||||
</Text>
|
||||
</View>
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Preset</ListSubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressableListItem}
|
||||
>
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Preset</List.SubHeader>
|
||||
<View style={styles.pressableListItem}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
@@ -137,44 +101,51 @@ const ReaderSettings = ({ navigation }) => {
|
||||
>
|
||||
Preset Themes
|
||||
</Text>
|
||||
<Row>
|
||||
<ToggleButton.Row
|
||||
onValueChange={onValueChange}
|
||||
value={reader.theme}
|
||||
>
|
||||
<ScrollView
|
||||
style={{ marginLeft: 16 }}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
<Row>
|
||||
{presetThemes.map((theme) => (
|
||||
<ToggleButton
|
||||
<ToggleColorButton
|
||||
key={theme.value}
|
||||
icon={
|
||||
reader.theme === theme.value &&
|
||||
"check"
|
||||
selected={
|
||||
reader.theme ===
|
||||
theme.backgroundColor
|
||||
}
|
||||
color={theme.textColor}
|
||||
value={theme.value}
|
||||
style={[
|
||||
{
|
||||
backgroundColor:
|
||||
theme.backgroundColor,
|
||||
},
|
||||
styles.presetToggleButton,
|
||||
]}
|
||||
backgroundColor={theme.backgroundColor}
|
||||
textColor={theme.textColor}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"theme",
|
||||
theme.backgroundColor
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textColor",
|
||||
theme.textColor
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ToggleButton.Row>
|
||||
</Row>
|
||||
</Pressable>
|
||||
<ListSubHeader theme={theme}>Reader</ListSubHeader>
|
||||
<ListItem
|
||||
</Row>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Reader</List.SubHeader>
|
||||
<List.ColorItem
|
||||
title="Background Color"
|
||||
description={readerBackground(
|
||||
reader.theme
|
||||
).toUpperCase()}
|
||||
onPress={showReaderBackgroundtColorModal}
|
||||
theme={theme}
|
||||
iconColor={readerBackground(reader.theme)}
|
||||
right="circle"
|
||||
/>
|
||||
<ListItem
|
||||
<List.ColorItem
|
||||
title="Text Color"
|
||||
description={
|
||||
reader.textColor.toUpperCase() ||
|
||||
@@ -182,16 +153,10 @@ const ReaderSettings = ({ navigation }) => {
|
||||
}
|
||||
onPress={showReaderTextColorModal}
|
||||
theme={theme}
|
||||
iconColor={
|
||||
reader.textColor || readerTextColor(reader.theme)
|
||||
}
|
||||
right="circle"
|
||||
/>
|
||||
<ListSubHeader theme={theme}>Text</ListSubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressableListItem}
|
||||
>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Text</List.SubHeader>
|
||||
<Pressable style={styles.pressableListItem}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
@@ -211,32 +176,92 @@ const ReaderSettings = ({ navigation }) => {
|
||||
</Text>
|
||||
</View>
|
||||
<Row>
|
||||
<ToggleButton.Row
|
||||
onValueChange={onValueChangeTextAlign}
|
||||
value={reader.textAlign}
|
||||
>
|
||||
{textAlignments.map((item) => (
|
||||
<ToggleButton
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
color={
|
||||
reader.textAlign === item.value
|
||||
? theme.colorAccent
|
||||
: "#FFFFFF"
|
||||
}
|
||||
value={item.value}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ToggleButton.Row>
|
||||
{textAlignments.map((item) => (
|
||||
<ToggleButton
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"textAlign",
|
||||
item.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
selected={item.value === reader.textAlign}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
</Pressable>
|
||||
</ListSection>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>
|
||||
Custom CSS (Only works in webview mode)
|
||||
</List.SubHeader>
|
||||
<View style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
|
||||
<TextInput
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
value={customHtmlCSS}
|
||||
onChangeText={(text) => setcustomHtmlCSS(text)}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
placeholder="Example: body { color: red; }"
|
||||
multiline={true}
|
||||
/>
|
||||
<View style={{ flexDirection: "row-reverse" }}>
|
||||
<Button
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings(
|
||||
"customCSS",
|
||||
customHtmlCSS
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={() => {
|
||||
setcustomHtmlCSS("");
|
||||
dispatch(
|
||||
setReaderSettings("customCSS", "")
|
||||
);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<List.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"
|
||||
@@ -254,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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { ListItem } from "../../components/List";
|
||||
import { List } from "../../components/List";
|
||||
|
||||
import { useTheme } from "../../hooks/reduxHooks";
|
||||
|
||||
@@ -18,7 +18,7 @@ const SettingsScreen = ({ navigation }) => {
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="General"
|
||||
icon="tune"
|
||||
onPress={() =>
|
||||
@@ -28,7 +28,7 @@ const SettingsScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="Reader"
|
||||
icon="book-open-outline"
|
||||
onPress={() =>
|
||||
@@ -38,7 +38,7 @@ const SettingsScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="Tracking"
|
||||
icon="sync"
|
||||
onPress={() =>
|
||||
@@ -48,7 +48,7 @@ const SettingsScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="Backup"
|
||||
icon="cloud-upload-outline"
|
||||
onPress={() =>
|
||||
@@ -58,7 +58,17 @@ const SettingsScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
<List.Item
|
||||
title="Appearance"
|
||||
icon="palette-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "AppearanceSettings",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Advanced"
|
||||
icon="code-tags"
|
||||
onPress={() =>
|
||||
|
||||
@@ -11,12 +11,7 @@ import {
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import {
|
||||
Divider,
|
||||
ListItem,
|
||||
ListSection,
|
||||
ListSubHeader,
|
||||
} from "../../components/List";
|
||||
import { List } from "../../components/List";
|
||||
import { removeTracker, setTracker } from "../../redux/tracker/tracker.actions";
|
||||
|
||||
const TrackerScreen = ({ navigation }) => {
|
||||
@@ -39,9 +34,9 @@ const TrackerScreen = ({ navigation }) => {
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Services</ListSubHeader>
|
||||
<ListItem
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Services</List.SubHeader>
|
||||
<List.Item
|
||||
title="MyAnimeList"
|
||||
onPress={async () => {
|
||||
if (tracker) {
|
||||
@@ -82,10 +77,10 @@ const TrackerScreen = ({ navigation }) => {
|
||||
{tracker && tracker.expires_in < new Date(Date.now()) && (
|
||||
<>
|
||||
<Divider theme={theme} />
|
||||
<ListSubHeader theme={theme}>
|
||||
<List.SubHeader theme={theme}>
|
||||
Settings
|
||||
</ListSubHeader>
|
||||
<ListItem
|
||||
</List.SubHeader>
|
||||
<List.Item
|
||||
title="Revalidate MyAnimeList"
|
||||
onPress={() => {
|
||||
const res = malTokenWatcher(tracker);
|
||||
@@ -95,7 +90,7 @@ const TrackerScreen = ({ navigation }) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ListSection>
|
||||
</List.Section>
|
||||
|
||||
<Portal>
|
||||
<Modal
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Text, Pressable } from "react-native";
|
||||
import { Portal, Modal, Checkbox } from "react-native-paper";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import { Portal, Modal } from "react-native-paper";
|
||||
|
||||
import { Checkbox } from "../../../components/Checkbox/Checkbox";
|
||||
|
||||
import { setAppSettings } from "../../../redux/settings/settings.actions";
|
||||
|
||||
const DisplayModeModal = ({
|
||||
@@ -13,44 +17,21 @@ const DisplayModeModal = ({
|
||||
const displayModes = [
|
||||
{ displayMode: 0, label: "Compact Grid" },
|
||||
{ displayMode: 1, label: "Comfortable Grid" },
|
||||
{ displayMode: 3, label: "No Title Grid" },
|
||||
{ displayMode: 2, label: "List" },
|
||||
];
|
||||
|
||||
const renderCheckboxes = () => {
|
||||
return displayModes.map((mode) => (
|
||||
<View
|
||||
<Checkbox
|
||||
key={mode.displayMode}
|
||||
style={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginVertical: 4,
|
||||
},
|
||||
displayMode === mode.displayMode && {
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings("displayMode", mode.displayMode)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
{mode.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
status={displayMode === mode.displayMode}
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings("displayMode", mode.displayMode))
|
||||
}
|
||||
label={mode.label}
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Portal, Modal } from "react-native-paper";
|
||||
import Slider from "@react-native-community/slider";
|
||||
import { RadioButton, RadioButtonGroup } from "../../../components/RadioButton";
|
||||
import { setNovelsPerRow } from "../../../redux/settings/settings.actions";
|
||||
import { Checkbox } from "../../../components/Checkbox/Checkbox";
|
||||
|
||||
const GridSizeModal = ({
|
||||
dispatch,
|
||||
@@ -46,19 +47,15 @@ const GridSizeModal = ({
|
||||
>
|
||||
{`${novelsPerRow} per row`}
|
||||
</Text>
|
||||
<RadioButtonGroup
|
||||
onValueChange={(value) => dispatch(setNovelsPerRow(value))}
|
||||
value={novelsPerRow}
|
||||
>
|
||||
{Object.keys(gridSizes).map((item) => (
|
||||
<RadioButton
|
||||
key={item}
|
||||
value={item}
|
||||
label={gridSizes[item]}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</RadioButtonGroup>
|
||||
{Object.keys(gridSizes).map((item) => (
|
||||
<Checkbox
|
||||
key={item}
|
||||
status={item === novelsPerRow}
|
||||
label={gridSizes[item]}
|
||||
onPress={() => dispatch(setNovelsPerRow(item))}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { Image, StyleSheet, View } from "react-native";
|
||||
import { Appbar } from "../../../components/Appbar";
|
||||
|
||||
export const MoreHeader = ({ title, navigation, theme, goBack }) => (
|
||||
<>
|
||||
<Appbar
|
||||
title={title}
|
||||
onBackAction={goBack && navigation.goBack}
|
||||
style={{ elevation: 0 }}
|
||||
/>
|
||||
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
|
||||
<View
|
||||
style={[
|
||||
styles.logoContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={require("../../../../assets/logo.png")}
|
||||
style={[styles.logo, { tintColor: theme.textColorPrimary }]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
logoContainer: {
|
||||
paddingTop: 8,
|
||||
paddingBottom: 24,
|
||||
alignItems: "center",
|
||||
elevation: 4,
|
||||
},
|
||||
logo: {
|
||||
height: 80,
|
||||
width: 80,
|
||||
},
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, View, Text } from "react-native";
|
||||
|
||||
import { Portal, Modal } from "react-native-paper";
|
||||
|
||||
import { setAppTheme } from "../../../redux/settings/settings.actions";
|
||||
|
||||
const ThemeModal = ({ themeModalVisible, hidethemeModal, dispatch, theme }) => {
|
||||
const themes = [
|
||||
{ id: 1, name: "Light" },
|
||||
{ id: 2, name: "Dark" },
|
||||
{ id: 3, name: "Midnight Dusk" },
|
||||
{ id: 4, name: "Green Apple" },
|
||||
{ id: 5, name: "Iris Blue" },
|
||||
{ id: 7, name: "Strawberry Daiquiri" },
|
||||
];
|
||||
|
||||
const ThemeItem = ({ item }) => (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginVertical: 4,
|
||||
},
|
||||
theme.id === item.id && {
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() => dispatch(setAppTheme(item.id))}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderThemeCheckboxes = () =>
|
||||
themes.map((item) => <ThemeItem item={item} key={item.id} />);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
+249
-48
@@ -7,10 +7,19 @@ import {
|
||||
StatusBar,
|
||||
Dimensions,
|
||||
Share,
|
||||
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,
|
||||
@@ -22,6 +31,7 @@ import {
|
||||
markChaptersRead,
|
||||
markChapterUnreadAction,
|
||||
markPreviousChaptersReadAction,
|
||||
setNovel,
|
||||
sortAndFilterChapters,
|
||||
updateNovelAction,
|
||||
} from "../../redux/novel/novel.actions";
|
||||
@@ -29,17 +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 { pickCustomNovelCover } from "../../database/queries/NovelQueries";
|
||||
import FadeView from "../../components/Common/CrossFadeView";
|
||||
|
||||
const Novel = ({ route, navigation }) => {
|
||||
const item = route.params;
|
||||
@@ -52,6 +66,7 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [downloadMenu, showDownloadMenu] = useState(false);
|
||||
const [extraMenu, showExtraMenu] = useState(false);
|
||||
|
||||
let flatlistRef = useRef(null);
|
||||
let chaptersSettingsSheetRef = useRef(null);
|
||||
@@ -68,6 +83,9 @@ const Novel = ({ route, navigation }) => {
|
||||
novel.novelId
|
||||
);
|
||||
|
||||
const { useFabForContinueReading = lastReadChapter && false } =
|
||||
useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter)
|
||||
@@ -127,23 +145,93 @@ 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
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToChapter = (chapter) =>
|
||||
navigation.navigate("Chapter", {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelName,
|
||||
...chapter,
|
||||
});
|
||||
|
||||
const showProgressPercentage = (chapter) => {
|
||||
const savedProgress =
|
||||
position &&
|
||||
position[chapter.chapterId] &&
|
||||
position[chapter.chapterId].percentage;
|
||||
if (savedProgress < 100 && savedProgress > 0 && !chapter.read) {
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorHint,
|
||||
fontSize: 12,
|
||||
marginLeft: chapter.releaseDate ? 5 : 0,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{chapter.releaseDate && "• "}
|
||||
{"Progress " + position[chapter.chapterId].percentage + "%"}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const setCustomNovelCover = async () => {
|
||||
const cover = await pickCustomNovelCover(novelId);
|
||||
|
||||
if (cover) {
|
||||
dispatch(
|
||||
setNovel({
|
||||
...novel,
|
||||
novelCover: cover,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
showExtraMenu(false);
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }) => (
|
||||
<ChapterItem
|
||||
theme={theme}
|
||||
chapter={item}
|
||||
index={index}
|
||||
novelData={{ sourceId, novelUrl, novelName }}
|
||||
navigation={navigation}
|
||||
position={position}
|
||||
showChapterTitles={showChapterTitles}
|
||||
downloadQueue={downloadQueue}
|
||||
deleteChapter={deleteChapter}
|
||||
@@ -151,9 +239,13 @@ const Novel = ({ route, navigation }) => {
|
||||
isSelected={isSelected}
|
||||
onSelectPress={onSelectPress}
|
||||
onSelectLongPress={onSelectLongPress}
|
||||
navigateToChapter={navigateToChapter}
|
||||
showProgressPercentage={showProgressPercentage}
|
||||
/>
|
||||
);
|
||||
|
||||
const [editInfoModal, showEditInfoModal] = useState(false);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<View
|
||||
@@ -162,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}
|
||||
@@ -196,7 +291,8 @@ const Novel = ({ route, navigation }) => {
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
</FadeView>
|
||||
{selected.length == 0 && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -209,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:
|
||||
@@ -235,7 +354,7 @@ const Novel = ({ route, navigation }) => {
|
||||
}}
|
||||
>
|
||||
<Menu.Item
|
||||
title="Download all"
|
||||
title="All"
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
@@ -250,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 }}
|
||||
@@ -263,29 +401,47 @@ 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,
|
||||
marginRight: 16,
|
||||
}}
|
||||
onPress={() =>
|
||||
Share.share({
|
||||
message: novel.sourceUrl,
|
||||
})
|
||||
|
||||
<Menu
|
||||
visible={extraMenu}
|
||||
onDismiss={() => showExtraMenu(false)}
|
||||
anchor={
|
||||
<IconButton
|
||||
icon="dots-vertical"
|
||||
color={theme.textColorPrimary}
|
||||
size={21}
|
||||
style={{
|
||||
marginTop:
|
||||
StatusBar.currentHeight + 8,
|
||||
marginRight: 16,
|
||||
}}
|
||||
onPress={() => showExtraMenu(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
contentStyle={{
|
||||
backgroundColor: theme.menuColor,
|
||||
}}
|
||||
>
|
||||
<Menu.Item
|
||||
title="Edit info"
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
onPress={() => {
|
||||
showEditInfoModal(true);
|
||||
showExtraMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Edit cover"
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
onPress={setCustomNovelCover}
|
||||
/>
|
||||
</Menu>
|
||||
</Row>
|
||||
</View>
|
||||
)}
|
||||
@@ -298,6 +454,7 @@ const Novel = ({ route, navigation }) => {
|
||||
windowSize={15}
|
||||
initialNumToRender={7}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={{ paddingBottom: 100 }}
|
||||
ListHeaderComponent={
|
||||
<NovelInfoHeader
|
||||
item={item}
|
||||
@@ -306,6 +463,7 @@ const Novel = ({ route, navigation }) => {
|
||||
filter={filter}
|
||||
loading={loading}
|
||||
lastRead={lastReadChapter}
|
||||
setCustomNovelCover={setCustomNovelCover}
|
||||
dispatch={dispatch}
|
||||
chapters={chapters}
|
||||
navigation={navigation}
|
||||
@@ -315,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",
|
||||
@@ -386,7 +574,7 @@ const Novel = ({ route, navigation }) => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Portal>
|
||||
{!loading && (
|
||||
<Portal>
|
||||
<JumpToChapterModal
|
||||
@@ -397,6 +585,13 @@ const Novel = ({ route, navigation }) => {
|
||||
novel={novel}
|
||||
navigation={navigation}
|
||||
/>
|
||||
<EditInfoModal
|
||||
modalVisible={editInfoModal}
|
||||
hideModal={() => showEditInfoModal(false)}
|
||||
novel={novel}
|
||||
theme={theme}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
<ChaptersSettingsSheet
|
||||
novelUrl={novelUrl}
|
||||
bottomSheetRef={chaptersSettingsSheetRef}
|
||||
@@ -431,4 +626,10 @@ const styles = StyleSheet.create({
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
fab: {
|
||||
position: "absolute",
|
||||
margin: 16,
|
||||
right: 0,
|
||||
bottom: 16,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,12 +10,9 @@ import {
|
||||
} from "./Chapter/ChapterDownloadButtons";
|
||||
|
||||
const ChapterItem = ({
|
||||
novelData,
|
||||
chapter,
|
||||
theme,
|
||||
navigation,
|
||||
index,
|
||||
position,
|
||||
showChapterTitles,
|
||||
downloadQueue,
|
||||
downloadChapter,
|
||||
@@ -23,54 +20,15 @@ const ChapterItem = ({
|
||||
isSelected,
|
||||
onSelectPress,
|
||||
onSelectLongPress,
|
||||
navigateToChapter,
|
||||
showProgressPercentage,
|
||||
}) => {
|
||||
const { sourceId, novelName, novelUrl } = novelData;
|
||||
const {
|
||||
novelId,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
read,
|
||||
releaseDate,
|
||||
bookmark,
|
||||
} = chapter;
|
||||
const { chapterId, chapterName, read, releaseDate, bookmark } = chapter;
|
||||
|
||||
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
|
||||
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
|
||||
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
|
||||
|
||||
const navigateToChapter = () =>
|
||||
navigation.navigate("Chapter", {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelId,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
novelName,
|
||||
bookmark,
|
||||
});
|
||||
|
||||
const renderProgressPercentage = () => {
|
||||
const savedProgress =
|
||||
position && position[chapterId] && position[chapterId].percentage;
|
||||
if (savedProgress < 100 && savedProgress > 0 && !read) {
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorHint,
|
||||
fontSize: 12,
|
||||
marginLeft: releaseDate ? 5 : 0,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{releaseDate && "• "}
|
||||
{"Progress " + position[chapterId].percentage + "%"}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[
|
||||
@@ -79,12 +37,14 @@ const ChapterItem = ({
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
onPress={() => onSelectPress(chapter, navigateToChapter)}
|
||||
onPress={() =>
|
||||
onSelectPress(chapter, () => navigateToChapter(chapter))
|
||||
}
|
||||
onLongPress={() => onSelectLongPress(chapter)}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Row>
|
||||
{bookmark === true && <ChapterBookmarkButton theme={theme} />}
|
||||
<Row style={{ flex: 1, overflow: "hidden" }}>
|
||||
{!!bookmark && <ChapterBookmarkButton theme={theme} />}
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
@@ -98,7 +58,7 @@ const ChapterItem = ({
|
||||
? parseChapterNumber(chapterName)
|
||||
? "Chapter " + parseChapterNumber(chapterName)
|
||||
: "Chapter " + index
|
||||
: chapterName.substring(0, 50)}
|
||||
: chapterName}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
@@ -119,7 +79,7 @@ const ChapterItem = ({
|
||||
{releaseDate}
|
||||
</Text>
|
||||
)}
|
||||
{renderProgressPercentage()}
|
||||
{showProgressPercentage(chapter)}
|
||||
</View>
|
||||
</View>
|
||||
</Row>
|
||||
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
Animated,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
|
||||
|
||||
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
|
||||
import { Checkbox as PaperCheckbox } from "react-native-paper";
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
|
||||
import { Checkbox, SortItem } from "../../../components/Checkbox/Checkbox";
|
||||
|
||||
import { showChapterTitlesAction } from "../../../redux/novel/novel.actions";
|
||||
import { ListItem } from "../../../components/List";
|
||||
import { RadioButton, RadioButtonGroup } from "../../../components/RadioButton";
|
||||
import { Checkbox } from "react-native-paper";
|
||||
|
||||
const ChaptersSettingsSheet = ({
|
||||
bottomSheetRef,
|
||||
@@ -34,7 +35,7 @@ const ChaptersSettingsSheet = ({
|
||||
|
||||
const FirstRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Checkbox.Item
|
||||
<PaperCheckbox.Item
|
||||
label="Downloaded"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
@@ -53,7 +54,7 @@ const ChaptersSettingsSheet = ({
|
||||
: filterChapters(filter + " AND downloaded=1")
|
||||
}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<PaperCheckbox.Item
|
||||
label="Unread"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
@@ -80,7 +81,7 @@ const ChaptersSettingsSheet = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Checkbox.Item
|
||||
<PaperCheckbox.Item
|
||||
label="Bookmarked"
|
||||
labelStyle={{
|
||||
fontSize: 14,
|
||||
@@ -101,48 +102,36 @@ const ChaptersSettingsSheet = ({
|
||||
);
|
||||
|
||||
const SecondRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ListItem
|
||||
style={{ paddingVertical: 10 }}
|
||||
title="By source"
|
||||
titleStyle={{ fontSize: 14 }}
|
||||
theme={theme}
|
||||
right={
|
||||
sort === "ORDER BY chapterId ASC"
|
||||
? "arrow-up"
|
||||
: "arrow-down"
|
||||
}
|
||||
iconColor={theme.colorAccent}
|
||||
<View style={{ flex: 1, margin: 8 }}>
|
||||
<SortItem
|
||||
label="By source"
|
||||
status={sort === "ORDER BY chapterId ASC" ? "asc" : "desc"}
|
||||
onPress={() =>
|
||||
sort === "ORDER BY chapterId ASC"
|
||||
? sortChapters("ORDER BY chapterId DESC")
|
||||
: sortChapters("ORDER BY chapterId ASC")
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const ThirdRoute = () => (
|
||||
<View style={{ flex: 1, padding: 8 }}>
|
||||
<RadioButtonGroup
|
||||
onValueChange={(value) =>
|
||||
dispatch(showChapterTitlesAction(novelId, value))
|
||||
<Checkbox
|
||||
status={!showChapterTitles}
|
||||
label="Source title"
|
||||
onPress={() =>
|
||||
dispatch(showChapterTitlesAction(novelId, false))
|
||||
}
|
||||
value={showChapterTitles}
|
||||
>
|
||||
<RadioButton
|
||||
value={false}
|
||||
label="Source title"
|
||||
theme={theme}
|
||||
labelStyle={{ fontSize: 14 }}
|
||||
/>
|
||||
<RadioButton
|
||||
value={true}
|
||||
label="Chapter number"
|
||||
theme={theme}
|
||||
labelStyle={{ fontSize: 14 }}
|
||||
/>
|
||||
</RadioButtonGroup>
|
||||
theme={theme}
|
||||
/>
|
||||
<Checkbox
|
||||
status={showChapterTitles}
|
||||
label="Chapter number"
|
||||
onPress={() => dispatch(showChapterTitlesAction(novelId, true))}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -167,7 +156,7 @@ const ChaptersSettingsSheet = ({
|
||||
indicatorStyle={{ backgroundColor: theme.colorAccent }}
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
renderLabel={({ route, focused, color }) => (
|
||||
<Text style={{ color, margin: 8 }}>{route.title}</Text>
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.textColorSecondary}
|
||||
activeColor={theme.colorAccent}
|
||||
@@ -181,11 +170,12 @@ const ChaptersSettingsSheet = ({
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 220, bottom: 0 }}
|
||||
snappingPoints={[0, 220]}
|
||||
backdropOpacity={0.5}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<TabView
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import React, { useState } from "react";
|
||||
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";
|
||||
import { setNovel } from "../../../redux/novel/novel.actions";
|
||||
import { updateNovelInfo } from "../../../database/queries/NovelQueries";
|
||||
|
||||
const EditInfoModal = ({ theme, hideModal, modalVisible, novel, dispatch }) => {
|
||||
const [info, setInfo] = useState(novel);
|
||||
|
||||
const [tag, setTag] = useState("");
|
||||
|
||||
const textInputTheme = {
|
||||
colors: {
|
||||
primary: theme.colorAccent,
|
||||
placeholder: theme.textColorHint,
|
||||
text: theme.textColorPrimary,
|
||||
background: theme.colorPrimary,
|
||||
},
|
||||
};
|
||||
|
||||
const removeTag = (tag) => {
|
||||
let tags = info.genre.split(",").filter((item) => item !== tag);
|
||||
setInfo({ ...info, genre: tags.join(",") });
|
||||
};
|
||||
|
||||
const status = ["Ongoing", "Hiatus", "Completed", "Unknown", "Cancelled"];
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
onDismiss={hideModal}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.modalTitle,
|
||||
{ color: theme.textColorPrimary },
|
||||
]}
|
||||
>
|
||||
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 }}
|
||||
numberOfLines={1}
|
||||
mode="outlined"
|
||||
theme={textInputTheme}
|
||||
onChangeText={(text) =>
|
||||
setInfo({ ...info, novelName: text })
|
||||
}
|
||||
dense
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={`Author: ${info.author}`}
|
||||
style={{ fontSize: 14 }}
|
||||
numberOfLines={1}
|
||||
mode="outlined"
|
||||
theme={textInputTheme}
|
||||
onChangeText={(text) => setInfo({ ...info, author: text })}
|
||||
dense
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={`Description: ${info.novelSummary.substring(
|
||||
0,
|
||||
16
|
||||
)}...`}
|
||||
style={{ fontSize: 14 }}
|
||||
numberOfLines={1}
|
||||
mode="outlined"
|
||||
onChangeText={(text) =>
|
||||
setInfo({ ...info, novelSummary: text })
|
||||
}
|
||||
theme={textInputTheme}
|
||||
dense
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
placeholder={`Add Tag`}
|
||||
style={{ fontSize: 14 }}
|
||||
numberOfLines={1}
|
||||
mode="outlined"
|
||||
onChangeText={(text) => setTag(text)}
|
||||
onSubmitEditing={() => {
|
||||
setInfo({ ...info, genre: info.genre + "," + tag });
|
||||
setTag("");
|
||||
}}
|
||||
theme={textInputTheme}
|
||||
dense
|
||||
/>
|
||||
|
||||
{info.genre !== null && info.genre !== "" && (
|
||||
<FlatList
|
||||
contentContainerStyle={{ marginVertical: 8 }}
|
||||
horizontal
|
||||
data={info.genre.split(",")}
|
||||
keyExtractor={(item) => item}
|
||||
renderItem={({ item }) => (
|
||||
<GenreChip
|
||||
theme={theme}
|
||||
onPress={() => removeTag(item)}
|
||||
>
|
||||
{item}
|
||||
</GenreChip>
|
||||
)}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
)}
|
||||
<View style={{ flexDirection: "row-reverse" }}>
|
||||
<Button
|
||||
labelStyle={{
|
||||
color: theme.colorAccent,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={() => {
|
||||
updateNovelInfo(info, novel.novelId);
|
||||
hideModal();
|
||||
dispatch(setNovel({ ...novel, ...info }));
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
labelStyle={{
|
||||
color: theme.colorAccent,
|
||||
letterSpacing: 0,
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={hideModal}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditInfoModal;
|
||||
|
||||
const GenreChip = ({ children, theme, onPress }) => (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
borderRadius: 50,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8,
|
||||
marginVertical: 4,
|
||||
marginRight: 8,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: theme.dividerColor,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="close-circle"
|
||||
color={theme.colorAccent}
|
||||
size={16}
|
||||
onPress={onPress}
|
||||
style={{ marginLeft: 4 }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
margin: 30,
|
||||
padding: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
marginBottom: 16,
|
||||
},
|
||||
errorText: {
|
||||
color: "#FF0033",
|
||||
paddingTop: 8,
|
||||
},
|
||||
genreChip: {},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from "react";
|
||||
import { Text, StyleSheet } from "react-native";
|
||||
|
||||
export const GenreChip = ({ children, theme }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{
|
||||
color: theme.colorAccent,
|
||||
borderColor: theme.colorAccent,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
genreChip: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 10,
|
||||
marginHorizontal: 2,
|
||||
fontSize: 13,
|
||||
borderWidth: 1,
|
||||
borderRadius: 50,
|
||||
},
|
||||
});
|
||||
+99
-43
@@ -1,48 +1,110 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
ImageBackground,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Pressable,
|
||||
Dimensions,
|
||||
StatusBar,
|
||||
} from "react-native";
|
||||
|
||||
import { Chip, IconButton } from "react-native-paper";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
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
|
||||
@@ -66,10 +128,7 @@ const NovelAuthor = ({ theme, children }) => (
|
||||
|
||||
const NovelInfo = ({ theme, children }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.novelInfo,
|
||||
{ color: theme.textColorSecondary, textTransform: "capitalize" },
|
||||
]}
|
||||
style={[styles.novelInfo, { color: theme.textColorSecondary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{children}
|
||||
@@ -145,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,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -188,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: {
|
||||
@@ -228,18 +285,17 @@ 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",
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user