Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cab9a83dc0 | |||
| 934798809d | |||
| 4ceacdf324 | |||
| 650ed2ff67 | |||
| c38fc3232e | |||
| f109c5747e | |||
| 79b4db0cd2 | |||
| 8858ea3d94 | |||
| dea534557e | |||
| 0b112d9465 | |||
| ddb8cd402a | |||
| 172145278f | |||
| 7acb63e0f0 | |||
| deefc10d41 | |||
| 7880cd0187 | |||
| 9a594c4dd3 | |||
| 11a32d6776 | |||
| 49e4e87f9e | |||
| 1d0d7be968 | |||
| 06c5efd3a6 | |||
| 6d67c67d5f | |||
| e559824718 | |||
| 2aaedfcfbf | |||
| 4cbd52a369 | |||
| 87a42d2766 | |||
| 1453c39fc3 | |||
| eccb84b4a3 | |||
| 3d69f46b44 | |||
| cc94bbd152 | |||
| 599224a5d4 | |||
| 70a22518be | |||
| 900a569886 | |||
| 6e851eed94 | |||
| a2ceba9019 | |||
| 412f216bf5 | |||
| 1e2dddf46c | |||
| 3e526658bf | |||
| 15cdeb30ad | |||
| 3cb573681f | |||
| 383c8f791b | |||
| 125632b9ff | |||
| cb67d17747 | |||
| 7ec578639e | |||
| a1d6e1d6ce | |||
| 1fed66792d | |||
| abc815e413 | |||
| 8cb27bf95f | |||
| a84ee5ade4 | |||
| 30ce798c2a | |||
| 5ed221648c | |||
| 93bc5f19ff | |||
| d527319901 | |||
| 20470929e5 | |||
| 69dc329fdb | |||
| 78165b072e |
@@ -45,7 +45,7 @@ body:
|
||||
label: LNReader version
|
||||
description: You can find your LNReader version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "1.1.12"
|
||||
Example: "1.1.14"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -86,9 +86,9 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.1.12](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.1.14](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -34,7 +34,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/LNReader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.1.12](https://github.com/LNReader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.1.14](https://github.com/LNReader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -33,3 +33,14 @@ jobs:
|
||||
- name: Build Android Release
|
||||
run: |
|
||||
cd android && ./gradlew assembleRelease --no-daemon
|
||||
|
||||
- name: Set Env
|
||||
run: |
|
||||
set -x
|
||||
echo "COMMIT_COUNT=$(git rev-list --count HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: LNReader-r${{ env.COMMIT_COUNT }}
|
||||
path: android/app/build/outputs/apk/release/app-release.apk
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import { enableFreeze } from 'react-native-screens';
|
||||
|
||||
enableFreeze(true);
|
||||
@@ -9,30 +10,36 @@ 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';
|
||||
|
||||
import { createDatabase } from './src/database/db';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import LottieSplashScreen from 'react-native-lottie-splash-screen';
|
||||
import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
|
||||
const App = () => {
|
||||
useEffect(() => {
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
LottieSplashScreen.hide();
|
||||
createDatabase();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<SafeAreaProvider>
|
||||
<PaperProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<Main />
|
||||
</PaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<AppErrorBoundary>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<SafeAreaProvider>
|
||||
<PaperProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<Main />
|
||||
</PaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</AppErrorBoundary>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -122,6 +122,12 @@ def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
*/
|
||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
|
||||
ext {
|
||||
versionMajor = 1
|
||||
versionMinor = 1
|
||||
versionPatch = 14
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
@@ -134,8 +140,9 @@ android {
|
||||
applicationId 'com.rajarsheechatterjee.LNReader'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.1.12"
|
||||
// Generated version code. Supports versions up to 1024.1024.2048
|
||||
versionCode ((((versionMajor << 10) | versionMinor) << 11) | versionPatch)
|
||||
versionName "$versionMajor.$versionMinor.$versionPatch"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
@@ -187,7 +194,7 @@ dependencies {
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
implementation project(':react-native-lottie-splash-screen')
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
-keep class com.swmansion.reanimated.** { *; }
|
||||
-keep class com.facebook.react.turbomodule.** { *; }
|
||||
-keep class com.facebook.hermes.unicode.** { *; }
|
||||
-keep class com.facebook.jni.** { *; }
|
||||
-keep public class com.dylanvann.fastimage.* {*;}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.rajarsheechatterjee.LNReader">
|
||||
<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"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_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.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"/>
|
||||
<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" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -28,24 +28,23 @@
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:largeHeap="true"
|
||||
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.notifications.default_notification_icon"
|
||||
android:resource="@drawable/notification_icon"/>
|
||||
|
||||
android:resource="@drawable/notification_icon" />
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.notifications.default_notification_color"
|
||||
android:resource="@color/notification_icon_color"/>
|
||||
|
||||
android:resource="@color/notification_icon_color" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
@@ -54,24 +53,24 @@
|
||||
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>
|
||||
<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>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,15 +1,14 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.content.Intent;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.WindowManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
@@ -17,9 +16,7 @@ import com.facebook.react.ReactRootView;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListener;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
|
||||
import expo.modules.splashscreen.singletons.SplashScreen;
|
||||
import expo.modules.splashscreen.SplashScreenImageResizeMode;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreen;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
@@ -35,7 +32,8 @@ public class MainActivity extends ReactActivity {
|
||||
@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
|
||||
// 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;
|
||||
@@ -45,11 +43,11 @@ public class MainActivity extends ReactActivity {
|
||||
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);
|
||||
|
||||
w.getDecorView()
|
||||
.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
SplashScreen.show(this,R.style.SplashScreenTheme, R.id.lottie);
|
||||
SplashScreen.setAnimationFinished(true);
|
||||
super.onCreate(null);
|
||||
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,7 +67,7 @@ public class MainActivity extends ReactActivity {
|
||||
return true;
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
//voldown
|
||||
// voldown
|
||||
VolumeButtonListener.down();
|
||||
if (!VolumeButtonListener.prevent)
|
||||
return super.dispatchKeyEvent(event);
|
||||
|
||||
@@ -21,10 +21,11 @@ import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreenReactPackage;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(
|
||||
new BasePackageList().getPackageList()
|
||||
);
|
||||
new BasePackageList().getPackageList());
|
||||
|
||||
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
|
||||
@Override
|
||||
@@ -66,7 +67,8 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
|
||||
* Loads Flipper in React Native templates. Call this in the onCreate method
|
||||
* with something like
|
||||
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
*
|
||||
* @param context
|
||||
@@ -77,9 +79,9 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
if (BuildConfig.DEBUG) {
|
||||
try {
|
||||
/*
|
||||
We use reflection here to pick up the class that initializes Flipper,
|
||||
since Flipper library is not available in release mode
|
||||
*/
|
||||
* We use reflection here to pick up the class that initializes Flipper,
|
||||
* since Flipper library is not available in release mode
|
||||
*/
|
||||
Class<?> aClass = Class.forName("com.rajarsheechatterjee.LNReader.ReactNativeFlipper");
|
||||
aClass
|
||||
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
|
||||
|
||||
@@ -20,7 +20,6 @@ public class BasePackageList {
|
||||
new expo.modules.localization.LocalizationPackage(),
|
||||
new expo.modules.notifications.NotificationsPackage(),
|
||||
new expo.modules.permissions.PermissionsPackage(),
|
||||
new expo.modules.splashscreen.SplashScreenPackage(),
|
||||
new expo.modules.sqlite.SQLitePackage(),
|
||||
new expo.modules.webbrowser.WebBrowserPackage()
|
||||
);
|
||||
|
||||
|
After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:background="@color/colorPrimaryDark"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:scaleType="centerCrop"
|
||||
android:navigationBarColor="#333333"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottie"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="25dp"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_loop="false"
|
||||
app:lottie_rawRes="@raw/loading" />
|
||||
</LinearLayout>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/iconBackground"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@mipmap/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/iconBackground"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 858 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 876 B After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 464 B |
|
Before Width: | Height: | Size: 876 B After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
@@ -6,4 +6,5 @@
|
||||
<color name="colorPrimaryDark">#202125</color>
|
||||
<color name="navigationBarColor">#202125</color>
|
||||
<color name="notification_icon_color">#00adb5</color>
|
||||
<color name="ic_launcher_background">#222831</color>
|
||||
</resources>
|
||||
@@ -8,16 +8,20 @@
|
||||
<item name="android:textColor">#000000</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="android:navigationBarColor">#202125</item>
|
||||
<item name="android:statusBarColor">#202125</item>
|
||||
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<style name="SplashScreenTheme" parent="SplashScreen_SplashTheme">
|
||||
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||
<!-- Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually -->
|
||||
<!-- Customize your splash screen theme here -->
|
||||
<!-- <item name="icon">@drawable/no_icon</item> -->
|
||||
<item name="android:windowBackground">@drawable/splashscreen</item>
|
||||
<item name="android:navigationBarColor">#202125</item>
|
||||
<item name="android:statusBarColor">#202125</item>
|
||||
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -22,6 +22,20 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
// We get React Native's Android binaries exclusively through npm,
|
||||
// from a local Maven repo inside node_modules/react-native/.
|
||||
// (The use of exclusiveContent prevents looking elsewhere like Maven Central
|
||||
// and potentially getting a wrong version.)
|
||||
filter {
|
||||
includeGroup "com.facebook.react"
|
||||
}
|
||||
forRepository {
|
||||
maven {
|
||||
url "$rootDir/../node_modules/react-native/android"
|
||||
}
|
||||
}
|
||||
}
|
||||
mavenLocal()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
|
||||
@@ -6,4 +6,7 @@ includeUnimodulesProjects()
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle");
|
||||
applyNativeModulesSettingsGradle(settings)
|
||||
|
||||
include ':react-native-lottie-splash-screen'
|
||||
project(':react-native-lottie-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-lottie-splash-screen/android')
|
||||
|
||||
include ':app'
|
||||
|
||||
@@ -20,6 +20,7 @@ module.exports = function (api) {
|
||||
},
|
||||
},
|
||||
],
|
||||
'react-native-reanimated/plugin',
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lnreader",
|
||||
"version": "1.1.12",
|
||||
"version": "1.1.14",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
@@ -8,18 +8,22 @@
|
||||
"start": "react-native start",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,ts,tsx}\"",
|
||||
"buildRelease": "cd android && gradlew clean && gradlew assembleRelease",
|
||||
"buildRelease": "cd android && ./gradlew clean && ./gradlew assembleRelease",
|
||||
"open": "open ./android/app/build/outputs/apk/release/",
|
||||
"sources:madara": "node scripts/generateMadaraSource.cjs",
|
||||
"source:generator": "node scripts/generateSource.cjs",
|
||||
"strings": "node scripts/stringTypes.cjs",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gorhom/bottom-sheet": "^4.4.5",
|
||||
"@react-native-async-storage/async-storage": "^1.15.11",
|
||||
"@react-native-community/clipboard": "^1.5.1",
|
||||
"@react-native-community/masked-view": "^0.1.11",
|
||||
"@react-native-community/slider": "^4.1.12",
|
||||
"@react-native-cookies/cookies": "^6.2.1",
|
||||
"@react-native-picker/picker": "^2.4.1",
|
||||
"@react-navigation/drawer": "^6.0.1",
|
||||
"@react-navigation/material-bottom-tabs": "^6.0.9",
|
||||
"@react-navigation/native": "^6.0.6",
|
||||
"@react-navigation/stack": "^6.0.11",
|
||||
@@ -38,29 +42,32 @@
|
||||
"expo-linking": "~2.2.3",
|
||||
"expo-localization": "~10.1.0",
|
||||
"expo-notifications": "~0.11.6",
|
||||
"expo-splash-screen": "^0.11.4",
|
||||
"expo-sqlite": "~9.1.0",
|
||||
"expo-web-browser": "~9.1.0",
|
||||
"i18n-js": "^3.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lottie-react-native": "^5.1.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.10.1",
|
||||
"qs": "^6.10.3",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "^0.64.3",
|
||||
"react-native-background-actions": "^2.6.5",
|
||||
"react-native-device-info": "^8.4.7",
|
||||
"react-native-easing-gradient": "^1.0.0",
|
||||
"react-native-error-boundary": "^1.1.16",
|
||||
"react-native-fast-image": "^8.5.11",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-gesture-handler": "~2.1.3",
|
||||
"react-native-lottie-splash-screen": "^1.0.1",
|
||||
"react-native-markdown-display": "^7.0.0-alpha.2",
|
||||
"react-native-mmkv": "^2.4.3",
|
||||
"react-native-pager-view": "^5.4.9",
|
||||
"react-native-paper": "^5.0.0-rc.4",
|
||||
"react-native-reanimated": "^2.2.4",
|
||||
"react-native-paper": "^5.1.2",
|
||||
"react-native-reanimated": "^2.13.0",
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"react-native-safe-area-context": "^3.2.0",
|
||||
"react-native-screens": "^3.9.0",
|
||||
"react-native-shimmer-placeholder": "^2.0.9",
|
||||
"react-native-swipe-gestures": "^1.0.5",
|
||||
"react-native-tab-view": "^3.1.1",
|
||||
"react-native-tts": "^4.1.0",
|
||||
@@ -71,7 +78,6 @@
|
||||
"redux": "^4.1.2",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"rn-sliding-up-panel": "^2.4.4",
|
||||
"sanitize-html": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text, StatusBar } from 'react-native';
|
||||
import ErrorBoundary from 'react-native-error-boundary';
|
||||
|
||||
import { Button, List } from '@components';
|
||||
import { useTheme } from '@hooks';
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
|
||||
interface ErrorFallbackProps {
|
||||
error: Error;
|
||||
resetError: () => void;
|
||||
}
|
||||
|
||||
export const ErrorFallback: React.FC<ErrorFallbackProps> = ({
|
||||
error,
|
||||
resetError,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View style={[styles.mainCtn, { backgroundColor: theme.background }]}>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<View style={styles.errorInfoCtn}>
|
||||
<Text style={[styles.errorTitle, { color: theme.onSurface }]}>
|
||||
An Unexpected Error Ocurred
|
||||
</Text>
|
||||
<Text style={[styles.errorDesc, { color: theme.onSurface }]}>
|
||||
The application ran into an unexpected error. We suggest you
|
||||
screenshot this message and then share it in our support channel on
|
||||
Discord.
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.errorCtn,
|
||||
{
|
||||
backgroundColor: theme.surfaceVariant,
|
||||
color: theme.onSurfaceVariant,
|
||||
},
|
||||
]}
|
||||
numberOfLines={20}
|
||||
>
|
||||
{`${error.message}\n\n${error.stack}`}
|
||||
</Text>
|
||||
</View>
|
||||
<List.Divider theme={theme} />
|
||||
<Button
|
||||
variation={ButtonVariation.DEFAULT}
|
||||
onPress={resetError}
|
||||
title={'Restart the application'}
|
||||
style={styles.buttonCtn}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface AppErrorBoundaryProps {
|
||||
children: React.ReactNode | React.ReactElement;
|
||||
}
|
||||
|
||||
const AppErrorBoundary: React.FC<AppErrorBoundaryProps> = ({ children }) => {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>{children}</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppErrorBoundary;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
mainCtn: {
|
||||
flex: 1,
|
||||
},
|
||||
errorInfoCtn: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
buttonCtn: {
|
||||
margin: 16,
|
||||
marginBottom: 32,
|
||||
},
|
||||
errorTitle: {
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
fontSize: 20,
|
||||
},
|
||||
errorDesc: {
|
||||
lineHeight: 20,
|
||||
marginVertical: 8,
|
||||
},
|
||||
errorCtn: {
|
||||
borderRadius: 8,
|
||||
lineHeight: 20,
|
||||
marginVertical: 16,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { ReactElement, Ref, useCallback } from 'react';
|
||||
import { overlay } from 'react-native-paper';
|
||||
import { default as BS, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
interface BottomSheetProps {
|
||||
bottomSheetRef: Ref<BS>;
|
||||
snapPoints: Array<number>;
|
||||
height: number;
|
||||
theme: MD3ThemeType;
|
||||
children: ReactElement;
|
||||
}
|
||||
|
||||
const BottomSheet: React.FC<BottomSheetProps> = ({
|
||||
bottomSheetRef,
|
||||
snapPoints,
|
||||
height,
|
||||
theme,
|
||||
children,
|
||||
}) => {
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
const renderBackdrop = useCallback(
|
||||
props => <BottomSheetBackdrop {...props} disappearsOnIndex={0} />,
|
||||
[],
|
||||
);
|
||||
return (
|
||||
<BS
|
||||
index={-1}
|
||||
backdropComponent={renderBackdrop}
|
||||
snapPoints={[0.1].concat(snapPoints)}
|
||||
enablePanDownToClose={true}
|
||||
ref={bottomSheetRef}
|
||||
handleStyle={styles.handle}
|
||||
containerHeight={height + bottom}
|
||||
containerStyle={{ paddingBottom: bottom }}
|
||||
backgroundStyle={{ backgroundColor: overlay(2, theme.surface) }}
|
||||
>
|
||||
{children}
|
||||
</BS>
|
||||
);
|
||||
};
|
||||
const styles = StyleSheet.create({
|
||||
handle: {
|
||||
display: 'none',
|
||||
},
|
||||
});
|
||||
export default BottomSheet;
|
||||
@@ -18,7 +18,7 @@ const ScreenContainer = ({ children, theme }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Row = ({ children, style }) => (
|
||||
const Row = ({ children, style = {} }) => (
|
||||
<View style={[styles.row, style]}>{children}</View>
|
||||
);
|
||||
|
||||
|
||||
@@ -28,24 +28,26 @@ const ListItem = ({
|
||||
}) => (
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
style={style}
|
||||
style={[{ paddingHorizontal: 16, paddingVertical: 12 }, style]}
|
||||
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
|
||||
description={description}
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
descriptionNumberOfLines={1}
|
||||
left={() => (
|
||||
<View style={{ justifyContent: 'center' }}>
|
||||
{icon && (
|
||||
<PaperList.Icon
|
||||
color={theme.primary}
|
||||
icon={icon}
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
left={
|
||||
icon
|
||||
? () => (
|
||||
<View style={{ justifyContent: 'center' }}>
|
||||
<PaperList.Icon
|
||||
color={theme.primary}
|
||||
icon={icon}
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
right={() =>
|
||||
right && (
|
||||
<MaterialCommunityIcons
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import color from 'color';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
@@ -66,21 +67,23 @@ const Divider = ({ theme }: { theme: MD3ThemeType }) => (
|
||||
|
||||
const InfoItem = ({
|
||||
title,
|
||||
icon,
|
||||
icon = 'information-outline',
|
||||
theme,
|
||||
}: {
|
||||
title: string;
|
||||
icon: string;
|
||||
theme: MD3ThemeType;
|
||||
}) => (
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
titleStyle={[{ color: theme.textColorSecondary }, styles.infoItem]}
|
||||
titleNumberOfLines={5}
|
||||
left={() =>
|
||||
icon && <PaperList.Icon color={theme.textColorSecondary} icon={icon} />
|
||||
}
|
||||
/>
|
||||
<View style={styles.infoCtn}>
|
||||
<MaterialIcon size={20} color={theme.onSurfaceVariant} name={icon} />
|
||||
<Text style={[styles.infoMsg, { color: theme.onSurfaceVariant }]}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const Icon = ({ icon, theme }: { icon: string; theme: MD3ThemeType }) => (
|
||||
<PaperList.Icon color={theme.primary} icon={icon} style={{ margin: 0 }} />
|
||||
);
|
||||
|
||||
export default {
|
||||
@@ -89,6 +92,7 @@ export default {
|
||||
Item,
|
||||
Divider,
|
||||
InfoItem,
|
||||
Icon,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -105,7 +109,12 @@ const styles = StyleSheet.create({
|
||||
icon: {
|
||||
marginVertical: 0,
|
||||
},
|
||||
infoItem: {
|
||||
fontSize: 14,
|
||||
infoCtn: {
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
infoMsg: {
|
||||
marginTop: 12,
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,8 +40,10 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
const searchbarRef = useRef<any>(null);
|
||||
const focusSearchbar = () => searchbarRef.current.focus();
|
||||
|
||||
const { top } = useSafeAreaInsets();
|
||||
const { top, right, left } = useSafeAreaInsets();
|
||||
const marginTop = top + 8;
|
||||
const marginRight = right + 16;
|
||||
const marginLeft = left + 16;
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -49,6 +51,8 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
styles.searchbarContainer,
|
||||
{
|
||||
marginTop,
|
||||
marginRight,
|
||||
marginLeft,
|
||||
backgroundColor: color(theme.primary).alpha(0.08).string(),
|
||||
},
|
||||
]}
|
||||
@@ -108,7 +112,7 @@ const styles = StyleSheet.create({
|
||||
searchbarContainer: {
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
minHeight: 46,
|
||||
minHeight: 56,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
zIndex: 1,
|
||||
|
||||
@@ -9,44 +9,33 @@ import { txnErrorCallback } from '@database/utils/helpers';
|
||||
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const insertChaptersQuery =
|
||||
'INSERT INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)';
|
||||
const insertChaptersQuery = `
|
||||
INSERT INTO chapters (
|
||||
chapterUrl, chapterName, releaseDate,
|
||||
novelId
|
||||
)
|
||||
values
|
||||
(?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
export const insertChapters = async (
|
||||
novelId: number,
|
||||
chapters: ChapterItem[],
|
||||
chapters?: ChapterItem[],
|
||||
) => {
|
||||
db.transaction(
|
||||
tx => {
|
||||
chapters.map(chapter =>
|
||||
tx.executeSql(
|
||||
insertChaptersQuery,
|
||||
[
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.releaseDate,
|
||||
novelId,
|
||||
],
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => false,
|
||||
// console.log(
|
||||
// "Error ",
|
||||
// error,
|
||||
// " Chapter URL: ",
|
||||
// chapter.chapterUrl,
|
||||
// " Novel ID ",
|
||||
// novelId
|
||||
// )
|
||||
),
|
||||
);
|
||||
},
|
||||
_err => {
|
||||
// console.log('Error', err);
|
||||
},
|
||||
() => {
|
||||
// console.log('Success ')
|
||||
},
|
||||
);
|
||||
if (!chapters?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
db.transaction(tx => {
|
||||
chapters.map(chapter =>
|
||||
tx.executeSql(insertChaptersQuery, [
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.releaseDate,
|
||||
novelId,
|
||||
]),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getChaptersQuery = (sort = 'ORDER BY chapterId ASC', filter = '') =>
|
||||
@@ -62,10 +51,7 @@ export const getChapters = (novelId: number, sort: string, filter: string) => {
|
||||
const _array = (rows as any)._array;
|
||||
resolve(_array);
|
||||
},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -261,7 +247,10 @@ const createImageFolder = async (
|
||||
await RNFetchBlob.fs.mkdir(p);
|
||||
}
|
||||
};
|
||||
|
||||
await mkdirIfNot(path);
|
||||
await RNFetchBlob.fs.createFile(path, '.nomedia', 'utf8');
|
||||
|
||||
if (data) {
|
||||
const { sourceId, novelId, chapterId } = data;
|
||||
await mkdirIfNot(`${path}/${sourceId}/`);
|
||||
@@ -378,9 +367,7 @@ const deleteDownloadedImages = async (
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const deleteChapter = async (
|
||||
@@ -418,6 +405,38 @@ export const deleteChapter = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteChapters = async (
|
||||
sourceId: number,
|
||||
chapters?: ChapterItem[],
|
||||
) => {
|
||||
if (!chapters?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chapterIdsString = chapters
|
||||
?.map(chapter => chapter.chapterId)
|
||||
.toString();
|
||||
|
||||
const updateIsDownloadedQuery = `UPDATE chapters SET downloaded = 0 WHERE chapterId IN (${chapterIdsString})`;
|
||||
const deleteChapterQuery = `DELETE FROM downloads WHERE downloadChapterId IN (${chapterIdsString})`;
|
||||
|
||||
await Promise.all(
|
||||
chapters?.map(chapter =>
|
||||
deleteDownloadedImages(sourceId, chapter.novelId, chapter.chapterId),
|
||||
),
|
||||
);
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
updateIsDownloadedQuery,
|
||||
undefined,
|
||||
undefined,
|
||||
txnErrorCallback,
|
||||
);
|
||||
tx.executeSql(deleteChapterQuery, undefined, undefined, txnErrorCallback);
|
||||
});
|
||||
};
|
||||
|
||||
const getLastReadChapterQuery = `
|
||||
SELECT chapters.*
|
||||
FROM history
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import { txnErrorCallback } from '@database/utils/helpers';
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import { sourceManager } from '../../sources/sourceManager';
|
||||
@@ -36,7 +37,7 @@ export const fetchAndInsertChapterInDb = async (
|
||||
downloadChapterQuery,
|
||||
[chapterId, chapter.chapterName, chapter.chapterText],
|
||||
(txObj, res) => {},
|
||||
(_txObj, error) => showToast(error),
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -55,7 +56,7 @@ export const deleteChapterFromDb = (chapterId: number) => {
|
||||
deleteChapterFromDbQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {},
|
||||
(_txObj, error) => showToast(error.message),
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -122,13 +123,13 @@ export const deleteReadChaptersFromDb = (chapterId: number) => {
|
||||
updateChaptersDeletedQuery,
|
||||
[],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => showToast(error.message),
|
||||
txnErrorCallback,
|
||||
);
|
||||
tx.executeSql(
|
||||
deleteReadChaptersFromDbQuery,
|
||||
[],
|
||||
(txObj, res) => showToast('Deleted read chapters.'),
|
||||
(txObj, error) => showToast(error.message),
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -143,7 +143,7 @@ export const restoreLibrary = async novel => {
|
||||
async (txObj, { insertId }) => {
|
||||
const chapters = await fetchChapters(novel.sourceId, novel.novelUrl);
|
||||
|
||||
if (chapters) {
|
||||
if (chapters.length) {
|
||||
await insertChapters(insertId, chapters);
|
||||
|
||||
resolve();
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { countBy } from 'lodash';
|
||||
import { LibraryStats } from '../types';
|
||||
import { txnErrorCallback } from '../utils/helpers';
|
||||
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getLibraryStatsQuery = `
|
||||
SELECT COUNT(*) as novelsCount, COUNT(DISTINCT sourceId) as sourcesCount
|
||||
FROM novels
|
||||
WHERE novels.followed = 1
|
||||
`;
|
||||
|
||||
const getChaptersReadCountQuery = `
|
||||
SELECT COUNT(*) as chaptersRead
|
||||
FROM chapters
|
||||
JOIN novels
|
||||
ON chapters.novelId = novels.novelId
|
||||
WHERE chapters.read = 1 AND novels.followed = 1
|
||||
`;
|
||||
|
||||
const getChaptersTotalCountQuery = `
|
||||
SELECT COUNT(*) as chaptersCount
|
||||
FROM chapters
|
||||
JOIN novels
|
||||
ON chapters.novelId = novels.novelId
|
||||
WHERE novels.followed = 1
|
||||
`;
|
||||
|
||||
const getChaptersUnreadCountQuery = `
|
||||
SELECT COUNT(*) as chaptersUnread
|
||||
FROM chapters
|
||||
JOIN novels
|
||||
ON chapters.novelId = novels.novelId
|
||||
WHERE chapters.read = 0 AND novels.followed = 1
|
||||
`;
|
||||
|
||||
const getChaptersDownloadedCountQuery = `
|
||||
SELECT COUNT(*) as chaptersDownloaded
|
||||
FROM chapters
|
||||
JOIN novels
|
||||
ON chapters.novelId = novels.novelId
|
||||
WHERE chapters.downloaded = 1 AND novels.followed = 1
|
||||
`;
|
||||
|
||||
const getNovelGenresQuery = `
|
||||
SELECT genre
|
||||
FROM novels
|
||||
WHERE novels.followed = 1
|
||||
`;
|
||||
|
||||
const getNovelStatusQuery = `
|
||||
SELECT status
|
||||
FROM novels
|
||||
WHERE novels.followed = 1
|
||||
`;
|
||||
|
||||
export const getLibraryStatsFromDb = async (): Promise<LibraryStats> => {
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getLibraryStatsQuery,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array[0]);
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getChaptersTotalCountFromDb = async (): Promise<LibraryStats> => {
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChaptersTotalCountQuery,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array[0]);
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getChaptersReadCountFromDb = async (): Promise<LibraryStats> => {
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChaptersReadCountQuery,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array[0]);
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getChaptersUnreadCountFromDb = async (): Promise<LibraryStats> => {
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChaptersUnreadCountQuery,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array[0]);
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getChaptersDownloadedCountFromDb =
|
||||
async (): Promise<LibraryStats> => {
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChaptersDownloadedCountQuery,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array[0]);
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getNovelGenresFromDb = async (): Promise<LibraryStats> => {
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getNovelGenresQuery,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
let genres: string[] = [];
|
||||
|
||||
_array.forEach((item: { genre: string }) => {
|
||||
const novelGenres = item.genre?.split(/\s*,\s*/);
|
||||
|
||||
if (novelGenres?.length) {
|
||||
genres.push(...novelGenres);
|
||||
}
|
||||
});
|
||||
|
||||
resolve({ genres: countBy(genres) });
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getNovelStatusFromDb = async (): Promise<LibraryStats> => {
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getNovelStatusQuery,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
let status: string[] = [];
|
||||
|
||||
_array.forEach((item: { status: string }) => {
|
||||
status.push(item.status);
|
||||
});
|
||||
|
||||
resolve({ status: countBy(status) });
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -35,7 +35,7 @@ const getUpdatesFromDbQuery = `
|
||||
WHERE
|
||||
date(updates.updateTime) > date('now','-3 months')
|
||||
ORDER BY
|
||||
updates.updateTime DESC, chapters.releaseDate DESC
|
||||
updates.updateTime DESC, chapters.chapterName DESC, chapters.releaseDate DESC
|
||||
`;
|
||||
|
||||
export const getUpdatesFromDb = async (): Promise<Update[]> => {
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface NovelInfo {
|
||||
author?: string;
|
||||
status?: string;
|
||||
followed: number;
|
||||
categoryIds: string;
|
||||
}
|
||||
|
||||
export interface LibraryNovelInfo extends NovelInfo {
|
||||
@@ -74,3 +75,14 @@ export interface Category {
|
||||
lastUpdatedAt: Date;
|
||||
sort: number | null;
|
||||
}
|
||||
|
||||
export interface LibraryStats {
|
||||
novelsCount?: number;
|
||||
chaptersCount?: number;
|
||||
chaptersRead?: number;
|
||||
chaptersUnread?: number;
|
||||
chaptersDownloaded?: number;
|
||||
sourcesCount?: number;
|
||||
genres?: Record<string, number>;
|
||||
status?: Record<string, number>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useMMKVObject } from 'react-native-mmkv';
|
||||
import { MMKVStorage } from '@utils/mmkv/mmkv';
|
||||
import { showToast } from './showToast';
|
||||
|
||||
export const SOURCE_STORAGE = 'SOURCE_STORAGE';
|
||||
|
||||
interface SourceStorage {
|
||||
cookies?: any;
|
||||
}
|
||||
|
||||
export type SourceStorageMap = Record<number, Partial<SourceStorage>>;
|
||||
|
||||
const useSourceStorage = ({ sourceId = -1 }: { sourceId?: number }) => {
|
||||
const [values, setValues] = useMMKVObject<SourceStorageMap>(
|
||||
SOURCE_STORAGE,
|
||||
MMKVStorage,
|
||||
);
|
||||
|
||||
const setSourceStorage = (key: keyof SourceStorage, value: any): void => {
|
||||
setValues({
|
||||
...values,
|
||||
[sourceId]: {
|
||||
...values?.[sourceId],
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const clearCookies = (): void => {
|
||||
// let tempStorage = {};
|
||||
|
||||
// if (values) {
|
||||
// tempStorage = Object.fromEntries(
|
||||
// Object.entries(values).map(([id, sourceData]) => {
|
||||
// delete sourceData.cookies;
|
||||
|
||||
// return [id, sourceData];
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
setValues({});
|
||||
showToast('Cookies cleared');
|
||||
};
|
||||
|
||||
const sourceStorage = values?.[sourceId];
|
||||
|
||||
return {
|
||||
...sourceStorage,
|
||||
setSourceStorage,
|
||||
clearCookies,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSourceStorage;
|
||||
|
||||
export const getSourceStorage = (sourceId: number) => {
|
||||
const rawSettings = MMKVStorage.getString(SOURCE_STORAGE) || '{}';
|
||||
const parsedSettings: Partial<SourceStorageMap> = JSON.parse(rawSettings);
|
||||
|
||||
return parsedSettings[sourceId] || {};
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import Tts from 'react-native-tts';
|
||||
import { htmlToText } from '../sources/helpers/htmlToText';
|
||||
|
||||
export const useTextToSpeech = html => {
|
||||
export const useTextToSpeech = (html, markChapterAdRead) => {
|
||||
const [ttsStatus, setTtsStatus] = useState();
|
||||
const [ttsPosition, setTtsPosition] = useState({ end: 0 });
|
||||
|
||||
@@ -14,7 +14,10 @@ export const useTextToSpeech = html => {
|
||||
setTtsStatus('progress');
|
||||
setTtsPosition(event);
|
||||
});
|
||||
Tts.addEventListener('tts-finish', () => setTtsStatus('finish'));
|
||||
Tts.addEventListener('tts-finish', () => {
|
||||
setTtsStatus('finish');
|
||||
markChapterAdRead();
|
||||
});
|
||||
Tts.addEventListener('tts-cancel', () => setTtsStatus('paused'));
|
||||
|
||||
return () => Tts.stop();
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import { SafeAreaProviderCompat } from '@react-navigation/elements';
|
||||
import {
|
||||
MaterialBottomTabDescriptorMap,
|
||||
MaterialBottomTabNavigationConfig,
|
||||
MaterialBottomTabNavigationHelpers,
|
||||
} from '@react-navigation/material-bottom-tabs/lib/typescript/src/types';
|
||||
import {
|
||||
CommonActions,
|
||||
Link,
|
||||
ParamListBase,
|
||||
Route,
|
||||
TabNavigationState,
|
||||
useLinkBuilder,
|
||||
} from '@react-navigation/native';
|
||||
import * as React from 'react';
|
||||
import { Platform, StyleSheet, Text } from 'react-native';
|
||||
import { BottomNavigation } from 'react-native-paper';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
type Props = MaterialBottomTabNavigationConfig & {
|
||||
state: TabNavigationState<ParamListBase>;
|
||||
navigation: MaterialBottomTabNavigationHelpers;
|
||||
descriptors: MaterialBottomTabDescriptorMap;
|
||||
};
|
||||
|
||||
type Scene = { route: { key: string } };
|
||||
|
||||
// Optionally require vector-icons referenced from react-native-paper:
|
||||
// https://github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx#L14
|
||||
let MaterialCommunityIcons: React.ComponentType<
|
||||
React.ComponentProps<
|
||||
typeof import('react-native-vector-icons/MaterialCommunityIcons').default
|
||||
>
|
||||
>;
|
||||
|
||||
try {
|
||||
// Optionally require vector-icons
|
||||
MaterialCommunityIcons =
|
||||
require('react-native-vector-icons/MaterialCommunityIcons').default;
|
||||
} catch (e: any) {
|
||||
let isErrorLogged = false;
|
||||
|
||||
// Fallback component for icons
|
||||
MaterialCommunityIcons = ({
|
||||
name,
|
||||
color,
|
||||
size,
|
||||
selectionColor: _0,
|
||||
onLayout: _1,
|
||||
...rest
|
||||
}) => {
|
||||
if (!isErrorLogged) {
|
||||
if (
|
||||
!/(Cannot find module|Module not found|Cannot resolve module)/.test(
|
||||
e.message,
|
||||
)
|
||||
) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Tried to use the icon '${name}' in a component from '@react-navigation/material-bottom-tabs', but 'react-native-vector-icons/MaterialCommunityIcons' could not be loaded.`,
|
||||
"To remove this warning, try installing 'react-native-vector-icons' or use another method to specify icon: https://reactnavigation.org/docs/material-bottom-tab-navigator/#tabbaricon.",
|
||||
);
|
||||
|
||||
isErrorLogged = true;
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-expect-error: we're passing icon props to text, but we don't care about it since it's just fallback
|
||||
<Text
|
||||
{...rest}
|
||||
style={[
|
||||
styles.icon,
|
||||
{
|
||||
color: typeof color !== 'number' ? color : undefined,
|
||||
fontSize: size,
|
||||
},
|
||||
]}
|
||||
>
|
||||
□
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function MaterialBottomTabViewInner({
|
||||
state,
|
||||
navigation,
|
||||
descriptors,
|
||||
...rest
|
||||
}: Props) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const buildLink = useLinkBuilder();
|
||||
|
||||
return (
|
||||
<BottomNavigation
|
||||
{...rest}
|
||||
navigationState={state}
|
||||
onIndexChange={(index: number) =>
|
||||
navigation.dispatch({
|
||||
...CommonActions.navigate({
|
||||
name: state.routes[index].name,
|
||||
merge: true,
|
||||
}),
|
||||
target: state.key,
|
||||
})
|
||||
}
|
||||
renderScene={({ route }) => descriptors[route.key].render()}
|
||||
renderTouchable={
|
||||
Platform.OS === 'web'
|
||||
? ({
|
||||
onPress,
|
||||
route,
|
||||
accessibilityRole: _0,
|
||||
borderless: _1,
|
||||
centered: _2,
|
||||
rippleColor: _3,
|
||||
style,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<Link
|
||||
{...rest}
|
||||
// @ts-expect-error: to could be undefined, but it doesn't affect functionality
|
||||
to={buildLink(route.name, route.params)}
|
||||
accessibilityRole="link"
|
||||
onPress={(e: any) => {
|
||||
if (
|
||||
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
|
||||
(e.button == null || e.button === 0) // ignore everything but left clicks
|
||||
) {
|
||||
e.preventDefault();
|
||||
onPress?.(e);
|
||||
}
|
||||
}}
|
||||
style={[styles.touchable, style]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
renderIcon={({ route, focused, color }) => {
|
||||
const { options } = descriptors[route.key];
|
||||
|
||||
if (typeof options.tabBarIcon === 'string') {
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
name={options.tabBarIcon}
|
||||
color={color}
|
||||
size={24}
|
||||
style={styles.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof options.tabBarIcon === 'function') {
|
||||
return options.tabBarIcon({ focused, color });
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
getLabelText={({ route }: Scene) => {
|
||||
const { options } = descriptors[route.key];
|
||||
|
||||
return options.tabBarLabel !== undefined
|
||||
? options.tabBarLabel
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: (route as Route<string>).name;
|
||||
}}
|
||||
getColor={({ route }) => descriptors[route.key].options.tabBarColor}
|
||||
getBadge={({ route }) => descriptors[route.key].options.tabBarBadge}
|
||||
getAccessibilityLabel={({ route }) =>
|
||||
descriptors[route.key].options.tabBarAccessibilityLabel
|
||||
}
|
||||
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
|
||||
onTabPress={({ route, preventDefault }) => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
preventDefault();
|
||||
}
|
||||
}}
|
||||
safeAreaInsets={insets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MaterialBottomTabView(props: Props) {
|
||||
return (
|
||||
<SafeAreaProviderCompat>
|
||||
<MaterialBottomTabViewInner {...props} />
|
||||
</SafeAreaProviderCompat>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
touchable: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import {
|
||||
createNavigatorFactory,
|
||||
DefaultNavigatorOptions,
|
||||
ParamListBase,
|
||||
TabActionHelpers,
|
||||
TabNavigationState,
|
||||
TabRouter,
|
||||
TabRouterOptions,
|
||||
useNavigationBuilder,
|
||||
} from '@react-navigation/native';
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
MaterialBottomTabNavigationConfig,
|
||||
MaterialBottomTabNavigationEventMap,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
} from '@react-navigation/material-bottom-tabs/lib/typescript/src/types';
|
||||
import MaterialBottomTabView from './MaterialBottomTabView';
|
||||
|
||||
type Props = DefaultNavigatorOptions<
|
||||
ParamListBase,
|
||||
TabNavigationState<ParamListBase>,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationEventMap
|
||||
> &
|
||||
TabRouterOptions &
|
||||
MaterialBottomTabNavigationConfig;
|
||||
|
||||
function MaterialBottomTabNavigator({
|
||||
id,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenListeners,
|
||||
screenOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { state, descriptors, navigation, NavigationContent } =
|
||||
useNavigationBuilder<
|
||||
TabNavigationState<ParamListBase>,
|
||||
TabRouterOptions,
|
||||
TabActionHelpers<ParamListBase>,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationEventMap
|
||||
>(TabRouter, {
|
||||
id,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenListeners,
|
||||
screenOptions,
|
||||
});
|
||||
|
||||
return (
|
||||
<NavigationContent>
|
||||
<MaterialBottomTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
</NavigationContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default createNavigatorFactory<
|
||||
TabNavigationState<ParamListBase>,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationEventMap,
|
||||
typeof MaterialBottomTabNavigator
|
||||
>(MaterialBottomTabNavigator);
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import Color from 'color';
|
||||
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
|
||||
|
||||
import Library from '../screens/library/LibraryScreen';
|
||||
import Updates from '../screens/updates/UpdatesScreen';
|
||||
@@ -10,9 +10,7 @@ import More from '../screens/more/MoreScreen';
|
||||
|
||||
import { useSettings } from '../hooks/reduxHooks';
|
||||
import { getString } from '../../strings/translations';
|
||||
import createMaterialBottomTabNavigator from './BottomNavigation/createMaterialBottomTabNavigator';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import Color from 'color';
|
||||
|
||||
const Tab = createMaterialBottomTabNavigator();
|
||||
|
||||
@@ -27,15 +25,14 @@ const BottomNavigator = () => {
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
theme={{
|
||||
colors: {
|
||||
...theme,
|
||||
background: theme.background,
|
||||
elevation: {
|
||||
level2: Color(theme.primary).alpha(0.08).string(),
|
||||
},
|
||||
},
|
||||
barStyle={{
|
||||
backgroundColor: Color(theme.surface)
|
||||
.mix(Color(theme.primary), 0.08)
|
||||
.rgb()
|
||||
.string(),
|
||||
}}
|
||||
theme={{ colors: theme }}
|
||||
activeColor={theme.onSecondaryContainer}
|
||||
shifting={!showLabelsInNav}
|
||||
>
|
||||
<Tab.Screen
|
||||
|
||||
@@ -6,7 +6,6 @@ import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { setBarColor } from '../theme/utils/setBarColor';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useGithubUpdateChecker } from '../hooks/githubUpdateChecker';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
|
||||
/**
|
||||
* Navigators
|
||||
@@ -31,6 +30,7 @@ import BrowseSettings from '../screens/browse/BrowseSettings';
|
||||
import { useAppDispatch } from '@redux/hooks';
|
||||
import { updateLibraryAction } from '@redux/updates/updates.actions';
|
||||
import { useSettings } from '@hooks/reduxHooks';
|
||||
import WebviewScreen from '@screens/WebviewScreen/WebviewScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
@@ -41,7 +41,6 @@ const MainNavigator = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(async () => {
|
||||
await SplashScreen.hideAsync();
|
||||
setBarColor(theme);
|
||||
}, 500);
|
||||
|
||||
@@ -59,7 +58,7 @@ const MainNavigator = () => {
|
||||
const { isNewVersion, latestRelease } = useGithubUpdateChecker() || {};
|
||||
|
||||
return (
|
||||
<NavigationContainer theme={{ colors: { background: theme.background } }}>
|
||||
<NavigationContainer theme={{ colors: theme }}>
|
||||
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="BottomNavigator" component={BottomNavigator} />
|
||||
@@ -76,6 +75,7 @@ const MainNavigator = () => {
|
||||
<Stack.Screen name="Migration" component={Migration} />
|
||||
<Stack.Screen name="SourceNovels" component={SourceNovels} />
|
||||
<Stack.Screen name="MigrateNovel" component={MigrateNovel} />
|
||||
<Stack.Screen name="WebviewScreen" component={WebviewScreen} />
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import Downloads from '../screens/more/DownloadsScreen';
|
||||
import AppearanceSettings from '../screens/settings/SettingsAppearanceScreen';
|
||||
import CategoriesScreen from '@screens/Categories/CategoriesScreen';
|
||||
import LibrarySettings from '@screens/settings/SettingsLibraryScreen/SettingsLibraryScreen';
|
||||
import StatsScreen from '@screens/StatsScreen/StatsScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
@@ -40,6 +41,7 @@ const MoreStack = () => (
|
||||
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
|
||||
<Stack.Screen name="Downloads" component={Downloads} />
|
||||
<Stack.Screen name="Categories" component={CategoriesScreen} />
|
||||
<Stack.Screen name="Statistics" component={StatsScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
BOOKMARK_CHAPTER,
|
||||
MARK_PREVIOUS_CHAPTERS_READ,
|
||||
MARK_PREVIOUS_CHAPTERS_UNREAD,
|
||||
ALL_CHAPTER_DELETED,
|
||||
} from './novel.types';
|
||||
|
||||
import { fetchNovel } from '../../services/Source/source';
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
markPreviuschaptersRead,
|
||||
markPreviousChaptersUnread,
|
||||
getNextChapter,
|
||||
deleteChapters,
|
||||
} from '../../database/queries/ChapterQueries';
|
||||
import { deleteUpdateFromDb } from '../../database/queries/UpdateQueries';
|
||||
import {
|
||||
@@ -257,10 +259,7 @@ export const markPreviousChaptersUnreadAction =
|
||||
};
|
||||
|
||||
export const markChapterUnreadAction =
|
||||
(chapters, novelId) => async (dispatch, getState) => {
|
||||
const { sort = 'ORDER BY chapterId ASC', filter = '' } =
|
||||
getState().preferenceReducer.novelSettings[novelId];
|
||||
|
||||
(chapters, novelId, sort, filter) => async dispatch => {
|
||||
chapters.map(chapter => markChapterUnread(chapter.chapterId));
|
||||
|
||||
const chaps = await getChapters(novelId, sort, filter);
|
||||
@@ -444,16 +443,14 @@ export const deleteChapterAction =
|
||||
*/
|
||||
export const deleteAllChaptersAction =
|
||||
(sourceId, chapters) => async dispatch => {
|
||||
await Promise.all(
|
||||
chapters.map(async chapter => {
|
||||
await deleteChapter(sourceId, chapter.novelId, chapter.chapterId);
|
||||
const chapterIds = chapters.map(chapter => chapter.chapterId);
|
||||
await deleteChapters(sourceId, chapters);
|
||||
|
||||
dispatch({
|
||||
type: ALL_CHAPTER_DELETED,
|
||||
payload: chapterIds,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DELETED,
|
||||
payload: chapter.chapterId,
|
||||
});
|
||||
}),
|
||||
);
|
||||
showToast('Chapters deleted');
|
||||
};
|
||||
|
||||
|
||||
@@ -117,6 +117,14 @@ const novelReducer = (state = initialState, action) => {
|
||||
: chapter,
|
||||
),
|
||||
};
|
||||
case CHAPTER_DELETED:
|
||||
return {
|
||||
...state,
|
||||
chapters: state.chapters.map(chapter => ({
|
||||
...chapter,
|
||||
downloaded: 0,
|
||||
})),
|
||||
};
|
||||
case UPDATE_LAST_READ:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -10,6 +10,8 @@ export const UPDATE_LAST_READ = 'UPDATE_LAST_READ';
|
||||
|
||||
export const CHAPTER_DOWNLOADED = 'CHAPTER_DOWNLOADED';
|
||||
export const CHAPTER_DELETED = 'CHAPTER_DELETED';
|
||||
export const ALL_CHAPTER_DELETED = 'ALL_CHAPTER_DELETED';
|
||||
|
||||
export const CHAPTER_DOWNLOADING = 'CHAPTER_DOWNLOADING';
|
||||
export const CHAPTER_READ = 'CHAPTER_READ';
|
||||
export const CHAPTER_UNREAD = 'CHAPTER_UNREAD';
|
||||
|
||||
@@ -31,6 +31,7 @@ export const initialState = {
|
||||
*/
|
||||
|
||||
downloadedOnlyMode: false,
|
||||
useLibraryFAB: false,
|
||||
|
||||
/**
|
||||
* Browse settings
|
||||
@@ -72,6 +73,7 @@ export const initialState = {
|
||||
autoScrollOffset: null,
|
||||
verticalSeekbar: true,
|
||||
removeExtraParagraphSpacing: false,
|
||||
useChapterDrawerSwipeNavigation: true,
|
||||
|
||||
reader: {
|
||||
theme: '#292832',
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { SET_LAST_UPDATE_TIME } from './updates.types';
|
||||
import BackgroundService from 'react-native-background-actions';
|
||||
|
||||
import { SET_LAST_UPDATE_TIME } from './updates.types';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import { updateLibrary } from '../../services/updates/updates';
|
||||
|
||||
export const updateLibraryAction = props => async (dispatch, getState) => {
|
||||
if (BackgroundService.isRunning()) {
|
||||
showToast('An update is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
showToast('Updating library');
|
||||
|
||||
dispatch({ type: SET_LAST_UPDATE_TIME, payload: Date.now() });
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
|
||||
import { FAB, Portal } from 'react-native-paper';
|
||||
import Bottomsheet from 'rn-sliding-up-panel';
|
||||
import {
|
||||
Container,
|
||||
LoadingScreenV2,
|
||||
LoadingMoreIndicator,
|
||||
ErrorScreenV2,
|
||||
SearchbarV2,
|
||||
} from '@components/index';
|
||||
import { Container, ErrorScreenV2, SearchbarV2 } from '@components/index';
|
||||
import NovelList from '@components/NovelList';
|
||||
import NovelCover from '@components/NovelCover';
|
||||
import FilterBottomSheet from './components/BottomSheet';
|
||||
import Bottomsheet from '@gorhom/bottom-sheet';
|
||||
import FilterBottomSheet from './components/FilterBottomSheet';
|
||||
|
||||
import { useCategorySettings, useSearch } from '../../hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
@@ -25,6 +18,8 @@ import { StyleSheet } from 'react-native';
|
||||
import { useLibraryNovels } from '../../screens/library/hooks/useLibrary';
|
||||
import { insertNovelInLibrary } from '../../database/queries/NovelQueriesV2';
|
||||
import { LibraryNovelInfo } from '../../database/types';
|
||||
import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
interface BrowseSourceScreenProps {
|
||||
route: {
|
||||
@@ -80,7 +75,11 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
};
|
||||
|
||||
const handleOpenWebView = async () => {
|
||||
WebBrowser.openBrowserAsync(sourceUrl);
|
||||
navigate('WebviewScreen', {
|
||||
sourceId,
|
||||
name: sourceName,
|
||||
url: sourceUrl,
|
||||
});
|
||||
};
|
||||
|
||||
const { library, setLibrary } = useLibraryNovels();
|
||||
@@ -104,8 +103,8 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
[sourceId],
|
||||
);
|
||||
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
const filterSheetRef = useRef<Bottomsheet | null>(null);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SearchbarV2
|
||||
@@ -120,7 +119,7 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
theme={theme}
|
||||
/>
|
||||
{isLoading || isSearching ? (
|
||||
<LoadingScreenV2 theme={theme} />
|
||||
<SourceScreenSkeletonLoading theme={theme} />
|
||||
) : errorMessage || novelList.length === 0 ? (
|
||||
<ErrorScreenV2
|
||||
error={errorMessage || getString('sourceScreen.noResultsFound')}
|
||||
@@ -173,20 +172,24 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
}}
|
||||
ListFooterComponent={
|
||||
hasNextPage && !searchText ? (
|
||||
<LoadingMoreIndicator theme={theme} />
|
||||
<SourceScreenSkeletonLoading theme={theme} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showLatestNovels && filterValues?.length ? (
|
||||
<>
|
||||
<FAB
|
||||
icon={'filter-variant'}
|
||||
style={[styles.filterFab, { backgroundColor: theme.primary }]}
|
||||
style={[
|
||||
styles.filterFab,
|
||||
{ backgroundColor: theme.primary, marginBottom: bottom },
|
||||
]}
|
||||
label={'Filter'}
|
||||
uppercase={false}
|
||||
color={theme.onPrimary}
|
||||
onPress={() => filterSheetRef?.current?.show()}
|
||||
onPress={() => filterSheetRef?.current?.expand()}
|
||||
/>
|
||||
<Portal>
|
||||
<FilterBottomSheet
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { LegacyRef, useState } from 'react';
|
||||
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import React, { Ref, useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import color from 'color';
|
||||
import Bottomsheet from 'rn-sliding-up-panel';
|
||||
|
||||
import BottomSheet from '../../../components/BottomSheet/BottomSheet';
|
||||
import BottomSheetType, { BottomSheetView } from '@gorhom/bottom-sheet';
|
||||
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import {
|
||||
@@ -116,41 +118,37 @@ const FilterItem: React.FC<FilterItemProps> = ({
|
||||
};
|
||||
|
||||
interface BottomSheetProps {
|
||||
filterSheetRef: Ref<BottomSheetType> | null;
|
||||
filtersValues: SourceFilter[] | undefined;
|
||||
filterSheetRef: LegacyRef<Bottomsheet> | null;
|
||||
setFilters: (filters?: SelectedFilter) => void;
|
||||
clearFilters: () => void;
|
||||
}
|
||||
|
||||
const BottomSheet: React.FC<BottomSheetProps> = ({
|
||||
const FilterBottomSheet: React.FC<BottomSheetProps> = ({
|
||||
filterSheetRef,
|
||||
filtersValues,
|
||||
clearFilters,
|
||||
setFilters,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
const [selectedFilters, setSelectedFilters] = useState<
|
||||
SelectedFilter | undefined
|
||||
>();
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={filterSheetRef}
|
||||
draggableRange={{ top: 600, bottom: 0 }}
|
||||
snappingPoints={[0, 400, 600]}
|
||||
showBackdrop={true}
|
||||
backdropOpacity={0.3}
|
||||
<BottomSheet
|
||||
bottomSheetRef={filterSheetRef}
|
||||
snapPoints={[400, 600]}
|
||||
height={600}
|
||||
theme={theme}
|
||||
>
|
||||
<View
|
||||
<BottomSheetView
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: overlay(2, theme.surface) },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
<BottomSheetView
|
||||
style={[
|
||||
styles.buttonContainer,
|
||||
{ borderBottomColor: dividerColor(theme.isDark) },
|
||||
@@ -167,14 +165,14 @@ const BottomSheet: React.FC<BottomSheetProps> = ({
|
||||
/>
|
||||
<Button
|
||||
title={'Filter'}
|
||||
textColor={theme.colorButtonText}
|
||||
textColor={theme.textColorPrimary}
|
||||
onPress={() => {
|
||||
setFilters(selectedFilters);
|
||||
filterSheetRef?.current?.hide();
|
||||
filterSheetRef?.current?.collapse();
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</BottomSheetView>
|
||||
<FlatList
|
||||
contentContainerStyle={styles.filterContainer}
|
||||
data={filtersValues}
|
||||
@@ -187,12 +185,12 @@ const BottomSheet: React.FC<BottomSheetProps> = ({
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
</BottomSheetView>
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomSheet;
|
||||
export default FilterBottomSheet;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { FAB, Portal } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { Appbar, EmptyView, LoadingScreenV2 } from '@components/index';
|
||||
import { Appbar, EmptyView } from '@components/index';
|
||||
import AddCategoryModal from './components/AddCategoryModal';
|
||||
|
||||
import {
|
||||
@@ -17,6 +17,7 @@ import { getString } from '@strings/translations';
|
||||
import { Category } from '../../database/types';
|
||||
import CategoryCard from './components/CategoryCard';
|
||||
import { orderBy } from 'lodash';
|
||||
import CategorySkeletonLoading from './components/CategorySkeletonLoading';
|
||||
|
||||
const CategoriesScreen = () => {
|
||||
const theme = useTheme();
|
||||
@@ -78,7 +79,6 @@ const CategoriesScreen = () => {
|
||||
setTrue: showCategoryModal,
|
||||
setFalse: closeCategoryModal,
|
||||
} = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar
|
||||
@@ -87,7 +87,7 @@ const CategoriesScreen = () => {
|
||||
theme={theme}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<LoadingScreenV2 theme={theme} />
|
||||
<CategorySkeletonLoading width={360.7} height={89.5} theme={theme} />
|
||||
) : (
|
||||
<FlatList
|
||||
data={categories}
|
||||
@@ -139,6 +139,7 @@ const styles = StyleSheet.create({
|
||||
bottom: 16,
|
||||
},
|
||||
contentCtn: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 16,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { memo } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
import getLoadingColors from '@utils/getLoadingColors';
|
||||
|
||||
interface Props {
|
||||
width: number;
|
||||
height: number;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const CategorySkeletonLoading: React.FC<Props> = ({ height, width, theme }) => {
|
||||
const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient);
|
||||
|
||||
const [highlightColor, backgroundColor] = getLoadingColors(theme);
|
||||
|
||||
const renderLoadingCard = (item: number, index: number) => {
|
||||
return (
|
||||
<View key={index}>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.categoryCard}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={height}
|
||||
width={width}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const items = [];
|
||||
for (let index = 0; index < Math.random() * 6 + 3; index++) {
|
||||
items.push(0);
|
||||
}
|
||||
|
||||
return <View style={styles.contentCtn}>{items.map(renderLoadingCard)}</View>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
categoryCard: {
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 8,
|
||||
borderRadius: 12,
|
||||
},
|
||||
contentCtn: {
|
||||
paddingVertical: 16,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
});
|
||||
|
||||
export default memo(CategorySkeletonLoading);
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import color from 'color';
|
||||
|
||||
@@ -21,6 +14,8 @@ import GlobalSearchNovelItem from './GlobalSearchNovelItem';
|
||||
import { useLibraryNovels } from '@screens/library/hooks/useLibrary';
|
||||
import { insertNovelInLibrary } from '../../../database/queries/NovelQueriesV2';
|
||||
import { LibraryNovelInfo } from '../../../database/types';
|
||||
import GlobalSearchSkeletonLoading from '@screens/browse/loadingAnimation/GlobalSearchSkeletonLoading';
|
||||
import { useCategorySettings } from '@hooks/useSettings';
|
||||
|
||||
interface GlobalSearchResultsListProps {
|
||||
searchResults: GlobalSearchResult[];
|
||||
@@ -35,6 +30,7 @@ const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
|
||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||
const keyExtractor = useCallback(item => item.source.sourceId.toString(), []);
|
||||
const { library, setLibrary } = useLibraryNovels();
|
||||
const { defaultCategoryId = 1 } = useCategorySettings();
|
||||
|
||||
const novelInLibrary = (sourceId: number, novelUrl: string) =>
|
||||
library?.some(
|
||||
@@ -88,7 +84,7 @@ const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
|
||||
/>
|
||||
</Pressable>
|
||||
{item.isLoading ? (
|
||||
<ActivityIndicator color={theme.primary} style={styles.loading} />
|
||||
<GlobalSearchSkeletonLoading theme={theme} />
|
||||
) : item.error ? (
|
||||
<Text style={[styles.error, { color: errorColor }]}>
|
||||
{item.error}
|
||||
@@ -145,6 +141,7 @@ const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
|
||||
novelItem.sourceId,
|
||||
novelItem.novelUrl,
|
||||
inLibrary,
|
||||
defaultCategoryId,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -168,12 +165,6 @@ const styles = StyleSheet.create({
|
||||
paddingTop: 8,
|
||||
paddingBottom: 60,
|
||||
},
|
||||
loading: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 16,
|
||||
},
|
||||
sourceHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { Appbar, ErrorScreenV2, LoadingScreenV2 } from '@components';
|
||||
|
||||
import { LibraryStats } from '@database/types';
|
||||
import {
|
||||
getChaptersDownloadedCountFromDb,
|
||||
getChaptersReadCountFromDb,
|
||||
getChaptersTotalCountFromDb,
|
||||
getChaptersUnreadCountFromDb,
|
||||
getLibraryStatsFromDb,
|
||||
getNovelGenresFromDb,
|
||||
getNovelStatusFromDb,
|
||||
} from '@database/queries/StatsQueries';
|
||||
import { Row } from '@components/Common';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
const StatsScreen = () => {
|
||||
const theme = useTheme();
|
||||
const { goBack } = useNavigation();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [stats, setStats] = useState<LibraryStats>({});
|
||||
const [error, setError] = useState();
|
||||
|
||||
const getStats = async () => {
|
||||
try {
|
||||
const res = await Promise.all([
|
||||
getLibraryStatsFromDb(),
|
||||
getChaptersTotalCountFromDb(),
|
||||
getChaptersReadCountFromDb(),
|
||||
getChaptersUnreadCountFromDb(),
|
||||
getChaptersDownloadedCountFromDb(),
|
||||
getNovelGenresFromDb(),
|
||||
getNovelStatusFromDb(),
|
||||
]);
|
||||
setStats(Object.assign(...res));
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getStats();
|
||||
}, []);
|
||||
|
||||
const Header = (
|
||||
<Appbar
|
||||
title={getString('statsScreen.title')}
|
||||
handleGoBack={goBack}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
{Header}
|
||||
<LoadingScreenV2 theme={theme} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
{Header}
|
||||
<ErrorScreenV2 theme={theme} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Header}
|
||||
<ScrollView
|
||||
style={styles.screenCtn}
|
||||
contentContainerStyle={styles.contentCtn}
|
||||
>
|
||||
<Text style={[styles.header, { color: theme.textColorSecondary }]}>
|
||||
{getString('moreScreen.settingsScreen.generalSettings')}
|
||||
</Text>
|
||||
<Row style={styles.statsRow}>
|
||||
<StatsCard
|
||||
label={getString('statsScreen.titlesInLibrary')}
|
||||
value={stats.novelsCount}
|
||||
/>
|
||||
<StatsCard
|
||||
label={getString('statsScreen.readChapters')}
|
||||
value={stats.chaptersRead}
|
||||
/>
|
||||
<StatsCard
|
||||
label={getString('statsScreen.totalChapters')}
|
||||
value={stats.chaptersCount}
|
||||
/>
|
||||
</Row>
|
||||
<Row style={styles.statsRow}>
|
||||
<StatsCard
|
||||
label={getString('statsScreen.unreadChapters')}
|
||||
value={stats.chaptersUnread}
|
||||
/>
|
||||
<StatsCard
|
||||
label={getString('statsScreen.downloadedChapters')}
|
||||
value={stats.chaptersDownloaded}
|
||||
/>
|
||||
</Row>
|
||||
<Row style={styles.statsRow}>
|
||||
<StatsCard label="Sources" value={stats.sourcesCount} />
|
||||
</Row>
|
||||
<Text style={[styles.header, { color: theme.textColorSecondary }]}>
|
||||
{getString('statsScreen.genreDistribution')}
|
||||
</Text>
|
||||
<Row style={[styles.statsRow, styles.genreRow]}>
|
||||
{Object.entries(stats.genres || {}).map(item => (
|
||||
<StatsCard key={item[0]} label={item[0]} value={item[1]} />
|
||||
))}
|
||||
</Row>
|
||||
<Text style={[styles.header, { color: theme.textColorSecondary }]}>
|
||||
{getString('statsScreen.statusDistribution')}
|
||||
</Text>
|
||||
<Row style={[styles.statsRow, styles.genreRow]}>
|
||||
{Object.entries(stats.status || {}).map(item => (
|
||||
<StatsCard key={item[0]} label={item[0]} value={item[1]} />
|
||||
))}
|
||||
</Row>
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsScreen;
|
||||
|
||||
export const StatsCard: React.FC<{ label: string; value?: number }> = ({
|
||||
label,
|
||||
value = 0,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.statsCardCtn,
|
||||
{
|
||||
backgroundColor: theme.isDark
|
||||
? overlay(2, theme.surface)
|
||||
: theme.secondaryContainer,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.statsVal, { color: theme.primary }]}>{value}</Text>
|
||||
<Text style={{ color: theme.textColorPrimary }}> {label}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
statsCardCtn: {
|
||||
elevation: 1,
|
||||
margin: 4,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
statsVal: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
},
|
||||
screenCtn: {
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
contentCtn: {
|
||||
paddingBottom: 40,
|
||||
},
|
||||
statsRow: {
|
||||
marginBottom: 8,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
genreRow: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
header: {
|
||||
paddingVertical: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||
import WebView from 'react-native-webview';
|
||||
import CookieManager from '@react-native-cookies/cookies';
|
||||
|
||||
import { Appbar } from '@components';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import useSourceStorage from '@hooks/useSourceStorage';
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
type ReaderScreenRouteProps = RouteProp<{
|
||||
params: {
|
||||
name: string;
|
||||
sourceId: number;
|
||||
url: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
const WebviewScreen = () => {
|
||||
const theme = useTheme();
|
||||
const { goBack } = useNavigation();
|
||||
|
||||
const {
|
||||
params: { name, sourceId, url },
|
||||
} = useRoute<ReaderScreenRouteProps>();
|
||||
const { setSourceStorage } = useSourceStorage({ sourceId });
|
||||
|
||||
useEffect(() => {
|
||||
CookieManager.get(url, true).then(cookies => {
|
||||
const cloudflareCookie = cookies?.cf_clearance;
|
||||
if (cloudflareCookie) {
|
||||
const cloudflareCookieString = `${cloudflareCookie.name}=${cloudflareCookie.value}`;
|
||||
setSourceStorage('cookies', cloudflareCookieString);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar mode="small" title={name} handleGoBack={goBack} theme={theme} />
|
||||
<WebView
|
||||
startInLoadingState
|
||||
userAgent={defaultUserAgentString}
|
||||
source={{ uri: url }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebviewScreen;
|
||||
@@ -4,12 +4,13 @@ import { StyleSheet, View, ActivityIndicator, FlatList } from 'react-native';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
|
||||
import { ErrorView } from '../../../components/ErrorView/ErrorView';
|
||||
import { LoadingScreenV2, SearchbarV2 } from '@components';
|
||||
import { SearchbarV2 } from '@components';
|
||||
|
||||
import { showToast } from '../../../hooks/showToast';
|
||||
import { scrapeSearchResults, scrapeTopNovels } from './MyAnimeListScraper';
|
||||
import MalNovelCard from './MalNovelCard/MalNovelCard';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import MalLoading from '../loadingAnimation/MalLoading';
|
||||
|
||||
const BrowseMalScreen = ({ navigation, route }) => {
|
||||
const theme = useTheme();
|
||||
@@ -143,7 +144,7 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
]}
|
||||
/>
|
||||
{loading ? (
|
||||
<LoadingScreenV2 theme={theme} />
|
||||
<MalLoading theme={theme} />
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.novelsContainer}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { memo } from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
import LoadingNovel from './LoadingNovel';
|
||||
import getLoadingColors from '@utils/getLoadingColors';
|
||||
|
||||
interface Props {
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const GlobalSearchSkeletonLoading: React.FC<Props> = ({ theme }) => {
|
||||
const styles = createStyleSheet();
|
||||
|
||||
const [highlightColor, backgroundColor] = getLoadingColors(theme);
|
||||
|
||||
const items: Array<number> = [1, 2, 3, 4];
|
||||
return (
|
||||
<View style={[styles.container, styles.row]}>
|
||||
{items.map((item: number, index: number) => {
|
||||
return (
|
||||
<LoadingNovel
|
||||
key={index}
|
||||
backgroundColor={backgroundColor}
|
||||
highlightColor={highlightColor}
|
||||
pictureHeight={153.1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const createStyleSheet = () => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
marginHorizontal: 4,
|
||||
marginBottom: 6,
|
||||
marginTop: 6,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 3,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default memo(GlobalSearchSkeletonLoading);
|
||||
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { DisplayModes } from '@screens/library/constants/constants';
|
||||
|
||||
interface Props {
|
||||
backgroundColor: string;
|
||||
highlightColor: string;
|
||||
pictureHeight: number;
|
||||
pictureWidth: number;
|
||||
displayMode: DisplayModes;
|
||||
}
|
||||
|
||||
const LoadingNovel: React.FC<Props> = ({
|
||||
backgroundColor,
|
||||
highlightColor,
|
||||
pictureHeight,
|
||||
pictureWidth,
|
||||
displayMode,
|
||||
}) => {
|
||||
const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient);
|
||||
let randomNumber = Math.random();
|
||||
randomNumber < 0.1 ? (randomNumber = 0) : null;
|
||||
// pictureWidth = pictureWidth === undefined ? 114.5 : pictureWidth;
|
||||
const styles = createStyleSheet(
|
||||
pictureHeight + (displayMode === 2 || displayMode === 0 ? 9.6 : 54.6),
|
||||
pictureWidth,
|
||||
);
|
||||
if (displayMode !== 3) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.picture}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={pictureHeight}
|
||||
width={pictureWidth}
|
||||
/>
|
||||
{displayMode === 2 || displayMode === 0 ? null : (
|
||||
<>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.text}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={16}
|
||||
width={pictureWidth}
|
||||
/>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.text}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={16}
|
||||
width={randomNumber * pictureWidth}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
const chapterNumberWidth = 10 * Math.floor(Math.random() * 7);
|
||||
const textWidth = 304.7 - chapterNumberWidth;
|
||||
return (
|
||||
<View style={styles.listLoadingContainer}>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.picture}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={40}
|
||||
width={40}
|
||||
/>
|
||||
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.listText}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={18}
|
||||
width={textWidth}
|
||||
/>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.picture}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={20}
|
||||
width={chapterNumberWidth}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const createStyleSheet = (pictureHeight: number, pictureWidth: number) => {
|
||||
return StyleSheet.create({
|
||||
loadingContainer: {
|
||||
padding: 4.8,
|
||||
marginBottom: 4,
|
||||
width: pictureWidth + 9.6,
|
||||
height: pictureHeight,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
text: {
|
||||
borderRadius: 8,
|
||||
marginTop: 5,
|
||||
},
|
||||
picture: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
listLoadingContainer: {
|
||||
marginHorizontal: 12,
|
||||
marginVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
listText: {
|
||||
marginLeft: 16,
|
||||
marginRight: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
listChapter: {
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default LoadingNovel;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { memo } from 'react';
|
||||
import { View, Dimensions, StyleSheet } from 'react-native';
|
||||
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
import { getDialogBackground } from '@theme/colors';
|
||||
import getLoadingColors from '@utils/getLoadingColors';
|
||||
|
||||
interface Props {
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const MalLoading: React.FC<Props> = ({ theme }) => {
|
||||
const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient);
|
||||
const styles = createStyleSheet(theme);
|
||||
|
||||
const [highlightColor, backgroundColor] = getLoadingColors(theme);
|
||||
|
||||
const renderLoadingRect = (item: number, index: number) => {
|
||||
let randomNumber = Math.random();
|
||||
randomNumber < 0.1 ? (randomNumber = 0.1) : null;
|
||||
return (
|
||||
<View key={index} style={styles.loadingContainer}>
|
||||
<ShimmerPlaceHolder
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={120 + Math.random() * 28}
|
||||
width={100}
|
||||
/>
|
||||
<View style={styles.loadingText}>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.text}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={16}
|
||||
width={Dimensions.get('window').width - 140}
|
||||
/>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.text}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={16}
|
||||
width={Dimensions.get('window').width - 140}
|
||||
/>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.text}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={16}
|
||||
width={randomNumber * (Dimensions.get('window').width - 140)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
const items: Array<number> = [0, 1, 2, 3, 4];
|
||||
return <View style={styles.container}>{items.map(renderLoadingRect)}</View>;
|
||||
};
|
||||
|
||||
const createStyleSheet = (theme: MD3ThemeType) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
position: 'relative',
|
||||
flexGrow: 1,
|
||||
// height: 150,
|
||||
backgroundColor: 'transparent',
|
||||
marginBottom: 8,
|
||||
marginTop: -3,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
loadingContainer: {
|
||||
margin: 10,
|
||||
borderRadius: 8,
|
||||
backgroundColor: getDialogBackground(theme),
|
||||
width: Dimensions.get('window').width - 20,
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'row',
|
||||
elevation: 1,
|
||||
},
|
||||
text: {
|
||||
borderRadius: 8,
|
||||
marginVertical: 5,
|
||||
},
|
||||
loadingText: {
|
||||
margin: 10,
|
||||
height: 10,
|
||||
width: Dimensions.get('window').width - 140,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default memo(MalLoading);
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { View, StyleSheet, useWindowDimensions } from 'react-native';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
import getLoadingColors from '@utils/getLoadingColors';
|
||||
import LoadingNovel from '@screens/browse/loadingAnimation/LoadingNovel';
|
||||
import { useLibrarySettings } from '@hooks/useSettings';
|
||||
import { useDeviceOrientation } from '../../../services/utils/helpers';
|
||||
import { DisplayModes } from '@screens/library/constants/constants';
|
||||
|
||||
interface Props {
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const SourceScreenSkeletonLoading: React.FC<Props> = ({ theme }) => {
|
||||
const [highlightColor, backgroundColor] = getLoadingColors(theme);
|
||||
|
||||
const { displayMode = DisplayModes.Comfortable, novelsPerRow = 3 } =
|
||||
useLibrarySettings();
|
||||
|
||||
const window = useWindowDimensions();
|
||||
const styles = createStyleSheet();
|
||||
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const numColumns = useMemo(
|
||||
() => (orientation === 'landscape' ? 6 : novelsPerRow),
|
||||
[orientation, novelsPerRow],
|
||||
);
|
||||
|
||||
const [pictureHeight, pictureWidth] = useMemo(() => {
|
||||
let height = (window.width / numColumns) * (4 / 3);
|
||||
let width = (window.width - 12 - 9.6 * numColumns) / numColumns;
|
||||
return [height, width];
|
||||
}, [numColumns]);
|
||||
|
||||
const renderLoadingNovel = (item: number, index: number) => {
|
||||
let randomNumber = Math.random();
|
||||
randomNumber < 0.1 ? (randomNumber = 0) : null;
|
||||
return (
|
||||
<LoadingNovel
|
||||
key={index}
|
||||
backgroundColor={backgroundColor}
|
||||
highlightColor={highlightColor}
|
||||
pictureHeight={pictureHeight}
|
||||
pictureWidth={pictureWidth}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const renderLoading = (item: number, index: number) => {
|
||||
const offset = Math.pow(10, item);
|
||||
const items: Array<number> = [1 * offset];
|
||||
if (displayMode !== DisplayModes.List) {
|
||||
for (let i = 2; i <= numColumns; i++) {
|
||||
items.push(i * offset);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View key={index} style={styles.row}>
|
||||
{items.map(renderLoadingNovel)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
let items: Array<number> = [];
|
||||
if (displayMode === DisplayModes.List) {
|
||||
items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
} else {
|
||||
for (let i = 1; i * pictureHeight < window.height - 100; i++) {
|
||||
items.push(i);
|
||||
}
|
||||
}
|
||||
return <View style={styles.container}>{items.map(renderLoading)}</View>;
|
||||
};
|
||||
|
||||
const createStyleSheet = () => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
marginHorizontal: 4,
|
||||
marginBottom: 8,
|
||||
marginTop: 2,
|
||||
overflow: 'visible',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 1,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default memo(SourceScreenSkeletonLoading);
|
||||
@@ -11,6 +11,7 @@ import { getDialogBackground } from '@theme/colors';
|
||||
import { Button } from '@components';
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
import { getString } from '@strings/translations';
|
||||
import { openNovel } from '@utils/handleNavigateParams';
|
||||
|
||||
const MigrationNovelList = ({ data, theme, library, navigation }) => {
|
||||
const [selectedNovel, setSelectedNovel] = useState(false);
|
||||
@@ -28,10 +29,13 @@ const MigrationNovelList = ({ data, theme, library, navigation }) => {
|
||||
theme={theme}
|
||||
onPress={() => showModal(item.sourceId, item.novelUrl, item.novelName)}
|
||||
onLongPress={() =>
|
||||
navigation.navigate('Novel', {
|
||||
...item,
|
||||
sourceId: item.sourceId,
|
||||
})
|
||||
navigation.navigate(
|
||||
'Novel',
|
||||
openNovel({
|
||||
...item,
|
||||
sourceId: item.sourceId,
|
||||
}),
|
||||
)
|
||||
}
|
||||
inLibrary={inLibrary(item.sourceId, item.novelUrl)}
|
||||
/>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { sourceManager } from '../../../sources/sourceManager';
|
||||
import { useBrowseSettings, useSourcesReducer } from '../../../redux/hooks';
|
||||
import { useLibraryNovels } from '@screens/library/hooks/useLibrary';
|
||||
import { Appbar } from '@components';
|
||||
import GlobalSearchSkeletonLoading from '../loadingAnimation/GlobalSearchSkeletonLoading';
|
||||
|
||||
const MigrationNovels = ({ navigation, route }) => {
|
||||
const { sourceId, novelName } = route.params;
|
||||
@@ -108,10 +109,7 @@ const MigrationNovels = ({ navigation, route }) => {
|
||||
{item.error ? (
|
||||
<Text style={[styles.error, colorError]}>{item.error}</Text>
|
||||
) : item.loading ? (
|
||||
<ActivityIndicator
|
||||
color={theme.primary}
|
||||
style={{ marginVertical: 16 }}
|
||||
/>
|
||||
<GlobalSearchSkeletonLoading theme={theme} />
|
||||
) : (
|
||||
<MigrationNovelList
|
||||
data={item.novels}
|
||||
|
||||
@@ -4,12 +4,7 @@ import { useNavigation } from '@react-navigation/native';
|
||||
import dayjs from 'dayjs';
|
||||
import { Portal } from 'react-native-paper';
|
||||
|
||||
import {
|
||||
EmptyView,
|
||||
ErrorScreenV2,
|
||||
LoadingScreenV2,
|
||||
SearchbarV2,
|
||||
} from '../../components/index';
|
||||
import { EmptyView, ErrorScreenV2, SearchbarV2 } from '../../components/index';
|
||||
import HistoryCard from './components/HistoryCard/HistoryCard';
|
||||
|
||||
import { useSearch, useHistory, useBoolean } from '@hooks/index';
|
||||
@@ -20,6 +15,15 @@ import { convertDateToISOString } from '../../database/utils/convertDateToISOStr
|
||||
import { History } from '../../database/types';
|
||||
import { getString } from '@strings/translations';
|
||||
import ClearHistoryDialog from './components/ClearHistoryDialog';
|
||||
import {
|
||||
openChapter,
|
||||
openChapterChapterTypes,
|
||||
openChapterNovelTypes,
|
||||
openChapterFunctionTypes,
|
||||
openNovel,
|
||||
openNovelProps,
|
||||
} from '@utils/handleNavigateParams';
|
||||
import HistorySkeletonLoading from './components/HistorySkeletonLoading';
|
||||
|
||||
const HistoryScreen = () => {
|
||||
const theme = useTheme();
|
||||
@@ -72,46 +76,16 @@ const HistoryScreen = () => {
|
||||
};
|
||||
|
||||
const handleNavigateToChapter = (
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
novelName: string,
|
||||
novelUrl: string,
|
||||
chapterId: number,
|
||||
chapterUrl: string,
|
||||
chapterName: string,
|
||||
bookmark: number,
|
||||
novel: openChapterNovelTypes,
|
||||
chapter: openChapterChapterTypes,
|
||||
) =>
|
||||
navigate(
|
||||
'Chapter' as never,
|
||||
{
|
||||
sourceId,
|
||||
novelId,
|
||||
novelName,
|
||||
novelUrl,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
bookmark,
|
||||
chapterName,
|
||||
} as never,
|
||||
openChapter(novel, chapter) as openChapterFunctionTypes as never,
|
||||
);
|
||||
|
||||
const handleNavigateToNovel = (
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
novelUrl: string,
|
||||
novelName: string,
|
||||
novelCover: string,
|
||||
) =>
|
||||
navigate(
|
||||
'Novel' as never,
|
||||
{
|
||||
sourceId,
|
||||
novelId,
|
||||
novelUrl,
|
||||
novelName,
|
||||
novelCover,
|
||||
} as never,
|
||||
);
|
||||
const handleNavigateToNovel = (novel: openNovelProps) =>
|
||||
navigate('Novel' as never, openNovel(novel) as openNovelProps as never);
|
||||
|
||||
const {
|
||||
value: clearHistoryDialogVisible,
|
||||
@@ -136,7 +110,7 @@ const HistoryScreen = () => {
|
||||
theme={theme}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<LoadingScreenV2 theme={theme} />
|
||||
<HistorySkeletonLoading theme={theme} />
|
||||
) : error ? (
|
||||
<ErrorScreenV2 error={error} theme={theme} />
|
||||
) : (
|
||||
|
||||
@@ -12,27 +12,20 @@ import { parseChapterNumber } from '../../../../utils/parseChapterNumber';
|
||||
import { History } from '../../../../database/types';
|
||||
import { MD3ThemeType } from '../../../../theme/types';
|
||||
import { coverPlaceholderColor } from '../../../../theme/colors';
|
||||
import {
|
||||
openChapterChapterTypes,
|
||||
openChapterNovelTypes,
|
||||
openNovelProps,
|
||||
} from '@utils/handleNavigateParams';
|
||||
|
||||
interface HistoryCardProps {
|
||||
history: History;
|
||||
handleNavigateToChapter: (
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
novelName: string,
|
||||
novelUrl: string,
|
||||
chapterId: number,
|
||||
chapterUrl: string,
|
||||
chapterName: string,
|
||||
isBookmarked: number,
|
||||
novel: openChapterNovelTypes,
|
||||
chapter: openChapterChapterTypes,
|
||||
) => void;
|
||||
handleRemoveFromHistory: (historyId: number) => void;
|
||||
handleNavigateToNovel: (
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
novelUrl: string,
|
||||
novelName: string,
|
||||
novelCover: string,
|
||||
) => void;
|
||||
handleNavigateToNovel: (novel: openNovelProps) => void;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
@@ -70,13 +63,13 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
|
||||
style={styles.container}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
onPress={() =>
|
||||
handleNavigateToNovel(
|
||||
handleNavigateToNovel({
|
||||
sourceId,
|
||||
novelId,
|
||||
novelUrl,
|
||||
novelName,
|
||||
novelCover,
|
||||
)
|
||||
})
|
||||
}
|
||||
>
|
||||
<View style={styles.imageAndNameContainer}>
|
||||
@@ -103,14 +96,14 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
|
||||
name="play"
|
||||
onPress={() =>
|
||||
handleNavigateToChapter(
|
||||
sourceId,
|
||||
novelId,
|
||||
novelName,
|
||||
novelUrl,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
bookmark,
|
||||
{ sourceId, novelName, novelUrl },
|
||||
{
|
||||
novelId,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
bookmark,
|
||||
},
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { memo } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
import getLoadingColors from '@utils/getLoadingColors';
|
||||
|
||||
interface Props {
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const HistorySkeletonLoading: React.FC<Props> = ({ theme }) => {
|
||||
const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient);
|
||||
|
||||
const [highlightColor, backgroundColor] = getLoadingColors(theme);
|
||||
|
||||
const renderLoadingChapter = (item: number, index: number) => {
|
||||
return (
|
||||
<>
|
||||
{index === 0 || Math.random() > 0.6 ? (
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.date}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={19.3}
|
||||
width={Math.random() * 40 + 50}
|
||||
/>
|
||||
) : null}
|
||||
<View style={styles.chapterCtn} key={index}>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.picture}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={80}
|
||||
width={56}
|
||||
/>
|
||||
<View style={styles.textCtn}>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.text}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={16}
|
||||
width={208.7}
|
||||
/>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.text}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={12}
|
||||
width={208.7}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.buttonCtn}>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.button}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={24}
|
||||
width={24}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.buttonCtn}>
|
||||
<ShimmerPlaceHolder
|
||||
style={styles.button}
|
||||
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
|
||||
height={24}
|
||||
width={24}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const items = [];
|
||||
for (let index = 0; index < Math.random() * 3 + 3; index++) {
|
||||
items.push(0);
|
||||
}
|
||||
|
||||
return <View>{items.map(renderLoadingChapter)}</View>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentCtn: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
textCtn: {
|
||||
marginTop: 5,
|
||||
marginBottom: 2,
|
||||
borderRadius: 6,
|
||||
},
|
||||
text: {
|
||||
marginBottom: 4,
|
||||
borderRadius: 6,
|
||||
},
|
||||
chapterCtn: {
|
||||
marginVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
picture: {
|
||||
marginHorizontal: 16,
|
||||
borderRadius: 4,
|
||||
height: 80,
|
||||
width: 56,
|
||||
},
|
||||
button: {
|
||||
borderRadius: 12.5,
|
||||
},
|
||||
buttonCtn: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 40,
|
||||
width: 40,
|
||||
},
|
||||
date: {
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 8,
|
||||
borderRadius: 6,
|
||||
},
|
||||
});
|
||||
|
||||
export default memo(HistorySkeletonLoading);
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { StyleSheet, Text, useWindowDimensions } from 'react-native';
|
||||
import Bottomsheet from 'rn-sliding-up-panel';
|
||||
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native';
|
||||
import BottomSheet from '@gorhom/bottom-sheet';
|
||||
import {
|
||||
NavigationState,
|
||||
SceneRendererProps,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import color from 'color';
|
||||
|
||||
import { LoadingScreenV2, SearchbarV2, Button } from '@components/index';
|
||||
import { SearchbarV2, Button } from '@components/index';
|
||||
import { LibraryView } from './components/LibraryListView';
|
||||
import LibraryBottomSheet from './components/LibraryBottomSheet/LibraryBottomSheet';
|
||||
import { Banner } from './components/Banner';
|
||||
@@ -20,7 +20,7 @@ import { useLibrary } from './hooks/useLibrary';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import useSearch from '@hooks/useSearch';
|
||||
import { getString } from '@strings/translations';
|
||||
import { Portal } from 'react-native-paper';
|
||||
import { FAB, Portal } from 'react-native-paper';
|
||||
import { useLibrarySettings } from '@hooks/useSettings';
|
||||
import {
|
||||
markAllChaptersRead,
|
||||
@@ -32,6 +32,12 @@ import useBoolean from '@hooks/useBoolean';
|
||||
import { debounce, intersection } from 'lodash';
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
import { useBackHandler } from '@hooks/useBackHandler';
|
||||
import { openChapter } from '@utils/handleNavigateParams';
|
||||
import useHistory from '@hooks/useHistory';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useSettings } from '@hooks/reduxHooks';
|
||||
import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading';
|
||||
import { Row } from '@components/Common';
|
||||
|
||||
type State = NavigationState<{
|
||||
key: string;
|
||||
@@ -48,6 +54,12 @@ const LibraryScreen = () => {
|
||||
incognitoMode = false,
|
||||
} = useLibrarySettings();
|
||||
|
||||
const { useLibraryFAB = false } = useSettings();
|
||||
|
||||
const { isLoading: isHistoryLoading, history, error } = useHistory();
|
||||
|
||||
const { right: rightInset } = useSafeAreaInsets();
|
||||
|
||||
const onChangeText = debounce((text: string) => {
|
||||
setSearchText(text);
|
||||
}, 100);
|
||||
@@ -72,7 +84,7 @@ const LibraryScreen = () => {
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const bottomSheetRef = useRef<Bottomsheet | null>(null);
|
||||
const bottomSheetRef = useRef<BottomSheet | null>(null);
|
||||
|
||||
const renderTabBar = (
|
||||
props: SceneRendererProps & { navigationState: State },
|
||||
@@ -91,9 +103,20 @@ const LibraryScreen = () => {
|
||||
styles.tabBar,
|
||||
]}
|
||||
renderLabel={({ route, color }) => (
|
||||
<Text style={{ color }}>{`${route.title} ${
|
||||
showNumberOfNovels ? '(' + (route as any).novels.length + ')' : ''
|
||||
}`}</Text>
|
||||
<Row>
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
<View
|
||||
style={[styles.badgeCtn, { backgroundColor: theme.surfaceVariant }]}
|
||||
>
|
||||
{showNumberOfNovels && (
|
||||
<Text
|
||||
style={[styles.badgetText, { color: theme.onSurfaceVariant }]}
|
||||
>
|
||||
{(route as any)?.novels.length}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Row>
|
||||
)}
|
||||
inactiveColor={theme.secondary}
|
||||
activeColor={theme.primary}
|
||||
@@ -154,7 +177,7 @@ const LibraryScreen = () => {
|
||||
}
|
||||
: {
|
||||
iconName: 'filter-variant',
|
||||
onPress: () => bottomSheetRef.current?.show(),
|
||||
onPress: () => bottomSheetRef.current?.expand(),
|
||||
},
|
||||
]}
|
||||
theme={theme}
|
||||
@@ -188,7 +211,7 @@ const LibraryScreen = () => {
|
||||
renderTabBar={renderTabBar}
|
||||
renderScene={({ route }) =>
|
||||
isLoading ? (
|
||||
<LoadingScreenV2 theme={theme} />
|
||||
<SourceScreenSkeletonLoading theme={theme} />
|
||||
) : (
|
||||
<>
|
||||
{searchText ? (
|
||||
@@ -222,6 +245,29 @@ const LibraryScreen = () => {
|
||||
onIndexChange={setIndex}
|
||||
initialLayout={{ width: layout.width }}
|
||||
/>
|
||||
{useLibraryFAB &&
|
||||
!isHistoryLoading &&
|
||||
history &&
|
||||
history.length !== 0 &&
|
||||
!error && (
|
||||
<FAB
|
||||
style={[
|
||||
styles.fab,
|
||||
{ backgroundColor: theme.primary, marginRight: rightInset + 16 },
|
||||
]}
|
||||
small
|
||||
color={theme.onPrimary}
|
||||
uppercase={false}
|
||||
label={'Resume'}
|
||||
icon="play"
|
||||
onPress={() => {
|
||||
navigate(
|
||||
'Chapter' as never,
|
||||
openChapter(history[0], history[0]) as never,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SetCategoryModal
|
||||
novelId={selectedNovelIds}
|
||||
currentCategoryIds={selectedNovelCategoryIds}
|
||||
@@ -285,4 +331,18 @@ const styles = StyleSheet.create({
|
||||
globalSearchBtnLabel: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
margin: 16,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
badgeCtn: {
|
||||
borderRadius: 50,
|
||||
marginHorizontal: 6,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
badgetText: {
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import React, { LegacyRef, useMemo, useState } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
StyleSheet,
|
||||
Text,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Bottomsheet from 'rn-sliding-up-panel';
|
||||
import React, { Ref, useMemo, useState } from 'react';
|
||||
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native';
|
||||
import { SceneMap, TabBar, TabView } from 'react-native-tab-view';
|
||||
import color from 'color';
|
||||
|
||||
@@ -25,10 +18,14 @@ import {
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { RadioButton } from '@components/RadioButton/RadioButton';
|
||||
import { overlay } from 'react-native-paper';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import {
|
||||
default as BottomSheetType,
|
||||
BottomSheetView,
|
||||
} from '@gorhom/bottom-sheet';
|
||||
import BottomSheet from '@components/BottomSheet/BottomSheet';
|
||||
|
||||
interface LibraryBottomSheetProps {
|
||||
bottomSheetRef: LegacyRef<Bottomsheet> | null;
|
||||
bottomSheetRef: Ref<BottomSheetType> | null;
|
||||
}
|
||||
|
||||
const FirstRoute = () => {
|
||||
@@ -168,7 +165,6 @@ const LibraryBottomSheet: React.FC<LibraryBottomSheetProps> = ({
|
||||
bottomSheetRef,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
@@ -210,21 +206,14 @@ const LibraryBottomSheet: React.FC<LibraryBottomSheetProps> = ({
|
||||
third: ThirdRoute,
|
||||
});
|
||||
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
|
||||
const bottomSheetHeight = 480 + bottom;
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: bottomSheetHeight, bottom: 0 }}
|
||||
snappingPoints={[0, bottomSheetHeight]}
|
||||
showBackdrop={true}
|
||||
backdropOpacity={0.3}
|
||||
height={bottomSheetHeight}
|
||||
<BottomSheet
|
||||
bottomSheetRef={bottomSheetRef}
|
||||
snapPoints={[520]}
|
||||
height={520}
|
||||
theme={theme}
|
||||
>
|
||||
<View
|
||||
<BottomSheetView
|
||||
style={[
|
||||
styles.bottomSheetCtn,
|
||||
{ backgroundColor: overlay(2, theme.surface) },
|
||||
@@ -238,8 +227,8 @@ const LibraryBottomSheet: React.FC<LibraryBottomSheetProps> = ({
|
||||
initialLayout={{ width: layout.width }}
|
||||
style={styles.tabView}
|
||||
/>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
</BottomSheetView>
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getString } from '@strings/translations';
|
||||
import { useAppDispatch } from '@redux/hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { updateLibraryAction } from '@redux/updates/updates.actions';
|
||||
import { openNovel } from '@utils/handleNavigateParams';
|
||||
|
||||
interface Props {
|
||||
categoryId: number;
|
||||
@@ -45,7 +46,7 @@ export const LibraryView: React.FC<Props> = ({
|
||||
if (selectedNovelIds.length) {
|
||||
setSelectedNovelIds(xor(selectedNovelIds, [item.novelId]));
|
||||
} else {
|
||||
navigate('Novel' as never, item as never);
|
||||
navigate('Novel' as never, openNovel(item) as never);
|
||||
dispatch(setNovel(item));
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -3,11 +3,11 @@ import { ScrollView } from 'react-native';
|
||||
|
||||
import * as Linking from 'expo-linking';
|
||||
|
||||
import { List } from '../../components/List';
|
||||
import { ScreenContainer } from '../../components/Common';
|
||||
import { MoreHeader } from './components/MoreHeader';
|
||||
import { appVersion, releaseDate } from '../../utils/versionUtils';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { List } from '@components';
|
||||
|
||||
const AboutScreen = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -24,14 +24,14 @@ const AboutScreen = ({ navigation }) => {
|
||||
<List.Section>
|
||||
<List.Item
|
||||
title="Version"
|
||||
description={`Preview ${appVersion} (${releaseDate})`}
|
||||
description={`Stable ${appVersion} (${releaseDate})`}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
`https://github.com/LNReader/lnreader-preview/releases/tag/v${appVersion}`,
|
||||
`https://github.com/LNReader/lnreader/releases/tag/v${appVersion}`,
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { ScreenContainer } from '../../components/Common';
|
||||
import EmptyView from '../../components/EmptyView';
|
||||
import { Appbar, LoadingScreenV2 } from '@components';
|
||||
import { Appbar, List } from '@components';
|
||||
import {
|
||||
deleteDownloads,
|
||||
getDownloadedChapters,
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
|
||||
import UpdatesItem from '../updates/components/UpdatesItem';
|
||||
import RemoveDownloadsDialog from './components/RemoveDownloadsDialog';
|
||||
import { openChapter, openNovel } from '@utils/handleNavigateParams';
|
||||
import UpdatesSkeletonLoading from '@screens/updates/components/UpdatesSkeletonLoading';
|
||||
|
||||
const Downloads = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -54,29 +56,13 @@ const Downloads = ({ navigation }) => {
|
||||
}, []);
|
||||
|
||||
const onPress = useCallback(
|
||||
item =>
|
||||
navigation.navigate('Chapter', {
|
||||
chapterId: item.chapterId,
|
||||
chapterUrl: item.chapterUrl,
|
||||
sourceId: item.sourceId,
|
||||
novelUrl: item.novelUrl,
|
||||
chapterName: item.chapterName,
|
||||
novelId: item.novelId,
|
||||
novelName: item.novelName,
|
||||
bookmark: item.bookmark,
|
||||
}),
|
||||
item => navigation.navigate('Chapter', openChapter(item, item)),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const onPressCover = useCallback(
|
||||
item =>
|
||||
navigation.navigate('Novel', {
|
||||
sourceId: item.sourceId,
|
||||
novelUrl: item.novelUrl,
|
||||
novelName: item.novelName,
|
||||
novelCover: item.novelCover,
|
||||
}),
|
||||
item => navigation.navigate('Novel', openNovel(item)),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
@@ -128,8 +114,12 @@ const Downloads = ({ navigation }) => {
|
||||
/>
|
||||
)}
|
||||
</Appbar>
|
||||
<List.InfoItem
|
||||
title="Downloads are saved in a SQLite Database."
|
||||
theme={theme}
|
||||
/>
|
||||
{loading ? (
|
||||
<LoadingScreenV2 theme={theme} />
|
||||
<UpdatesSkeletonLoading theme={theme} />
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.flatList}
|
||||
|
||||
@@ -32,7 +32,7 @@ const MoreScreen = ({ navigation }) => {
|
||||
setLibrarySettings({ incognitoMode: !incognitoMode });
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<>
|
||||
<MoreHeader title="More" navigation={navigation} theme={theme} />
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<List.Section>
|
||||
@@ -136,6 +136,16 @@ const MoreScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('statsScreen.title')}
|
||||
icon="chart-line"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Statistics',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title={getString('moreScreen.settings')}
|
||||
@@ -159,7 +169,7 @@ const MoreScreen = ({ navigation }) => {
|
||||
/>
|
||||
</List.Section>
|
||||
</ScrollView>
|
||||
</ScreenContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Image, StyleSheet, View } from 'react-native';
|
||||
|
||||
import { Appbar } from '@components';
|
||||
import { Appbar, List } from '@components';
|
||||
|
||||
export const MoreHeader = ({ title, navigation, theme, goBack }) => (
|
||||
<>
|
||||
@@ -19,6 +19,7 @@ export const MoreHeader = ({ title, navigation, theme, goBack }) => (
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<List.Divider theme={theme} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -27,7 +28,6 @@ const styles = StyleSheet.create({
|
||||
paddingTop: 16,
|
||||
paddingBottom: 24,
|
||||
alignItems: 'center',
|
||||
elevation: 4,
|
||||
},
|
||||
logo: {
|
||||
height: 80,
|
||||
|
||||
@@ -65,6 +65,8 @@ import DownloadCustomChapterModal from './components/DownloadCustomChapterModal'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { useCategorySettings } from '@hooks/useSettings';
|
||||
import { openChapter } from '../../utils/handleNavigateParams';
|
||||
import NovelScreenLoading from './components/LoadingAnimation/NovelScreenLoading';
|
||||
|
||||
const Novel = ({ route, navigation }) => {
|
||||
const item = route.params;
|
||||
@@ -72,7 +74,7 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { top: topInset } = useSafeAreaInsets();
|
||||
const { top: topInset, bottom: bottomInset } = useSafeAreaInsets();
|
||||
const progressViewOffset = topInset + 32;
|
||||
|
||||
const { novel, chapters, loading, updating } = useNovel();
|
||||
@@ -199,7 +201,9 @@ const Novel = ({ route, navigation }) => {
|
||||
list.push({
|
||||
icon: 'check-outline',
|
||||
onPress: () => {
|
||||
dispatch(markChapterUnreadAction(selected, novel.novelId));
|
||||
dispatch(
|
||||
markChapterUnreadAction(selected, novel.novelId, sort, filter),
|
||||
);
|
||||
setSelected([]);
|
||||
},
|
||||
});
|
||||
@@ -251,6 +255,10 @@ const Novel = ({ route, navigation }) => {
|
||||
}
|
||||
setSelected(sel => [...sel, chapter]);
|
||||
} else {
|
||||
if (selected.length === chapters.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select custom range
|
||||
*/
|
||||
@@ -282,13 +290,12 @@ const Novel = ({ route, navigation }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToChapter = chapter =>
|
||||
navigation.navigate('Chapter', {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelName,
|
||||
...chapter,
|
||||
});
|
||||
const navigateToChapter = chapter => {
|
||||
navigation.navigate(
|
||||
'Chapter',
|
||||
openChapter({ sourceId, novelUrl, novelName }, chapter),
|
||||
);
|
||||
};
|
||||
|
||||
const showProgressPercentage = chapter => {
|
||||
const savedProgress =
|
||||
@@ -350,6 +357,7 @@ const Novel = ({ route, navigation }) => {
|
||||
return (
|
||||
<Provider>
|
||||
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
{loading && <NovelScreenLoading theme={theme} />}
|
||||
<FadeView
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -616,60 +624,59 @@ const Novel = ({ route, navigation }) => {
|
||||
</Row>
|
||||
</View>
|
||||
)}
|
||||
<FlashList
|
||||
ref={ref => (flatlistRef.current = ref)}
|
||||
estimatedItemSize={100}
|
||||
data={!loading && chapters}
|
||||
extraData={[downloadQueue, selected]}
|
||||
keyExtractor={keyExtractor}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={5}
|
||||
windowSize={15}
|
||||
initialNumToRender={7}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={{ paddingBottom: 100 }}
|
||||
ListHeaderComponent={
|
||||
<NovelInfoHeader
|
||||
item={item}
|
||||
novel={novel}
|
||||
theme={theme}
|
||||
filter={filter}
|
||||
loading={loading}
|
||||
lastRead={lastReadChapter}
|
||||
setCustomNovelCover={setCustomNovelCover}
|
||||
dispatch={dispatch}
|
||||
chapters={chapters}
|
||||
navigation={navigation}
|
||||
trackerSheetRef={trackerSheetRef}
|
||||
novelBottomSheetRef={novelBottomSheetRef}
|
||||
deleteDownloadsSnackbar={deleteDownloadsSnackbar}
|
||||
/>
|
||||
}
|
||||
refreshControl={refreshControl()}
|
||||
/>
|
||||
<View style={{ minHeight: 3, flex: 1 }}>
|
||||
<FlashList
|
||||
ref={ref => (flatlistRef.current = ref)}
|
||||
estimatedItemSize={61.1}
|
||||
data={!loading && chapters}
|
||||
extraData={[downloadQueue, selected]}
|
||||
keyExtractor={keyExtractor}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={5}
|
||||
windowSize={15}
|
||||
initialNumToRender={7}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={{ paddingBottom: 100 }}
|
||||
ListHeaderComponent={
|
||||
<NovelInfoHeader
|
||||
item={item}
|
||||
novel={novel}
|
||||
theme={theme}
|
||||
filter={filter}
|
||||
loading={loading}
|
||||
lastRead={lastReadChapter}
|
||||
setCustomNovelCover={setCustomNovelCover}
|
||||
dispatch={dispatch}
|
||||
chapters={chapters}
|
||||
navigation={navigation}
|
||||
trackerSheetRef={trackerSheetRef}
|
||||
novelBottomSheetRef={novelBottomSheetRef}
|
||||
deleteDownloadsSnackbar={deleteDownloadsSnackbar}
|
||||
/>
|
||||
}
|
||||
refreshControl={refreshControl()}
|
||||
/>
|
||||
</View>
|
||||
{useFabForContinueReading &&
|
||||
!loading &&
|
||||
chapters.length > 0 &&
|
||||
lastReadChapter && (
|
||||
<FAB
|
||||
style={[styles.fab, { backgroundColor: theme.primary }]}
|
||||
style={[
|
||||
styles.fab,
|
||||
{ backgroundColor: theme.primary, marginBottom: bottomInset },
|
||||
]}
|
||||
small
|
||||
color={theme.onPrimary}
|
||||
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,
|
||||
})
|
||||
}
|
||||
onPress={() => {
|
||||
navigation.navigate(
|
||||
'Chapter',
|
||||
openChapter(novel, lastReadChapter),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Portal>
|
||||
@@ -701,9 +708,9 @@ const Novel = ({ route, navigation }) => {
|
||||
<JumpToChapterModal
|
||||
modalVisible={jumpToChapterModal}
|
||||
hideModal={() => showJumpToChapterModal(false)}
|
||||
theme={theme}
|
||||
chapters={chapters}
|
||||
novel={novel}
|
||||
chapterListRef={flatlistRef.current}
|
||||
navigation={navigation}
|
||||
/>
|
||||
<EditInfoModal
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ActivityIndicator } from 'react-native';
|
||||
|
||||
import { IconButton, Menu } from 'react-native-paper';
|
||||
import { IconButton, Menu, overlay } from 'react-native-paper';
|
||||
|
||||
export const DownloadButton = ({
|
||||
downloadQueue,
|
||||
@@ -23,7 +23,7 @@ export const DownloadButton = ({
|
||||
anchor={
|
||||
<DeleteChapterButton theme={theme} onPress={showDeleteChapterMenu} />
|
||||
}
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
contentStyle={{ backgroundColor: overlay(2, theme.surface) }}
|
||||
>
|
||||
<Menu.Item
|
||||
onPress={() => deleteChapter(chapter.chapterId, chapter.chapterName)}
|
||||
|
||||
@@ -202,7 +202,7 @@ const TrackerButton = ({ theme, isTracked, onPress }) => (
|
||||
);
|
||||
|
||||
const NovelGenres = ({ theme, genre }) => {
|
||||
const data = genre.split(',');
|
||||
const data = genre.split(/,\s*/);
|
||||
|
||||
const renderItem = ({ item }) => <Chip label={item} theme={theme} />;
|
||||
|
||||
@@ -250,7 +250,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
novelTitle: {
|
||||
fontSize: 18,
|
||||
fontSize: 20,
|
||||
},
|
||||
novelAuthor: {
|
||||
marginVertical: 6,
|
||||
|
||||