Compare commits

..

98 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 8084cdbffb feat(source): Add ArthurScans
LNReader/lnreader-sources#71
2021-09-23 11:31:45 +05:30
Rajarshee Chatterjee dc9529c322 feat(source): Add ReaperScans Br
LNReader/lnreader-sources#71
2021-09-23 10:40:26 +05:30
Rajarshee Chatterjee 91168a335b Minor fixes 2021-09-23 10:39:08 +05:30
Rajarshee Chatterjee a6b3eb9350 Add text to speech play/pause 2021-09-21 09:51:01 +05:30
Rajarshee Chatterjee 0ce4e6b985 Increase bottom padding 2021-09-21 09:49:47 +05:30
Rajarshee Chatterjee 6684848a42 Fix FAB text color 2021-09-21 08:56:03 +05:30
Rajarshee Chatterjee 87d12cabb3 fix(source): RanobeHub 2021-09-21 08:51:40 +05:30
Rajarshee Chatterjee e01fc21a49 feat(source): Add Rain Of Snow
Closes LNReader/lnreader-sources#60
2021-09-15 10:09:09 +05:30
Rajarshee Chatterjee f0cfd9066b feat(source): Add Novel De Glace
Closes LNReader/lnreader-sources#57
2021-09-15 09:41:58 +05:30
Rajarshee Chatterjee c2cc6ea96b Option to clear updates tab 2021-09-15 09:26:24 +05:30
Rajarshee Chatterjee eeeea6b4e0 feat(source): Add Travis Translations
Closes (lnreader-sources#59)
2021-09-13 16:48:10 +05:30
Rajarshee Chatterjee ddce153b53 fix(source): ReaperScans 2021-09-13 16:01:11 +05:30
Rajarshee Chatterjee 38e45ca097 Downloaded only mode 2021-09-13 15:36:05 +05:30
Rajarshee Chatterjee a6a60607a7 Minor fixes 2021-09-12 08:46:58 +05:30
Rajarshee Chatterjee d3e772f0dd Add option to remove backdrop 2021-09-12 08:46:35 +05:30
Rajarshee Chatterjee 095d46681f fix(source): TuNovelaLigera 2021-09-12 08:46:06 +05:30
Rajarshee Chatterjee 3e4e967197 Enlarge cover 2021-09-11 21:42:54 +05:30
Rajarshee Chatterjee c5b4e7b902 Option to use fab instead of button 2021-09-11 10:13:41 +05:30
Rajarshee Chatterjee e9421a5fd0 fix: WPMangaStream (Closes #75) 2021-09-11 08:44:18 +05:30
Rajarshee Chatterjee afef0d63ce Edit status in edit info modal 2021-09-11 08:40:32 +05:30
Rajarshee Chatterjee e68a5c5f82 Minor fixes 2021-09-10 10:04:07 +05:30
Rajarshee Chatterjee ff1635b817 fix(source): Fix ReadLightNovel
Closes LNReader/LNReader-sources#65
2021-09-10 09:35:25 +05:30
Rajarshee Chatterjee f1ca221b91 Add option to only update ongoing novels (Closes #73) 2021-09-10 09:21:13 +05:30
Rajarshee Chatterjee 5a1106fc68 Add Text to Speech (Closes #22) 2021-08-31 10:01:23 +05:30
Rajarshee Chatterjee 3b49a1e516 Minor fixes 2021-08-31 08:03:33 +05:30
Rajarshee Chatterjee 85acbc8c4a Appearance settings 2021-08-29 11:02:24 +05:30
Rajarshee Chatterjee 66e0be5e84 Add option to turn off fullscreen (Closes #70) 2021-08-28 07:27:05 +05:30
Rajarshee Chatterjee 2234006272 Fix fullscreen not working on changing chapters 2021-08-28 07:00:48 +05:30
Rajarshee Chatterjee eca00ddcfa Experimental auto scroll (Closes #63) 2021-08-27 14:28:15 +05:30
Rajarshee Chatterjee 2328e55105 Show time and battery level (Closes #65) 2021-08-27 11:43:17 +05:30
Rajarshee Chatterjee b8a25b9172 Multiple select in chapters (Closes #69) 2021-08-26 21:50:05 +05:30
Rajarshee Chatterjee 9af8d55985 Add option to download unread chapters 2021-08-26 20:18:07 +05:30
Rajarshee Chatterjee 4777f453a4 Refactor themes 2021-08-25 11:15:40 +05:30
Rajarshee Chatterjee 4a0195762d Cross fade 2021-08-25 08:52:51 +05:30
Rajarshee Chatterjee e8785c2905 Minor UI adjustments 2021-08-24 19:26:11 +05:30
Rajarshee Chatterjee 1302cbde16 Draw content under navbar 2021-08-24 16:09:09 +05:30
Rajarshee Chatterjee 43d0adced8 feat(source): Add Mystical Series
Closes LNReader/LNReader-sources#61
2021-08-24 11:16:19 +05:30
Rajarshee Chatterjee 0c8b30cd72 feat(source): Add WuxiaWorld.Site
Closes LNReader/LNReader-sources#58
2021-08-24 11:08:58 +05:30
Rajarshee Chatterjee cc86fc7da4 feat(source): Add EpikNovel
Closes LNReader/LNReader-sources#55
2021-08-24 10:56:16 +05:30
Rajarshee Chatterjee 625acdb585 Release v1.1.4 2021-08-15 13:37:13 +05:30
Rajarshee Chatterjee 93617e60bd feat(source): Add Light Novel Updates 2021-08-14 08:56:59 +05:30
Rajarshee Chatterjee d75c607f67 feat(source): Add NovelOnlineFull.com 2021-08-14 08:04:31 +05:30
Marcin Majewski dd087ff485 fix(chapterlist): Bookmark not updating when added (#64)
When a chapter was bookmarked the list wasn't updating
2021-08-14 07:34:04 +05:30
Rajarshee Chatterjee 5da3e05bba feat: Show last update time 2021-08-13 10:12:26 +05:30
Rajarshee Chatterjee e628c3cfd6 fix: Remove text shadow in comfortable grid 2021-08-13 09:32:13 +05:30
Rajarshee Chatterjee deba021384 Readwn.com: Add user agents 2021-08-12 18:23:43 +05:30
Rajarshee Chatterjee c6d2ba8442 feat: Use tabs for settings and preview in reader settings 2021-08-12 18:21:56 +05:30
Rajarshee Chatterjee 6a3736dced fix: Crash when long pressing a cover in browse 2021-08-12 16:21:22 +05:30
Rajarshee Chatterjee f63ae573ba fix: Mark chapter as unread 2021-08-12 16:19:41 +05:30
Rajarshee Chatterjee 8f3020a544 Release v1.1.3 2021-08-11 16:25:05 +05:30
Rajarshee Chatterjee 04e32a4234 Minor fixes 2021-08-11 16:24:47 +05:30
Rajarshee Chatterjee 9b3d65dded refactor: Reorganize reader sheet 2021-08-11 09:53:18 +05:30
Rajarshee Chatterjee aa3a5c2d0c Minor fixes 2021-08-10 14:32:25 +05:30
Rajarshee Chatterjee 2aab5621a6 feat: Select novels in library 2021-08-10 13:34:19 +05:30
Rajarshee Chatterjee b4b7e6770d feat(source): Add Divine Dao Library
Closes LNReader/lnreader-sources#54
2021-08-09 12:44:32 +05:30
Rajarshee Chatterjee 0ed65d64e4 fix: novelCover crash in list view 2021-08-09 07:26:04 +05:30
Rajarshee Chatterjee a7aaa7d230 feat: Custom CSS 2021-08-07 12:08:31 +05:30
Rajarshee Chatterjee e358b3753f feat(source): Add ranobehub 2021-08-07 08:34:08 +05:30
Rajarshee Chatterjee 780a67e1a6 Update README.md 2021-08-06 10:50:00 +05:30
Rajarshee Chatterjee 5b57e78486 Update README.md 2021-08-06 10:37:22 +05:30
Rajarshee Chatterjee 6430c11cae Update README.md 2021-08-06 10:31:09 +05:30
Rajarshee Chatterjee 322bfc224d Full backups (Closes #34) 2021-08-05 12:24:54 +05:30
Rajarshee Chatterjee a14d35f031 Minor fixes 2021-08-05 12:23:59 +05:30
Rajarshee Chatterjee f03b1703e2 fix(source): ArNovel (Closes #61)
Closes LNReader/lnreader-sources#52
2021-08-04 09:20:35 +05:30
Rajarshee Chatterjee 40d2052f16 Custom covers and open links in reader 2021-08-03 14:38:54 +05:30
Rajarshee Chatterjee 01814e92d0 Fix empty novelCover crash 2021-08-03 10:52:13 +05:30
Rajarshee Chatterjee dffc9c3b99 fix(source): LightNovelPub 2021-08-03 10:48:12 +05:30
Rajarshee Chatterjee 8bb3ffe82c fix(source): ComradeMao 2021-08-03 10:40:34 +05:30
Rajarshee Chatterjee c746081dee Minor fixes 2021-08-03 09:44:14 +05:30
Rajarshee Chatterjee d25fe85a64 Fix discord link 2021-08-02 16:52:32 +05:30
Rajarshee Chatterjee 92d9d53da7 Fix no novelCover crash 2021-07-31 13:32:00 +05:30
Rajarshee Chatterjee 17fcd96f16 Source fixes 2021-07-30 09:15:15 +05:30
Rajarshee Chatterjee 31bada1c1a Refactor 2021-07-29 20:49:21 +05:30
Rajarshee Chatterjee 605bd6a514 Add edit info modal 2021-07-29 11:17:13 +05:30
Rajarshee Chatterjee 01bac76bed Minor fixes 2021-07-29 09:35:55 +05:30
Rajarshee Chatterjee b5ed62d9f8 Fix global search and splashscreen 2021-07-28 17:47:23 +05:30
Rajarshee Chatterjee fd21cc6e85 Minor fixes 2021-07-28 12:14:38 +05:30
Rajarshee Chatterjee 40f1495cf1 Add webview option to support tables and original text style 2021-07-28 11:46:57 +05:30
Rajarshee Chatterjee 983b069dba Add Readwn.com source 2021-07-28 09:47:12 +05:30
Rajarshee Chatterjee e21101ca28 Fix build 2021-07-26 17:01:18 +05:30
Bread Genie d381e878bc docs(readme): Add badges (#60) 2021-07-26 16:59:43 +05:30
Rajarshee Chatterjee bcf5632668 Remove history from redux state and minor fixes 2021-07-26 08:24:59 +05:30
Bread Genie ac8bca7f03 feat(sources): Add ReaperScans Novels (#59) 2021-07-25 21:38:33 +05:30
Bread Genie 62a5e27b1e fix: Indonesia -> Indonesian (#58) 2021-07-25 19:19:08 +05:30
Rajarshee Chatterjee 7c5a9a407f Add new sources 2021-07-23 09:04:47 +05:30
Rajarshee Chatterjee d8d231cb90 Add source WLNUpdates 2021-07-22 09:08:40 +05:30
Rajarshee Chatterjee a5f90eaf8b Add new sources 2021-07-21 20:32:30 +05:30
Rajarshee Chatterjee ec50189ea4 Minor fixes 2021-07-21 08:34:45 +05:30
Rajarshee Chatterjee ccdccdb6bb Remove prevChapter and nextChapter 2021-07-20 08:38:55 +05:30
Bread Genie 059f440713 fix(source): nextChapter link undefined (#55) 2021-07-20 07:55:48 +05:30
Rajarshee Chatterjee b695f57ef4 Refactor 2021-07-19 09:32:48 +05:30
Rajarshee Chatterjee db86b707e7 Fix bookmark icon 2021-07-19 06:00:51 +05:30
Rajarshee Chatterjee 7db4e4b872 Minor fixes 2021-07-18 18:21:25 +05:30
Rajarshee Chatterjee a2c6e9fe58 Custom checkbox 2021-07-18 10:31:07 +05:30
Rajarshee Chatterjee 6c27993454 Remove [hr]() chapter content 2021-07-18 10:30:21 +05:30
Rajarshee Chatterjee 63de0a3794 Fix long chapter names (Closes #53) 2021-07-17 20:24:32 +05:30
Marcin Majewski 93fbcf1246 fix(syosetu): change oneshot chapter name (#52)
Changed oneshot chapter names to "Oneshot" to avoid too long chapters names destroying UI.
2021-07-17 19:55:24 +05:30
Rajarshee Chatterjee 50357f7d6f Release v1.1.2 2021-07-17 08:12:04 +05:30
180 changed files with 28852 additions and 24683 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+2
View File
@@ -6,6 +6,7 @@ import { Provider as PaperProvider } from "react-native-paper";
import { Provider } from "react-redux";
import { persistor, store } from "./src/redux/store";
import { PersistGate } from "redux-persist/lib/integration/react";
import * as SplashScreen from "expo-splash-screen";
import Main from "./src/navigators/Main";
@@ -16,6 +17,7 @@ const App = () => {
const [loaded] = useFonts(fonts);
useEffect(() => {
SplashScreen.preventAutoHideAsync();
createDB();
}, []);
+33 -12
View File
@@ -1,20 +1,41 @@
<h1 align="center">
<br>
LNReader
<br>
</h1>
<p align="center">LNReader is a Tachiyomi-like, free and open source light novel reader for Android.</p>
---
## Screenshots:
<p align="center">
<img src="./.github/readme-images/screenshots.png" align="center" height="350" />
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
<p>
<h1 align="center">
LNReader
</h1>
<p align="center">
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
</p>
<div align="center">
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/LNReader/lnreader/CI">
</a>
<a href="https://discord.gg/QdcWN4MD63">
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48">
</a>
<a href="https://github.com/LNReader/lnreader/releases/latest">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
</a>
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
</a>
</div>
<br>
<br>
<p align="center">
<img src="./.github/readme-images/screenshots.png" align="center" />
</p>
## Download
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
## Sources
LNReader does not have any affiliation with the content providers available.
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
## License
[MIT ](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
+1 -1
View File
@@ -136,7 +136,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.1.1"
versionName "1.1.4"
}
splits {
abi {
+88 -37
View File
@@ -1,38 +1,89 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.rajarsheechatterjee.LNReader">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="41.0.0"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/>
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="unspecified">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="lnreader"/>
<data android:scheme="com.rajarsheechatterjee.LNReader"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
</application>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.rajarsheechatterjee.LNReader">
<!-- Internet -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<meta-data
android:name="expo.modules.updates.EXPO_UPDATE_URL"
android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
<meta-data
android:name="expo.modules.updates.EXPO_SDK_VERSION"
android:value="41.0.0"/>
<meta-data
android:name="expo.modules.updates.ENABLED"
android:value="false"/>
<meta-data
android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH"
android:value="ALWAYS"/>
<meta-data
android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS"
android:value="0"/>
<meta-data
android:name="expo.modules.notifications.default_notification_icon"
android:resource="@drawable/notification_icon"/>
<meta-data
android:name="expo.modules.notifications.default_notification_color"
android:resource="@color/notification_icon_color"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.App.SplashScreen"
android:screenOrientation="unspecified">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="lnreader"/>
<data android:scheme="com.rajarsheechatterjee.LNReader"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
</application>
</manifest>
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);
}
/**
@@ -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.
+19798 -19793
View File
File diff suppressed because it is too large Load Diff
+72 -71
View File
@@ -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"
}
+26 -21
View File
@@ -3,27 +3,32 @@ import { Dimensions, StyleSheet, View } from "react-native";
import { IconButton } from "react-native-paper";
export const Actionbar = ({ actions, theme }) => {
return (
<View
style={[
styles.actionbarContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
{actions.map(
(action, index) =>
action.icon && (
<IconButton
key={index}
icon={action.icon}
color={theme.textColorPrimary}
onPress={action.onPress}
/>
)
)}
</View>
);
export const Actionbar = ({ active, actions, theme, style }) => {
if (active) {
return (
<View
style={[
styles.actionbarContainer,
{ backgroundColor: theme.colorPrimary },
style,
]}
>
{actions.map(
(action, index) =>
action.icon && (
<IconButton
key={index}
icon={action.icon}
color={theme.textColorPrimary}
onPress={action.onPress}
/>
)
)}
</View>
);
} else {
return null;
}
};
const styles = StyleSheet.create({
+5 -123
View File
@@ -1,17 +1,11 @@
import React, { useRef } from "react";
import { View, TextInput, StyleSheet } from "react-native";
import {
TouchableRipple,
IconButton,
Appbar as MaterialAppbar,
} from "react-native-paper";
import Constants from "expo-constants";
import React from "react";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
import { Appbar as MaterialAppbar } from "react-native-paper";
import { useTheme } from "../hooks/reduxHooks";
export const Appbar = ({ title, onBackAction, style, children }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
const theme = useTheme();
return (
<MaterialAppbar.Header
@@ -28,115 +22,3 @@ export const Appbar = ({ title, onBackAction, style, children }) => {
</MaterialAppbar.Header>
);
};
export const SearchAppbar = ({
placeholder,
getSearchResults,
setSearchText,
searchText,
getNovels,
setLoading,
screen,
openWebView,
}) => {
const searchRef = useRef(null);
const navigation = useNavigation();
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<TouchableRipple
borderless
onPress={() => searchRef.current.focus()}
style={[
styles.searchAppbarContainer,
{ backgroundColor: theme.searchBarColor },
]}
>
<View
style={{
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
}}
>
<View style={{ flex: 1, flexDirection: "row" }}>
{screen === "Library" ? (
<IconButton
icon="magnify"
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
/>
) : (
<IconButton
icon="arrow-left"
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
onPress={() => navigation.goBack()}
/>
)}
<TextInput
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorSecondary,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
onChangeText={(text) => {
setSearchText(text);
if (screen === "Library") {
getSearchResults(text);
}
}}
onSubmitEditing={() => {
if (screen === "Extension") {
getSearchResults(searchText);
}
}}
defaultValue={searchText}
/>
</View>
{searchText !== "" && (
<IconButton
icon="close"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={() => {
setLoading?.(true);
getNovels();
setSearchText("");
}}
/>
)}
{openWebView && (
<IconButton
icon="earth"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={openWebView}
/>
)}
</View>
</TouchableRipple>
);
};
const styles = StyleSheet.create({
searchAppbarContainer: {
marginTop: Constants.statusBarHeight + 8,
height: 48,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
marginBottom: 12,
borderRadius: 8,
marginHorizontal: 12,
elevation: 2,
},
});
+4 -4
View File
@@ -4,11 +4,11 @@ import { View } from "react-native";
const BottomSheetHandle = ({ theme }) => (
<View
style={{
backgroundColor: theme.textColorHint,
height: 5,
width: 30,
backgroundColor: `${theme.textColorPrimary}66`,
height: 4,
width: 40,
borderRadius: 50,
top: 10,
top: 8,
alignSelf: "center",
}}
/>
+85
View File
@@ -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",
},
});
+56 -1
View File
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { StyleSheet, Text } from "react-native";
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
import { Modal, Portal, TextInput } from "react-native-paper";
@@ -43,6 +43,29 @@ const ColorPickerModal = ({
},
};
const accentColors = [
"#EF5350",
"#EC407A",
"#AB47BC",
"#7E57C2",
"#5C6BC0",
"#42A5F5",
"#29B6FC",
"#26C6DA",
"#26A69A",
"#66BB6A",
"#9CCC65",
"#D4E157",
"#FFEE58",
"#FFCA28",
"#FFA726",
"#FF7043",
"#8D6E63",
"#BDBDBD",
"#78909C",
"#000000",
];
return (
<Portal>
<Modal
@@ -61,6 +84,38 @@ const ColorPickerModal = ({
>
{title}
</Text>
{showAccentColors && (
<FlatList
contentContainerStyle={{ marginBottom: 8 }}
data={accentColors}
numColumns={4}
keyExtractor={(item) => item}
renderItem={({ item }) => (
<View
style={{
borderRadius: 4,
overflow: "hidden",
backgroundColor: item,
flex: 1 / 4,
height: 40,
marginHorizontal: 4,
marginVertical: 4,
}}
>
<Pressable
style={{ flex: 1 }}
android_ripple={{
color: "rgba(0,0,0,0.12)",
}}
onPress={() => {
onSubmit(item);
hideModal();
}}
/>
</View>
)}
/>
)}
<TextInput
value={text}
defaultValue={color}
+8 -2
View File
@@ -1,8 +1,14 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { View, StyleSheet } from "react-native";
const ScreenContainer = ({ children, theme }) => (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<View
style={{
flex: 1,
backgroundColor: theme.colorPrimaryDark,
paddingBottom: 16,
}}
>
{children}
</View>
);
+110
View File
@@ -0,0 +1,110 @@
import PropTypes from "prop-types";
import React, { PureComponent, Children } from "react";
import { View, Animated, StyleSheet } from "react-native";
export default class FadeView extends PureComponent {
static defaultProps = {
animationDuration: 225,
active: false,
removeHiddenSubviews: true,
};
static propTypes = {
animationDuration: PropTypes.number,
active: PropTypes.bool,
removeHiddenSubviews: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};
constructor(props) {
super(props);
this.renderChild = this.renderChild.bind(this);
this.mounted = false;
let { active } = this.props;
this.state = {
progress: new Animated.Value(Number(active)),
animating: false,
};
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
UNSAFE_componentWillReceiveProps({ active, animationDuration }) {
let { progress } = this.state;
if (active ^ this.props.active) {
this.setState({ animating: true });
Animated.timing(progress, {
toValue: Number(active),
duration: animationDuration,
useNativeDriver: true,
}).start(() => {
if (this.mounted) {
this.setState({ animating: false });
}
});
}
}
renderChild(child, index) {
let { active, removeHiddenSubviews } = this.props;
let { animating, progress } = this.state;
let opacity = progress.interpolate({
inputRange: [0, 1],
outputRange: index ? [0, 1] : [1, 0],
});
let hidden = active ^ !!index;
if (removeHiddenSubviews) {
if (!animating && hidden) {
return null;
}
}
let pointerEvents = hidden ? "none" : "box-none";
return (
<Animated.View
style={[styles.container, { opacity }]}
pointerEvents={pointerEvents}
>
{child}
</Animated.View>
);
}
render() {
let { children, ...props } = this.props;
return (
<View {...props}>{Children.map(children, this.renderChild)}</View>
);
}
}
const styles = StyleSheet.create({
container: {
// ...StyleSheet.absoluteFillObject,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
});
+72
View File
@@ -0,0 +1,72 @@
import React from "react";
import { Pressable, View } from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
<View
style={{
borderRadius: 8,
overflow: "hidden",
marginHorizontal: 4,
}}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 10,
alignItems: "center",
justifyContent: "center",
}}
onPress={onPress}
>
<MaterialCommunityIcons
name={icon}
color={
selected
? theme.colorAccent
: color
? color
: theme.textColorPrimary
}
size={24}
/>
</Pressable>
</View>
);
export const ToggleColorButton = ({
selected,
backgroundColor,
textColor,
onPress,
}) => (
<View
style={{
borderRadius: 50,
overflow: "hidden",
marginHorizontal: 6,
height: 44,
width: 44,
}}
>
<Pressable
android_ripple={{ color: textColor }}
style={{
flex: 1,
padding: 10,
backgroundColor: backgroundColor,
alignItems: "center",
justifyContent: "center",
}}
onPress={onPress}
>
{selected && (
<MaterialCommunityIcons
name="check"
color={textColor}
size={24}
/>
)}
</Pressable>
</View>
);
+75
View File
@@ -0,0 +1,75 @@
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { IconButton } from "react-native-paper";
const icons = ["(・o・;)", "Σ(ಠ_ಠ)", "ಥ_ಥ", "(˘・_・˘)", "(; ̄Д ̄)", "(・Д・。"];
export const ErrorView = ({ errorName, actions, theme }) => (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text style={[styles.emptyViewIcon, { color: theme.textColorHint }]}>
{/* {icons[Math.floor(Math.random() * 5)]} */}
ಥ_ಥ
</Text>
<Text style={[styles.emptyViewText, { color: theme.textColorHint }]}>
{errorName}
</Text>
<View style={{ flexDirection: "row" }}>
{actions.map((action) => (
<View
style={{
borderRadius: 4,
overflow: "hidden",
margin: 16,
}}
>
<Pressable
android_ripple={{
color: theme.rippleColor,
borderless: false,
}}
onPress={action.onPress}
style={{
justifyContent: "center",
alignItems: "center",
paddingBottom: 8,
paddingHorizontal: 20,
minWidth: 100,
}}
>
<IconButton
icon={action.icon}
color={theme.textColorHint}
size={24}
style={{ margin: 0 }}
/>
<Text
style={{
fontSize: 12,
color: theme.textColorHint,
}}
>
{action.name}
</Text>
</Pressable>
</View>
))}
</View>
</View>
);
const styles = StyleSheet.create({
emptyViewContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
emptyViewIcon: {
fontSize: 45,
},
emptyViewText: {
fontWeight: "bold",
marginTop: 10,
textAlign: "center",
paddingHorizontal: 30,
},
});
+57 -10
View File
@@ -1,19 +1,19 @@
import React from "react";
import { StyleSheet, View } from "react-native";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { List, Divider as PaperDivider } from "react-native-paper";
import { List as PaperList, Divider as PaperDivider } from "react-native-paper";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
const ListSection = ({ children }) => (
<List.Section style={styles.listSection}>{children}</List.Section>
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
);
const ListSubHeader = ({ children, theme }) => (
<List.Subheader
<PaperList.Subheader
style={[styles.listSubHeader, { color: theme.colorAccent }]}
>
{children}
</List.Subheader>
</PaperList.Subheader>
);
const ListItem = ({
@@ -27,7 +27,7 @@ const ListItem = ({
titleStyle,
style,
}) => (
<List.Item
<PaperList.Item
title={title}
style={style}
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
@@ -37,7 +37,7 @@ const ListItem = ({
left={() => (
<View style={{ justifyContent: "center" }}>
{icon && (
<List.Icon
<PaperList.Icon
color={theme.colorAccent}
icon={icon}
style={{
@@ -67,13 +67,13 @@ const Divider = ({ theme }) => (
);
const InfoItem = ({ title, icon, theme }) => (
<List.Item
<PaperList.Item
title={title}
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
titleNumberOfLines={5}
left={() =>
icon && (
<List.Icon
<PaperList.Icon
color={theme.textColorSecondary}
icon={icon}
style={{ marginVertical: 0 }}
@@ -83,7 +83,54 @@ const InfoItem = ({ title, icon, theme }) => (
/>
);
export { ListSection, ListSubHeader, ListItem, Divider, InfoItem };
const ColorItem = ({ title, description, theme, onPress }) => (
<Pressable
style={{
padding: 16,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
android_ripple={{ color: theme.rippleColor }}
onPress={onPress}
>
<View>
<Text style={{ color: theme.textColorPrimary, fontSize: 16 }}>
{title}
</Text>
<Text style={{ color: theme.textColorSecondary }}>
{description}
</Text>
</View>
<View
style={{
backgroundColor: description,
height: 24,
width: 24,
borderRadius: 50,
marginRight: 16,
}}
/>
</Pressable>
);
const Icon = ({ icon, theme }) => (
<PaperList.Icon
color={theme.colorAccent}
icon={icon}
style={{ margin: 0 }}
/>
);
export const List = {
Section: ListSection,
SubHeader: ListSubHeader,
Item: ListItem,
Divider,
InfoItem,
ColorItem,
Icon,
};
const styles = StyleSheet.create({
listSection: {
+35 -31
View File
@@ -1,7 +1,7 @@
import React from "react";
import { StyleSheet, View, Text, Image } from "react-native";
import { StyleSheet, View, Text, Pressable } from "react-native";
import FastImage from "react-native-fast-image";
import { TouchableRipple } from "react-native-paper";
const ListView = ({
item,
@@ -10,39 +10,43 @@ const ListView = ({
inLibraryBadge,
theme,
onPress,
isSelected,
onLongPress,
}) => {
return (
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.listView}
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={[
styles.listView,
isSelected && { backgroundColor: theme.rippleColor },
]}
onPress={onPress}
onLongPress={onLongPress}
>
<>
<FastImage
source={{ uri: item.novelCover }}
style={[
styles.extensionIcon,
inLibraryBadge && { opacity: 0.5 },
]}
/>
<Text
style={[
{ color: theme.textColorPrimary },
styles.novelName,
]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={styles.badgeContainer}>
{downloadBadge}
{unreadBadge}
{inLibraryBadge}
</View>
</>
</TouchableRipple>
<FastImage
source={{
uri:
item.novelCover && !item.novelCover.startsWith("/")
? item.novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
style={[
styles.extensionIcon,
inLibraryBadge && { opacity: 0.5 },
]}
/>
<Text
style={[{ color: theme.textColorPrimary }, styles.novelName]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={styles.badgeContainer}>
{downloadBadge}
{unreadBadge}
{inLibraryBadge}
</View>
</Pressable>
);
};
+54 -14
View File
@@ -15,7 +15,15 @@ import ListView from "./ListView";
import { getDeviceOrientation } from "../services/utils/helpers";
import { useSettings } from "../hooks/reduxHooks";
const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
const NovelCover = ({
item,
onPress,
libraryStatus,
theme,
isSelected,
onLongPress,
selectedNovels,
}) => {
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
useSettings();
@@ -31,18 +39,32 @@ const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
[]
);
const selectNovel = () => onLongPress && 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,
+4 -2
View File
@@ -33,7 +33,10 @@ const NovelList = ({
return (
<FlatList
contentContainerStyle={styles.flatListCont}
contentContainerStyle={[
styles.flatListCont,
displayMode !== 2 && { paddingHorizontal: 4 },
]}
numColumns={getNovelsPerRow()}
key={[orientation, getNovelsPerRow()]}
data={data}
@@ -54,6 +57,5 @@ const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
+9 -9
View File
@@ -32,7 +32,7 @@ export const Searchbar = ({
{backAction && (
<IconButton
icon={backAction}
color={theme.textColorSecondary}
color={theme.textColorPrimary}
style={{ marginLeft: 0 }}
size={23}
onPress={onBackAction}
@@ -42,7 +42,7 @@ export const Searchbar = ({
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorSecondary,
color: theme.textColorPrimary,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
@@ -54,7 +54,7 @@ export const Searchbar = ({
{searchText !== "" && (
<IconButton
icon="close"
color={theme.textColorSecondary}
color={theme.textColorPrimary}
style={{ marginRight: 0 }}
size={23}
onPress={clearSearchbar}
@@ -65,7 +65,7 @@ export const Searchbar = ({
<IconButton
key={index}
icon={action.icon}
color={action.color ?? theme.textColorSecondary}
color={action.color ?? theme.textColorPrimary}
style={{ marginRight: 0 }}
size={23}
onPress={action.onPress}
@@ -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",
},
});
+172
View File
@@ -0,0 +1,172 @@
import React from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
export const ThemePicker = ({ theme, currentTheme, onPress }) => (
<View
style={{
justifyContent: "center",
alignItems: "center",
marginRight: 16,
}}
>
<View
style={{
backgroundColor: theme.colorPrimaryDark,
borderWidth: 3.6,
borderColor:
currentTheme.id == theme.id
? theme.colorAccent
: currentTheme.colorPrimaryDark,
width: 95,
height: 140,
borderRadius: 16,
overflow: "hidden",
elevation: 1,
}}
>
<Pressable style={{ flex: 1 }} onPress={onPress}>
{currentTheme.id == theme.id && (
<MaterialCommunityIcons
name="check"
color={theme.colorButtonText}
size={15}
style={{
backgroundColor: theme.colorAccent,
position: "absolute",
top: 5,
right: 5,
elevation: 2,
borderRadius: 50,
padding: 1.6,
}}
/>
)}
<View
style={{
height: 20,
backgroundColor: theme.colorPrimary,
elevation: 1,
justifyContent: "center",
}}
>
<View
style={{
backgroundColor: theme.textColorPrimary,
width: 44,
height: 10,
marginLeft: 8,
borderRadius: 50,
}}
/>
</View>
<View style={{ padding: 8 }}>
<View
style={{
height: 18,
backgroundColor: theme.textColorSecondary,
borderRadius: 4,
}}
/>
<View style={{ paddingVertical: 4, flexDirection: "row" }}>
<View
style={{
height: 10,
width: 44,
backgroundColor: theme.textColorPrimary,
borderRadius: 50,
}}
/>
<View
style={{
height: 10,
width: 16,
marginLeft: 4,
backgroundColor: theme.colorAccent,
borderRadius: 50,
}}
/>
</View>
<View style={{ flexDirection: "row" }}>
<View
style={{
height: 10,
width: 24,
backgroundColor: theme.textColorSecondary,
borderRadius: 50,
}}
/>
<View
style={{
height: 10,
width: 24,
backgroundColor: theme.textColorSecondary,
borderRadius: 50,
marginLeft: 4,
}}
/>
</View>
</View>
<View
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 24,
backgroundColor: theme.colorPrimary,
justifyContent: "center",
}}
>
<View
style={{
flex: 1,
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
paddingHorizontal: 16,
}}
>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.textColorPrimary,
opacity: 0.54,
}}
/>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.colorAccent,
}}
/>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.textColorPrimary,
opacity: 0.54,
}}
/>
</View>
</View>
</Pressable>
</View>
<Text
style={{
color: currentTheme.textColorPrimary,
fontSize: 12,
paddingVertical: 4,
}}
>
{theme.name}
</Text>
</View>
);
const styles = StyleSheet.create({});
+32 -11
View File
@@ -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
+1 -1
View File
@@ -13,7 +13,7 @@ const getHistoryQuery = `
ORDER BY history.historyTimeRead DESC
`;
export const getHistory = async () => {
export const getHistoryFromDb = async () => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
+47 -1
View File
@@ -2,6 +2,7 @@ import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import BackgroundService from "react-native-background-actions";
import * as DocumentPicker from "expo-document-picker";
import { fetchChapters, fetchNovel } from "../../services/Source/source";
import { insertChapters } from "./ChapterQueries";
@@ -46,6 +47,17 @@ export const followNovel = async (followed, novelId) => {
});
};
export const unfollowNovel = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET followed = 0 WHERE novelId = ?",
[novelId],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
const checkNovelInCacheQuery = `SELECT * FROM novels WHERE novelUrl=? LIMIT 1`;
export const checkNovelInCache = (novelUrl) => {
@@ -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;
}
};
+4
View File
@@ -8,6 +8,7 @@ const getUpdatesQuery = `
ON updates.chapterId = chapters.chapterId
JOIN novels
ON updates.novelId = novels.novelId
WHERE date(updates.updateTime) > date('now','-3 months')
ORDER BY updates.updateTime DESC
`;
@@ -36,3 +37,6 @@ export const deleteNovelUpdates = (novelId) => {
);
});
};
export const clearUpdates = () =>
db.transaction((tx) => tx.executeSql("DELETE FROM updates; VACCUM;"));
-2
View File
@@ -4,8 +4,6 @@ export const createDownloadTableQuery = `
downloadChapterId INTEGER NOT NULL,
chapterName TEXT,
chapterText TEXT,
nextChapter TEXT,
prevChapter TEXT,
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
ON DELETE CASCADE
)`;
+1 -1
View File
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
};
const isNewVersion = (versionTag) => {
let currentVersion = "v1.1.1";
let currentVersion = "v1.1.4";
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, "");
+3 -3
View File
@@ -1,10 +1,10 @@
import { shallowEqual, useSelector } from "react-redux";
import { showChapterTitlesAction } from "../redux/novel/novel.actions";
import { useSelector } from "react-redux";
import { darkTheme } from "../theme/theme";
const useTheme = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
return theme;
return theme || darkTheme;
};
const useReaderSettings = () => {
+44 -28
View File
@@ -2,6 +2,7 @@ import React from "react";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Library from "../screens/Library/Library";
import Updates from "../screens/Updates/Updates";
@@ -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}
+5 -1
View File
@@ -7,6 +7,7 @@ import { createStackNavigator } from "@react-navigation/stack";
import { setBarColor } from "../theme/utils/setBarColor";
import { useTheme } from "../hooks/reduxHooks";
import { githubUpdateChecker } from "../hooks/githubUpdateChecker";
import * as SplashScreen from "expo-splash-screen";
// Navigators
import BottomNavigator from "./BottomNavigator";
@@ -30,7 +31,10 @@ const MainNavigator = () => {
const theme = useTheme();
useEffect(() => {
setBarColor(theme);
setTimeout(async () => {
await SplashScreen.hideAsync();
setBarColor(theme);
}, 500);
}, [theme]);
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
+5
View File
@@ -12,6 +12,7 @@ import AdvancedSettings from "../screens/More/AdvancedSettings";
import GeneralSettings from "../screens/More/GeneralSettings";
import DownloadQueue from "../screens/More/DownloadQueue";
import Downloads from "../screens/More/Downloads";
import AppearanceSettings from "../screens/More/AppearanceSettings";
const Stack = createStackNavigator();
@@ -24,6 +25,10 @@ const SettingsStack = () => (
<Stack.Screen name="ReaderSettings" component={ReaderSettings} />
<Stack.Screen name="TrackerSettings" component={TrackerSettings} />
<Stack.Screen name="BackupSettings" component={BackupSettings} />
<Stack.Screen
name="AppearanceSettings"
component={AppearanceSettings}
/>
<Stack.Screen name="AdvancedSettings" component={AdvancedSettings} />
</Stack.Navigator>
);
+1
View File
@@ -31,6 +31,7 @@ export const resumeDownloads = (chapters) => async (dispatch) => {
parameters: {
delay: 1000,
},
linkingURI: "lnreader://downloads",
progressBar: {
max: chapters.length,
value: 0,
-75
View File
@@ -1,75 +0,0 @@
import {
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
CLEAR_HISTORY,
} from "./history.types";
import {
getHistory,
insertHistory,
deleteHistory,
deleteAllHistory,
} from "../../database/queries/HistoryQueries";
import { SET_LAST_READ } from "../preferences/preference.types";
import { showToast } from "../../hooks/showToast";
export const getHistoryAction = () => async (dispatch) => {
dispatch({ type: LOAD_HISTORY });
const history = await getHistory();
const groups = history.reduce((groups, update) => {
var dateParts = update.historyTimeRead.split("-");
var jsDate = new Date(
dateParts[0],
dateParts[1] - 1,
dateParts[2].substr(0, 2)
);
const date = jsDate.toISOString();
if (!groups[date]) {
groups[date] = [];
}
groups[date].push(update);
return groups;
}, {});
const groupedHistory = Object.keys(groups).map((date) => {
return {
date,
novels: groups[date],
};
});
// console.log(groupedHistory);
dispatch({ type: GET_HISTORY, payload: groupedHistory });
};
export const insertHistoryAction = (novelId, chapterId) => async (dispatch) => {
await insertHistory(novelId, chapterId);
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId },
});
dispatch(getHistoryAction());
};
export const deleteHistoryAction = (historyId) => async (dispatch) => {
await deleteHistory(historyId);
console.log("Delete History Action Called");
dispatch(getHistoryAction());
};
export const clearAllHistoryAction = () => async (dispatch) => {
try {
await deleteAllHistory();
showToast("History deleted");
dispatch({ type: CLEAR_HISTORY });
} catch (error) {
showToast(error.message);
}
};
-35
View File
@@ -1,35 +0,0 @@
import {
CLEAR_HISTORY,
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
} from "./history.types";
const initialState = {
history: [],
loading: true,
};
const historyReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case LOAD_HISTORY:
return { ...state, loading: true };
case GET_HISTORY:
return { ...state, history: payload, loading: false };
// case CLEAR_NOVEL_HISTORY:
// return {
// ...state,
// history: state.history.filter(
// (item) => item.novelId !== payload.novelId
// ),
// };
case CLEAR_HISTORY:
return { ...state, loading: false, history: [] };
default:
return state;
}
};
export default historyReducer;
-5
View File
@@ -1,5 +0,0 @@
export const LOAD_HISTORY = "LOAD_HISTORY";
export const GET_HISTORY = "GET_HISTORY";
export const UPDATE_NOVEL_HISTORY = "UPDATE_NOVEL_HISTORY";
export const CLEAR_NOVEL_HISTORY = "CLEAR_NOVEL_HISTORY";
export const CLEAR_HISTORY = "CLEAR_HISTORY";
+13 -8
View File
@@ -10,15 +10,20 @@ import {
} from "../../database/queries/LibraryQueries";
import { restoreBackup } from "../../services/backup";
export const getLibraryAction = (sort, filter) => async (dispatch) => {
// console.log("1.", sort, filter);
const res = await getLibrary(sort, filter);
export const getLibraryAction =
(sort, filter) => async (dispatch, getState) => {
const { downloadedOnlyMode = false } = getState().settingsReducer;
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
const res = await getLibrary(
sort,
downloadedOnlyMode ? "chaptersDownloaded > 0" : filter
);
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
export const searchLibraryAction =
(searchText, sort, filter) => async (dispatch) => {
+79 -73
View File
@@ -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);
}
+76 -23
View File
@@ -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,
}
-2
View File
@@ -7,7 +7,6 @@ import settingsReducer from "./settings/settings.reducer";
import libraryReducer from "./library/library.reducer";
import sourceReducer from "./source/source.reducers";
import novelReducer from "./novel/novel.reducer";
import historyReducer from "./history/history.reducer";
import updatesReducer from "./updates/updates.reducer";
import trackerReducer from "./tracker/tracker.reducer";
import preferenceReducer from "./preferences/preference.reducer";
@@ -26,7 +25,6 @@ const persistedReducer = persistReducer(
libraryReducer,
sourceReducer,
novelReducer,
historyReducer,
updatesReducer,
trackerReducer,
preferenceReducer,
+6 -5
View File
@@ -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());
};
+18 -1
View File
@@ -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;
}
+2
View File
@@ -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";
+1
View File
@@ -41,6 +41,7 @@ const Browse = ({ navigation }) => {
pinned.indexOf(sourceId) === -1 ? false : true;
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
search = search.filter((source) => filters.indexOf(source.lang) === -1);
let pinnedSources = sources.filter((source) => isPinned(source.sourceId));
-168
View File
@@ -1,168 +0,0 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
import * as WebBrowser from "expo-web-browser";
import NovelCover from "../../components/NovelCover";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import ErrorView from "../../components/ErrorView";
import { useTheme, useSettings } from "../../hooks/reduxHooks";
import { showToast } from "../../hooks/showToast";
import { getDeviceOrientation } from "../../services/utils/helpers";
import { scrapeSearchResults } from "./discover/MyAnimeListScraper";
const BrowseMalScreen = ({ navigation, route }) => {
const theme = useTheme();
const settings = useSettings();
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState();
const [error, setError] = useState();
const [searchText, setSearchText] = useState("");
const baseUrl = "https://api.jikan.moe/v3/";
const malUrl = "https://myanimelist.net/topmanga.php?type=lightnovels";
const getNovels = async () => {
try {
const url = `${baseUrl}top/manga/1/novels`;
const res = await fetch(url);
const data = await res.json();
setNovels(data.top);
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
const clearSearchbar = () => {
getNovels();
setLoading(true);
setSearchText("");
};
const getSearchResults = async () => {
try {
setLoading(true);
const data = await scrapeSearchResults(searchText);
setNovels(data);
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
useEffect(() => {
getNovels();
}, []);
const renderItem = ({ item }) => (
<NovelCover
item={{ novelName: item.title, novelCover: item.image_url }}
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: item.title,
})
}
theme={theme}
/>
);
const { displayMode, novelsPerRow } = settings;
const orientation = getDeviceOrientation();
const getNovelsPerRow = () => {
if (displayMode === 2) {
return 1;
}
if (orientation === "landscape") {
return 6;
} else {
return novelsPerRow;
}
};
const ListEmptyComponent = () => (
<ErrorView
error={error || "No results found"}
onRetry={() => {
getNovels();
setLoading(true);
setError();
}}
openWebView={() => WebBrowser.openBrowserAsync(topNovelsUrl)}
theme={theme}
/>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
theme={theme}
placeholder="Search MyAnimeList"
backAction="arrow-left"
onBackAction={() => navigation.goBack()}
searchText={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchResults}
clearSearchbar={clearSearchbar}
actions={[
{
icon: "earth",
onPress: () => WebBrowser.openBrowserAsync(malUrl),
},
]}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={60} color={theme.colorAccent} />
</View>
) : (
<FlatList
contentContainerStyle={styles.novelsContainer}
numColumns={getNovelsPerRow()}
key={getNovelsPerRow()}
data={novels}
keyExtractor={(item) => item.mal_id.toString()}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
/>
)}
</View>
);
};
export default BrowseMalScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
},
contentContainer: {
flex: 1,
},
novelsContainer: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
+89 -61
View File
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import { FlatList, Pressable, Text, View } from "react-native";
import { Switch } from "react-native-paper";
@@ -6,7 +6,7 @@ import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import { ListSubHeader } from "../../components/List";
import { List } from "../../components/List";
import { useSettings, useTheme } from "../../hooks/reduxHooks";
import { setAppSettings } from "../../redux/settings/settings.actions";
@@ -18,10 +18,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>
+13 -8
View File
@@ -4,7 +4,7 @@ import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
import * as WebBrowser from "expo-web-browser";
import { Searchbar } from "../../../components/Searchbar/Searchbar";
import ErrorView from "../../../components/ErrorView";
import { ErrorView } from "../../../components/ErrorView/ErrorView";
import { useTheme, useSettings } from "../../../hooks/reduxHooks";
import { showToast } from "../../../hooks/showToast";
@@ -106,13 +106,18 @@ const BrowseMalScreen = ({ navigation, route }) => {
const ListEmptyComponent = () => (
<ErrorView
error={error || "No results found"}
onRetry={() => {
getNovels();
setLoading(true);
setError();
}}
openWebView={() => WebBrowser.openBrowserAsync(topNovelsUrl)}
errorName={error || "No results found"}
actions={[
{
name: "Retry",
onPress: () => {
getNovels();
setLoading(true);
setError();
},
icon: "reload",
},
]}
theme={theme}
/>
);
@@ -74,9 +74,6 @@ const GlobalSearch = ({ route, navigation }) => {
},
]);
setLoading(false);
setProgress(
(progress) => progress + 1 / globalSearchSources.length
);
} catch (error) {
showToast(
globalSearchSources[i].sourceName + ": " + error.message
@@ -92,6 +89,9 @@ const GlobalSearch = ({ route, navigation }) => {
]);
setLoading(false);
}
setProgress(
(progress) => progress + 1 / globalSearchSources.length
);
}
};
@@ -1,7 +1,5 @@
import React from "react";
import { StyleSheet, View, Text, Image } from "react-native";
import { TouchableRipple } from "react-native-paper";
import { StyleSheet, View, Text, Image, Pressable } from "react-native";
const GlobalSearchNovelCover = ({
novel,
@@ -13,18 +11,21 @@ const GlobalSearchNovelCover = ({
const { novelName, novelCover } = novel;
return (
<View style={{ flex: 1 }}>
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
<View style={{ flex: 1, borderRadius: 6, overflow: "hidden" }}>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.novelContainer}
onPress={onPress}
onLongPress={onLongPress}
>
<>
<Image
source={{ uri: novelCover }}
source={{
uri:
novelCover && !novelCover.startsWith("/")
? novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
style={[
styles.novelCover,
inLibrary && { opacity: 0.5 },
@@ -41,7 +42,7 @@ const GlobalSearchNovelCover = ({
{novelName}
</Text>
</>
</TouchableRipple>
</Pressable>
</View>
);
};
@@ -9,11 +9,7 @@ const GlobalSearchNovelList = ({ data, theme, library, navigation }) => {
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
);
const navigateToNovel = (item) =>
navigation.navigate("Novel", {
...item,
sourceId: item.sourceId,
});
const navigateToNovel = (item) => navigation.push("Novel", item);
const renderItem = ({ item }) => (
<GlobalSearchNovelCover
@@ -61,9 +61,6 @@ const MigrationNovels = ({ navigation, route }) => {
},
]);
setLoading(false);
setProgress(
(progress) => progress + 1 / migrationSources.length
);
} catch (error) {
showToast(error.message);
@@ -78,6 +75,7 @@ const MigrationNovels = ({ navigation, route }) => {
]);
setLoading(false);
}
setProgress((progress) => progress + 1 / migrationSources.length);
}
};
+490 -127
View File
@@ -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",
},
});
-257
View File
@@ -1,257 +0,0 @@
import React, { memo, useCallback, useEffect } from "react";
import { Text, View } from "react-native";
import FitImage from "react-native-fit-image";
import { showToast } from "../../hooks/showToast";
const getHeadingSize = (size) => {
const headings = {
h1: 6,
h2: 5,
h3: 4,
h4: 3,
h5: 2,
h6: 1,
strong: 0,
};
return headings[size];
};
const Header = ({ size, text, textStyle, textSelectable, textSize }) => (
<Text
style={[
textStyle,
{
fontSize: textSize + getHeadingSize(size),
fontWeight: "bold",
},
]}
selectable={textSelectable}
>
{text}
</Text>
);
const DefaultText = ({ text, textStyle, textSelectable }) => (
<Text style={textStyle} selectable={textSelectable}>
{text}
</Text>
);
const ItalicText = ({ text, textStyle, textSelectable }) => (
<Text
style={[textStyle, { fontStyle: "italic" }]}
selectable={textSelectable}
>
{text}
</Text>
);
const HorizontalRule = ({ textColor }) => (
<View
style={{
borderBottomWidth: 1,
flex: 1,
borderBottomColor: textColor,
marginVertical: 16,
}}
/>
);
const Link = ({
link,
text,
textStyle,
textSelectable,
onPressLink,
theme,
}) => (
<Text
style={[
textStyle,
{
alignSelf: "flex-start",
color: theme.colorAccent,
textDecorationLine: "underline",
},
]}
selectable={textSelectable}
onPress={() => onPressLink(link)}
>
{text}
</Text>
);
const TextToComponents = ({
text,
textStyle,
textSelectable,
textSize,
textColor,
onPressLink,
theme,
}) => {
try {
regex = /(\[(?:\n|.)*?\]\(.*?\))/;
let parseText = text.trim().split(regex);
return parseText.map((part, index) => {
if (part) {
let match = part.match(/\[((?:\n|.)*?)\]\((.*?)\)/);
if (match) {
tag = match[1];
text = match[2];
if (tag.startsWith("Anchor:")) {
link = tag.replace("Anchor: ", "");
tag = "a";
}
switch (tag) {
case "h1":
return (
<Header
size="h1"
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
textSize={textSize}
/>
);
case "h2":
return (
<Header
size="h2"
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
textSize={textSize}
/>
);
case "h3":
return (
<Header
size="h3"
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
textSize={textSize}
/>
);
case "h4":
return (
<Header
size="h4"
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
textSize={textSize}
/>
);
case "h5":
return (
<Header
size="h5"
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
textSize={textSize}
/>
);
case "h6":
return (
<Header
size="h6"
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
textSize={textSize}
/>
);
case "strong":
return (
<Header
size="strong"
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
textSize={textSize}
/>
);
case "em":
return (
<ItalicText
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
/>
);
case "a":
return (
<Link
link={text}
textStyle={textStyle}
textSelectable={textSelectable}
text={link}
onPressLink={onPressLink}
theme={theme}
key={index}
/>
);
case "img":
return (
<FitImage source={{ uri: text }} key={index} />
);
case "hr":
return (
<HorizontalRule
key={index}
textColor={textColor}
/>
);
default:
return (
<DefaultText
text={part}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
/>
);
}
} else {
return (
<DefaultText
text={part}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
/>
);
}
}
});
} catch (error) {
return (
<DefaultText
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
key={index}
/>
);
}
};
export default memo(TextToComponents);
+48 -25
View File
@@ -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;
+67 -36
View File
@@ -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,
},
});
+514 -403
View File
@@ -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>
`,
}}
/>
);
+2 -6
View File
@@ -5,6 +5,7 @@ export const readerBackground = (val) => {
3: "#F7DFC6",
4: "#292832",
5: "#2B2C30",
6: "#dce5e2",
};
return backgroundColor[val] || val;
@@ -17,6 +18,7 @@ export const readerTextColor = (val) => {
3: "#593100",
4: "#CCCCCC",
5: "#CCCCCC",
6: "#000000",
};
return textColor[val] || val;
@@ -29,9 +31,3 @@ export const readerLineHeight = (fontSize, lineHeightMultiplier) => {
return lineHeight;
};
export const errorTextColor = (val) => {
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.54)";
return textColor;
};
+31 -17
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { StyleSheet, View, ActivityIndicator } from "react-native";
import * as WebBrowser from "expo-web-browser";
@@ -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 &&
+69 -19
View File
@@ -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>
);
};
+15 -5
View File
@@ -5,10 +5,15 @@ import moment from "moment";
import { IconButton } from "react-native-paper";
import { parseChapterNumber } from "../../services/utils/helpers";
import { deleteHistoryAction } from "../../redux/history/history.actions";
import { setNovel } from "../../redux/novel/novel.actions";
const HistoryItem = ({ history, theme, dispatch, navigation }) => {
const HistoryItem = ({
history,
theme,
dispatch,
navigation,
deleteHistory,
}) => {
const {
historyId,
historyTimeRead,
@@ -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"
+5 -8
View File
@@ -1,16 +1,13 @@
import React from "react";
import { Button, Dialog, Portal } from "react-native-paper";
import { clearAllHistoryAction } from "../../redux/history/history.actions";
import { deleteAllHistory } from "../../database/queries/HistoryQueries";
import { showToast } from "../../hooks/showToast";
const RemoveHistoryDialog = ({
dialogVisible,
hideDialog,
dispatch,
theme,
}) => {
const RemoveHistoryDialog = ({ dialogVisible, hideDialog, theme }) => {
const onPress = () => {
dispatch(clearAllHistoryAction());
deleteAllHistory();
showToast("History deleted.");
hideDialog();
};
+152 -31
View File
@@ -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
View File
@@ -1,71 +1,67 @@
import React from "react";
import { View } from "react-native";
import * as Linking from "expo-linking";
import { Appbar } from "../../components/Appbar";
import { useSelector } from "react-redux";
import { Divider, ListItem, ListSection } from "../../components/List";
import { List } from "../../components/List";
import { ScreenContainer } from "../../components/Common";
import { MoreHeader } from "./components/MoreHeader";
const AboutScreen = ({ navigation }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<>
<Appbar title="About" onBackAction={() => navigation.goBack()} />
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<ListSection>
<ListItem
title="Version"
description="Stable 1.1.1"
theme={theme}
/>
<ListItem
title="Build Time"
description="04-07-21 21:00 PM"
theme={theme}
/>
<ListItem
title="What's new"
onPress={() =>
Linking.openURL(
"https://github.com/LNReader/lnreader/commits/main"
)
}
theme={theme}
/>
<Divider theme={theme} />
<ListItem
title="Discord"
description="https://discord.gg/QdcWN4MD63"
onPress={() =>
Linking.openURL("https://discord.gg/QdcWN4MD63")
}
theme={theme}
/>
<ListItem
title="Github"
description="https://github.com/LNReader/lnreader"
onPress={() =>
Linking.openURL(
"https://github.com/LNReader/lnreader"
)
}
theme={theme}
/>
<ListItem
title="Sources"
description="https://github.com/LNReader/lnreader-sources"
onPress={() =>
Linking.openURL(
"https://github.com/LNReader/lnreader-sources"
)
}
theme={theme}
/>
</ListSection>
</View>
</>
<ScreenContainer theme={theme}>
<MoreHeader
title="About"
navigation={navigation}
theme={theme}
goBack={true}
/>
<List.Section>
<List.Item
title="Version"
description="Stable 1.1.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>
);
};
+16 -54
View File
@@ -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}
+262
View File
@@ -0,0 +1,262 @@
import React, { useState } from "react";
import { ScrollView, Text } from "react-native";
import { useDispatch } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import { List } from "../../components/List";
import { ThemePicker } from "../../components/ThemePicker/ThemePicker";
import SwitchSetting from "../../components/Switch/Switch";
import ColorPickerModal from "../../components/ColorPickerModal";
import { useSettings, useTheme } from "../../hooks/reduxHooks";
import {
setAccentColor,
setAmoledMode,
setAppSettings,
setAppTheme,
setRippleColor,
} from "../../redux/settings/settings.actions";
import {
darkTheme,
greenAppleTheme,
irisBlueTheme,
lightTheme,
midnightDuskTheme,
oceanicTheme,
springBlossomTheme,
strawberryDaiquiri,
takoLightTheme,
takoTheme,
yangTheme,
yinYangTheme,
yotsubaTheme,
} from "../../theme/theme";
const lightThemes = [
lightTheme,
springBlossomTheme,
takoLightTheme,
yangTheme,
yotsubaTheme,
];
const darkThemes = [
darkTheme,
midnightDuskTheme,
strawberryDaiquiri,
takoTheme,
greenAppleTheme,
yinYangTheme,
irisBlueTheme,
oceanicTheme,
];
const AppearanceSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const {
showHistoryTab = true,
showUpdatesTab = true,
showLabelsInNav = false,
hideBackdrop = false,
useFabForContinueReading = false,
} = useSettings();
/**
* Accent Color Modal
*/
const [accentColorModal, setAccentColorModal] = useState(false);
const showAccentColorModal = () => setAccentColorModal(true);
const hideAccentColorModal = () => setAccentColorModal(false);
const hexToRgb = (hex) => {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};
const onSubmit = (val) => {
dispatch(setAccentColor(val));
const rgb = hexToRgb(val);
const rgbaColor = `rgba(${rgb.r},${rgb.g},${rgb.b},0.12)`;
dispatch(setRippleColor(rgbaColor));
};
return (
<ScreenContainer theme={theme}>
<Appbar title="Appearance" onBackAction={navigation.goBack} />
<ScrollView style={{ flex: 1 }}>
<List.Section>
<List.SubHeader theme={theme}>App theme</List.SubHeader>
<Text
style={{
color: theme.textColorPrimary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Light Theme
</Text>
<ScrollView
contentContainerStyle={{
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: "row",
}}
horizontal={true}
showsHorizontalScrollIndicator={false}
>
{lightThemes.map((item) => (
<ThemePicker
key={item.id}
currentTheme={theme}
theme={item}
onPress={() => dispatch(setAppTheme(item.id))}
/>
))}
</ScrollView>
<Text
style={{
color: theme.textColorPrimary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Dark Theme
</Text>
<ScrollView
contentContainerStyle={{
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: "row",
}}
horizontal={true}
showsHorizontalScrollIndicator={false}
>
{darkThemes.map((item) => (
<ThemePicker
key={item.id}
currentTheme={theme}
theme={item}
onPress={() => dispatch(setAppTheme(item.id))}
/>
))}
</ScrollView>
{theme.statusBar === "light-content" && (
<SwitchSetting
label="Pure black dark mode"
value={
theme.colorPrimary === "#000000" &&
theme.colorPrimaryDark === "#000000"
}
onPress={() =>
dispatch(
setAmoledMode(
theme.id,
!(
theme.colorPrimary === "#000000" &&
theme.colorPrimaryDark === "#000000"
)
)
)
}
theme={theme}
/>
)}
<List.ColorItem
title="Accent Color"
description={theme.colorAccent.toUpperCase()}
onPress={showAccentColorModal}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Novel info</List.SubHeader>
<SwitchSetting
label="Hide backdrop"
value={hideBackdrop}
onPress={() =>
dispatch(
setAppSettings("hideBackdrop", !hideBackdrop)
)
}
theme={theme}
/>
<SwitchSetting
label="Use FAB instead of button"
value={useFabForContinueReading}
onPress={() =>
dispatch(
setAppSettings(
"useFabForContinueReading",
!useFabForContinueReading
)
)
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Navbar</List.SubHeader>
<SwitchSetting
label="Show updates in the nav"
value={showUpdatesTab}
onPress={() =>
dispatch(
setAppSettings(
"showUpdatesTab",
!showUpdatesTab
)
)
}
theme={theme}
/>
<SwitchSetting
label="Show history in the nav"
value={showHistoryTab}
onPress={() =>
dispatch(
setAppSettings(
"showHistoryTab",
!showHistoryTab
)
)
}
theme={theme}
/>
<SwitchSetting
label="Always show nav labels"
value={showLabelsInNav}
onPress={() =>
dispatch(
setAppSettings(
"showLabelsInNav",
!showLabelsInNav
)
)
}
theme={theme}
/>
</List.Section>
</ScrollView>
<ColorPickerModal
title="Accent color"
modalVisible={accentColorModal}
hideModal={hideAccentColorModal}
color={theme.colorAccent}
onSubmit={onSubmit}
theme={theme}
showAccentColors={true}
/>
</ScreenContainer>
);
};
export default AppearanceSettings;
+20 -15
View File
@@ -2,17 +2,15 @@ import React from "react";
import { useDispatch } from "react-redux";
import { Appbar } from "../../components/Appbar";
import {
InfoItem,
ListItem,
ListSection,
ListSubHeader,
} from "../../components/List";
import { List } from "../../components/List";
import { ScreenContainer } from "../../components/Common";
import { useTheme } from "../../hooks/reduxHooks";
import { restoreLibraryAction } from "../../redux/library/library.actions";
import { createBackup } from "../../services/backup";
import {
createFullBackup,
restoreFullBackup,
} from "../../services/backup/backup";
const BackupSettings = ({ navigation }) => {
const theme = useTheme();
@@ -25,27 +23,34 @@ const BackupSettings = ({ navigation }) => {
title="Backup"
onBackAction={() => navigation.goBack()}
/>
<ListSection>
<ListSubHeader theme={theme}>Backup</ListSubHeader>
<ListItem
<List.Section>
<List.SubHeader theme={theme}>Backup</List.SubHeader>
<List.Item
title="Create backup"
description="Can be used to restore current library"
onPress={createBackup}
onPress={createFullBackup}
theme={theme}
/>
<ListItem
<List.Item
title="Restore backup"
description="Restore library from backup file"
onPress={() => 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>
</>
);
-1
View File
@@ -55,7 +55,6 @@ const DownloadQueue = ({ navigation }) => {
if (downloadQueue.length > 0) {
dispatch(cancelDownload());
showToast("Downloads cancelled.");
BackgroundService.stop();
}
closeMenu();
}}
+84 -137
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
};
+16 -6
View File
@@ -2,7 +2,7 @@ import React from "react";
import { View } from "react-native";
import { Appbar } from "../../components/Appbar";
import { ListItem } from "../../components/List";
import { List } from "../../components/List";
import { useTheme } from "../../hooks/reduxHooks";
@@ -18,7 +18,7 @@ const SettingsScreen = ({ navigation }) => {
backgroundColor: theme.colorPrimaryDark,
}}
>
<ListItem
<List.Item
title="General"
icon="tune"
onPress={() =>
@@ -28,7 +28,7 @@ const SettingsScreen = ({ navigation }) => {
}
theme={theme}
/>
<ListItem
<List.Item
title="Reader"
icon="book-open-outline"
onPress={() =>
@@ -38,7 +38,7 @@ const SettingsScreen = ({ navigation }) => {
}
theme={theme}
/>
<ListItem
<List.Item
title="Tracking"
icon="sync"
onPress={() =>
@@ -48,7 +48,7 @@ const SettingsScreen = ({ navigation }) => {
}
theme={theme}
/>
<ListItem
<List.Item
title="Backup"
icon="cloud-upload-outline"
onPress={() =>
@@ -58,7 +58,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={() =>
+8 -13
View File
@@ -11,12 +11,7 @@ import {
import { Appbar } from "../../components/Appbar";
import { useSelector, useDispatch } from "react-redux";
import {
Divider,
ListItem,
ListSection,
ListSubHeader,
} from "../../components/List";
import { List } from "../../components/List";
import { removeTracker, setTracker } from "../../redux/tracker/tracker.actions";
const TrackerScreen = ({ navigation }) => {
@@ -39,9 +34,9 @@ const TrackerScreen = ({ navigation }) => {
paddingVertical: 8,
}}
>
<ListSection>
<ListSubHeader theme={theme}>Services</ListSubHeader>
<ListItem
<List.Section>
<List.SubHeader theme={theme}>Services</List.SubHeader>
<List.Item
title="MyAnimeList"
onPress={async () => {
if (tracker) {
@@ -82,10 +77,10 @@ const TrackerScreen = ({ navigation }) => {
{tracker && tracker.expires_in < new Date(Date.now()) && (
<>
<Divider theme={theme} />
<ListSubHeader theme={theme}>
<List.SubHeader theme={theme}>
Settings
</ListSubHeader>
<ListItem
</List.SubHeader>
<List.Item
title="Revalidate MyAnimeList"
onPress={() => {
const res = malTokenWatcher(tracker);
@@ -95,7 +90,7 @@ const TrackerScreen = ({ navigation }) => {
/>
</>
)}
</ListSection>
</List.Section>
<Portal>
<Modal
+15 -34
View File
@@ -1,6 +1,10 @@
import React from "react";
import { StyleSheet, View, Text, Pressable } from "react-native";
import { Portal, Modal, Checkbox } from "react-native-paper";
import { StyleSheet } from "react-native";
import { Portal, Modal } from "react-native-paper";
import { Checkbox } from "../../../components/Checkbox/Checkbox";
import { setAppSettings } from "../../../redux/settings/settings.actions";
const DisplayModeModal = ({
@@ -13,44 +17,21 @@ const DisplayModeModal = ({
const displayModes = [
{ displayMode: 0, label: "Compact Grid" },
{ displayMode: 1, label: "Comfortable Grid" },
{ displayMode: 3, label: "No Title Grid" },
{ displayMode: 2, label: "List" },
];
const renderCheckboxes = () => {
return displayModes.map((mode) => (
<View
<Checkbox
key={mode.displayMode}
style={[
{
borderRadius: 8,
overflow: "hidden",
marginVertical: 4,
},
displayMode === mode.displayMode && {
backgroundColor: theme.rippleColor,
},
]}
>
<Pressable
android_ripple={{
color: theme.rippleColor,
}}
style={{
paddingHorizontal: 16,
paddingVertical: 12,
justifyContent: "space-between",
}}
onPress={() =>
dispatch(
setAppSettings("displayMode", mode.displayMode)
)
}
>
<Text style={{ color: theme.textColorPrimary }}>
{mode.label}
</Text>
</Pressable>
</View>
status={displayMode === mode.displayMode}
onPress={() =>
dispatch(setAppSettings("displayMode", mode.displayMode))
}
label={mode.label}
theme={theme}
/>
));
};
+10 -13
View File
@@ -4,6 +4,7 @@ import { Portal, Modal } from "react-native-paper";
import Slider from "@react-native-community/slider";
import { RadioButton, RadioButtonGroup } from "../../../components/RadioButton";
import { setNovelsPerRow } from "../../../redux/settings/settings.actions";
import { Checkbox } from "../../../components/Checkbox/Checkbox";
const GridSizeModal = ({
dispatch,
@@ -46,19 +47,15 @@ const GridSizeModal = ({
>
{`${novelsPerRow} per row`}
</Text>
<RadioButtonGroup
onValueChange={(value) => dispatch(setNovelsPerRow(value))}
value={novelsPerRow}
>
{Object.keys(gridSizes).map((item) => (
<RadioButton
key={item}
value={item}
label={gridSizes[item]}
theme={theme}
/>
))}
</RadioButtonGroup>
{Object.keys(gridSizes).map((item) => (
<Checkbox
key={item}
status={item === novelsPerRow}
label={gridSizes[item]}
onPress={() => dispatch(setNovelsPerRow(item))}
theme={theme}
/>
))}
</Modal>
</Portal>
);
+39
View File
@@ -0,0 +1,39 @@
import React from "react";
import { Image, StyleSheet, View } from "react-native";
import { Appbar } from "../../../components/Appbar";
export const MoreHeader = ({ title, navigation, theme, goBack }) => (
<>
<Appbar
title={title}
onBackAction={goBack && navigation.goBack}
style={{ elevation: 0 }}
/>
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
<View
style={[
styles.logoContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<Image
source={require("../../../../assets/logo.png")}
style={[styles.logo, { tintColor: theme.textColorPrimary }]}
/>
</View>
</View>
</>
);
const styles = StyleSheet.create({
logoContainer: {
paddingTop: 8,
paddingBottom: 24,
alignItems: "center",
elevation: 4,
},
logo: {
height: 80,
width: 80,
},
});
-76
View File
@@ -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
View File
@@ -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 -50
View File
@@ -10,12 +10,9 @@ import {
} from "./Chapter/ChapterDownloadButtons";
const ChapterItem = ({
novelData,
chapter,
theme,
navigation,
index,
position,
showChapterTitles,
downloadQueue,
downloadChapter,
@@ -23,54 +20,15 @@ const ChapterItem = ({
isSelected,
onSelectPress,
onSelectLongPress,
navigateToChapter,
showProgressPercentage,
}) => {
const { sourceId, novelName, novelUrl } = novelData;
const {
novelId,
chapterId,
chapterUrl,
chapterName,
read,
releaseDate,
bookmark,
} = chapter;
const { chapterId, chapterName, read, releaseDate, bookmark } = chapter;
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
const navigateToChapter = () =>
navigation.navigate("Chapter", {
sourceId,
novelUrl,
novelId,
chapterId,
chapterUrl,
chapterName,
novelName,
bookmark,
});
const renderProgressPercentage = () => {
const savedProgress =
position && position[chapterId] && position[chapterId].percentage;
if (savedProgress < 100 && savedProgress > 0 && !read) {
return (
<Text
style={{
color: theme.textColorHint,
fontSize: 12,
marginLeft: releaseDate ? 5 : 0,
}}
numberOfLines={1}
>
{releaseDate && "• "}
{"Progress " + position[chapterId].percentage + "%"}
</Text>
);
}
};
return (
<Pressable
style={[
@@ -79,12 +37,14 @@ const ChapterItem = ({
backgroundColor: theme.rippleColor,
},
]}
onPress={() => onSelectPress(chapter, navigateToChapter)}
onPress={() =>
onSelectPress(chapter, () => navigateToChapter(chapter))
}
onLongPress={() => onSelectLongPress(chapter)}
android_ripple={{ color: theme.rippleColor }}
>
<Row>
{bookmark === true && <ChapterBookmarkButton theme={theme} />}
<Row style={{ flex: 1, overflow: "hidden" }}>
{!!bookmark && <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,
},
});
@@ -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