Compare commits
99 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 | |||
| d749225820 | |||
| d6d487101b | |||
| ba62c1ba0a | |||
| 75f4f18448 | |||
| f9ebec4a30 | |||
| de18e3d9d8 | |||
| acaac74db4 | |||
| d8845661a7 | |||
| b66a6130c5 | |||
| 220f25bf72 | |||
| 934edbb84c | |||
| 0d8a5d6172 | |||
| e706745f30 | |||
| dce3652ae8 | |||
| 0ca3b8ce15 | |||
| c62db5cb23 | |||
| e33b433e84 | |||
| c95f376e20 | |||
| 9b45375638 | |||
| 7fbdc6732c | |||
| 3f9549d320 | |||
| 079010b6ca | |||
| 107e534565 | |||
| f0665866ae | |||
| 706b4581b1 | |||
| a8d9c3abfa | |||
| 66798c46db | |||
| 87fa081cfd | |||
| ea26cd67e4 | |||
| 9400fcd252 | |||
| 30ca05df06 | |||
| eac3ee32c8 | |||
| 0cf249eec5 | |||
| 27e5300f4f | |||
| 97e02177a4 | |||
| 42b37f87fe | |||
| 4ad1c0253b | |||
| a57ed7a7a2 | |||
| 9fa00d9d9b | |||
| 18f63a217f | |||
| 05401ada6b | |||
| e256537079 | |||
| 0eec38f26f | |||
| fa90d57eff |
@@ -0,0 +1,94 @@
|
||||
# Cloned from tachiyomi
|
||||
|
||||
name: Issue report
|
||||
description: Report an issue in LNReader
|
||||
labels: [Bug]
|
||||
body:
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Provide an example of the issue.
|
||||
placeholder: |
|
||||
Example:
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Issue here
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Explain what you should expect to happen.
|
||||
placeholder: |
|
||||
Example:
|
||||
"This should happen..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: Explain what actually happens.
|
||||
placeholder: |
|
||||
Example:
|
||||
"This happened instead..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: lnreader-version
|
||||
attributes:
|
||||
label: LNReader version
|
||||
description: You can find your LNReader version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "1.1.14"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: Android version
|
||||
description: You can find this somewhere in your Android settings.
|
||||
placeholder: |
|
||||
Example: "Android 11"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device
|
||||
description: List your device and model.
|
||||
placeholder: |
|
||||
Example: "Google Pixel 5"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
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.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
|
||||
@@ -0,0 +1,40 @@
|
||||
# Cloned from tachiyomi
|
||||
|
||||
name: Feature request
|
||||
description: Suggest a feature to improve LNReader
|
||||
labels: [Feature Request]
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Describe your suggested feature
|
||||
description: How can LNReader be improved?
|
||||
placeholder: |
|
||||
Example:
|
||||
"It should work like this..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
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.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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Cloned from tachiyomi
|
||||
|
||||
name: Issue moderator
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
moderate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: tachiyomiorg/issue-moderator-action@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-close-rules: |
|
||||
[
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*\\* (LNReader version|Android version|Device): \\?.*",
|
||||
"message": "Requested information in the template was not filled out."
|
||||
}
|
||||
]
|
||||
@@ -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 { createDB } from './src/database/db';
|
||||
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();
|
||||
createDB();
|
||||
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>
|
||||
|
||||
@@ -6,6 +6,7 @@ buildscript {
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
ndkVersion = "21.1.6352462"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
@@ -21,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'
|
||||
|
||||
@@ -6,17 +6,21 @@ module.exports = function (api) {
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
root: ['./src'],
|
||||
alias: {
|
||||
'@components': './src/components',
|
||||
'@database': './src/database',
|
||||
'@hooks': './src/hooks',
|
||||
'@screens': './src/screens',
|
||||
'@strings': './strings',
|
||||
'@redux': './src/redux',
|
||||
'@services': './src/services',
|
||||
'@sources': './src/sources',
|
||||
'@utils': './src/utils',
|
||||
'@theme': './src/theme',
|
||||
},
|
||||
extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
|
||||
},
|
||||
],
|
||||
'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,24 +8,31 @@
|
||||
"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",
|
||||
"sources:madara": "node scripts/generateMadaraSource.cjs && prettier --write {src/sources/multisrc/madara/MadaraGenerator.ts,src/sources/sources.json}",
|
||||
"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",
|
||||
"@reduxjs/toolkit": "^1.7.2",
|
||||
"@shopify/flash-list": "^1.2.1",
|
||||
"buffer": "^6.0.3",
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"color": "^4.2.3",
|
||||
"dayjs": "^1.11.4",
|
||||
"expo": "^41.0.1",
|
||||
"expo-document-picker": "^9.2.4",
|
||||
"expo-file-system": "~11.0.2",
|
||||
@@ -35,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",
|
||||
"moment": "^2.29.4",
|
||||
"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": "^4.10.1",
|
||||
"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",
|
||||
@@ -68,12 +78,12 @@
|
||||
"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": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@react-native-community/eslint-config": "^3.0.1",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/i18n-js": "^3.8.2",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/lodash": "^4.14.179",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const prettier = require('prettier');
|
||||
const prettierConfig = require('../.prettierrc');
|
||||
|
||||
const MadaraSources = require('../src/sources/multisrc/madara/MadaraSources.json');
|
||||
const AllSources = require('../src/sources/sources.json');
|
||||
@@ -7,6 +9,7 @@ const AllSources = require('../src/sources/sources.json');
|
||||
const getScraperName = scraperName => {
|
||||
let name = scraperName
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/-/g, 'Dash')
|
||||
.replace(/\./g, 'Dot')
|
||||
.replace('1st', 'First');
|
||||
|
||||
@@ -26,6 +29,11 @@ MadaraSources.forEach(source => {
|
||||
}", ${source.options ? JSON.stringify(source.options) : ''});\n\n`;
|
||||
});
|
||||
|
||||
content = prettier.format(content, {
|
||||
parser: 'typescript',
|
||||
...prettierConfig,
|
||||
});
|
||||
|
||||
fs.writeFile(
|
||||
path.resolve(process.cwd(), 'src/sources/multisrc/madara/MadaraGenerator.ts'),
|
||||
content,
|
||||
@@ -52,6 +60,12 @@ MadaraSources.forEach(madaraSource => {
|
||||
|
||||
allSourcesContent = JSON.stringify(allSourcesContent);
|
||||
|
||||
allSourcesContent = prettier.format(allSourcesContent, {
|
||||
parser: 'json5',
|
||||
quoteProps: 'preserve',
|
||||
trailingComma: 'none',
|
||||
});
|
||||
|
||||
fs.writeFile(
|
||||
path.resolve(process.cwd(), 'src/sources/sources.json'),
|
||||
allSourcesContent,
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Dimensions, StyleSheet, View } from 'react-native';
|
||||
|
||||
import { IconButton } from 'react-native-paper';
|
||||
|
||||
export const Actionbar = ({ active, actions, theme, style }) => {
|
||||
if (active) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.actionbarContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
style,
|
||||
]}
|
||||
>
|
||||
{actions.map(
|
||||
(action, index) =>
|
||||
action.icon && (
|
||||
<IconButton
|
||||
key={index}
|
||||
icon={action.icon}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={action.onPress}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
actionbarContainer: {
|
||||
position: 'absolute',
|
||||
width: Dimensions.get('window').width - 32,
|
||||
bottom: 0,
|
||||
margin: 16,
|
||||
borderRadius: 6,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import React from 'react';
|
||||
import {
|
||||
Dimensions,
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewProps,
|
||||
} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import color from 'color';
|
||||
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
type Action = {
|
||||
icon: string;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
interface ActionbarProps {
|
||||
active: boolean;
|
||||
actions: Action[];
|
||||
viewStyle?: StyleProp<ViewProps>;
|
||||
}
|
||||
|
||||
export const Actionbar: React.FC<ActionbarProps> = ({
|
||||
active,
|
||||
actions,
|
||||
viewStyle,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
|
||||
if (active) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.actionbarContainer,
|
||||
{
|
||||
backgroundColor: theme.isDark
|
||||
? overlay(2, theme.surface)
|
||||
: theme.secondaryContainer,
|
||||
minHeight: 80 + bottom,
|
||||
paddingBottom: bottom,
|
||||
},
|
||||
viewStyle,
|
||||
]}
|
||||
>
|
||||
{actions.map(({ icon, onPress }, id) => (
|
||||
<Pressable
|
||||
key={id}
|
||||
android_ripple={{
|
||||
radius: 50,
|
||||
color: color(theme.primary).alpha(0.12).string(),
|
||||
borderless: true,
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={theme.onSurface}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
actionbarContainer: {
|
||||
position: 'absolute',
|
||||
width: Dimensions.get('window').width,
|
||||
bottom: 0,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
elevation: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Appbar as MaterialAppbar } from 'react-native-paper';
|
||||
|
||||
import { useTheme } from '../hooks/reduxHooks';
|
||||
import { ViewPropTypes } from 'react-native';
|
||||
|
||||
export const Appbar = ({ title, onBackAction, style, children }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<MaterialAppbar.Header
|
||||
style={[{ backgroundColor: theme.colorPrimary }, style]}
|
||||
>
|
||||
{onBackAction && <MaterialAppbar.BackAction onPress={onBackAction} />}
|
||||
<MaterialAppbar.Content
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
{children}
|
||||
</MaterialAppbar.Header>
|
||||
);
|
||||
};
|
||||
|
||||
Appbar.propTypes = {
|
||||
title: PropTypes.string,
|
||||
onBackAction: PropTypes.func,
|
||||
style: ViewPropTypes.style,
|
||||
};
|
||||
@@ -2,24 +2,38 @@ import React from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
|
||||
import { Appbar as PaperAppbar } from 'react-native-paper';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface AppbarProps {
|
||||
title: string;
|
||||
handleGoBack: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
mode?: 'small' | 'medium' | 'large' | 'center-aligned';
|
||||
}
|
||||
|
||||
const Appbar: React.FC<AppbarProps> = ({ title, handleGoBack, theme }) => (
|
||||
const Appbar: React.FC<AppbarProps> = ({
|
||||
title,
|
||||
handleGoBack,
|
||||
theme,
|
||||
mode = 'large',
|
||||
children,
|
||||
}) => (
|
||||
<PaperAppbar.Header
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
style={{ backgroundColor: theme.surface }}
|
||||
statusBarHeight={StatusBar.currentHeight}
|
||||
mode={mode}
|
||||
>
|
||||
{handleGoBack && <PaperAppbar.BackAction onPress={handleGoBack} />}
|
||||
{handleGoBack && (
|
||||
<PaperAppbar.BackAction
|
||||
onPress={handleGoBack}
|
||||
iconColor={theme.textColorPrimary}
|
||||
/>
|
||||
)}
|
||||
<PaperAppbar.Content
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
{children}
|
||||
</PaperAppbar.Header>
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Pressable, StyleSheet, Text, View, ViewStyle } from 'react-native';
|
||||
import React from 'react';
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import React, { useMemo } from 'react';
|
||||
import color from 'color';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
export enum ButtonVariation {
|
||||
DEFAULT = 'default',
|
||||
@@ -14,8 +23,10 @@ interface Props {
|
||||
title: string;
|
||||
textColor?: string;
|
||||
onPress?: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
style?: ViewStyle;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Button: React.FC<Props> = ({
|
||||
@@ -25,7 +36,24 @@ const Button: React.FC<Props> = ({
|
||||
onPress,
|
||||
theme,
|
||||
style,
|
||||
disabled,
|
||||
labelStyle,
|
||||
}) => {
|
||||
const textStyles: StyleProp<TextStyle> = useMemo(
|
||||
() => ({
|
||||
color: disabled
|
||||
? theme.textColorHint
|
||||
: textColor
|
||||
? textColor
|
||||
: variation === ButtonVariation.OUTLINED
|
||||
? theme.primary
|
||||
: variation === ButtonVariation.CLEAR
|
||||
? theme.primary
|
||||
: theme.onSecondary,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -36,7 +64,7 @@ const Button: React.FC<Props> = ({
|
||||
borderColor: theme.textColorHint,
|
||||
}
|
||||
: variation !== ButtonVariation.CLEAR && {
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.secondary,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
@@ -44,20 +72,10 @@ const Button: React.FC<Props> = ({
|
||||
<Pressable
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: textColor
|
||||
? textColor
|
||||
: variation === ButtonVariation.OUTLINED
|
||||
? theme.colorAccent
|
||||
: variation === ButtonVariation.CLEAR
|
||||
? theme.colorAccent
|
||||
: theme.textColorPrimary,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text style={[textStyles, labelStyle]} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text } from 'react-native';
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { Checkbox as PaperCheckbox } from 'react-native-paper';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import color from 'color';
|
||||
|
||||
export const Checkbox = ({
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface CheckboxProps {
|
||||
label: string;
|
||||
status: boolean | 'indeterminate';
|
||||
onPress?: () => void;
|
||||
disabled?: boolean;
|
||||
theme: MD3ThemeType;
|
||||
viewStyle?: StyleProp<ViewStyle>;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
export const Checkbox: React.FC<CheckboxProps> = ({
|
||||
label,
|
||||
status,
|
||||
onPress,
|
||||
disabled = false,
|
||||
theme,
|
||||
disabled,
|
||||
onPress,
|
||||
viewStyle,
|
||||
labelStyle,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressable}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[styles.pressable, viewStyle]}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -26,11 +47,14 @@ export const Checkbox = ({
|
||||
: 'unchecked'
|
||||
}
|
||||
onPress={onPress}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
theme={{
|
||||
colors: { disabled: theme.textColorSecondary },
|
||||
}}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text style={{ color: theme.textColorSecondary, marginLeft: 12 }}>
|
||||
<Text style={[styles.defaultLabel, { color: theme.onSurface }, labelStyle]}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -38,19 +62,19 @@ export const Checkbox = ({
|
||||
|
||||
export const SortItem = ({ label, status, onPress, theme }) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[styles.pressable, { paddingVertical: 16, paddingLeft: 64 }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
{status && (
|
||||
<MaterialCommunityIcons
|
||||
name={status === 'asc' ? 'arrow-up' : 'arrow-down'}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
size={21}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
<Text style={{ color: theme.textColorPrimary }}>{label}</Text>
|
||||
<Text style={{ color: theme.onSurface }}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
@@ -66,4 +90,7 @@ const styles = StyleSheet.create({
|
||||
left: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
defaultLabel: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
@@ -1,20 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
interface ChipProps {
|
||||
label: string;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const Chip: React.FC<ChipProps> = ({ label, theme }) => (
|
||||
<View style={[styles.chipContainer, { backgroundColor: theme.colorPrimary }]}>
|
||||
<View
|
||||
style={[
|
||||
styles.chipContainer,
|
||||
{
|
||||
backgroundColor: theme.isDark
|
||||
? overlay(1, theme.surface)
|
||||
: theme.secondaryContainer,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={styles.pressable}
|
||||
>
|
||||
<Text style={[styles.label, { color: theme.textColorSecondary }]}>
|
||||
<Text
|
||||
style={[
|
||||
styles.label,
|
||||
{
|
||||
color: theme.isDark ? theme.onSurface : theme.onSecondaryContainer,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -27,7 +45,7 @@ const styles = StyleSheet.create({
|
||||
chipContainer: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 8,
|
||||
height: 28,
|
||||
height: 32,
|
||||
marginRight: 8,
|
||||
},
|
||||
pressable: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { Modal, Portal, TextInput } from 'react-native-paper';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { Modal, overlay, Portal, TextInput } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface ColorPickerModalProps {
|
||||
visible: boolean;
|
||||
@@ -10,7 +10,7 @@ interface ColorPickerModalProps {
|
||||
color: string;
|
||||
onSubmit: (val: string) => void;
|
||||
closeModal: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
showAccentColors?: boolean;
|
||||
}
|
||||
|
||||
@@ -47,15 +47,6 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const textInputTheme = {
|
||||
colors: {
|
||||
primary: color,
|
||||
placeholder: theme.textColorHint,
|
||||
text: theme.textColorPrimary,
|
||||
background: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
const accentColors = [
|
||||
'#EF5350',
|
||||
'#EC407A',
|
||||
@@ -86,7 +77,7 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
{ backgroundColor: overlay(2, theme.surface) },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalTitle, { color: theme.textColorPrimary }]}>
|
||||
@@ -131,7 +122,7 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
mode="outlined"
|
||||
theme={textInputTheme}
|
||||
theme={{ colors: { ...theme } }}
|
||||
underlineColor={theme.textColorHint}
|
||||
dense
|
||||
error={Boolean(error)}
|
||||
@@ -147,11 +138,11 @@ export default ColorPickerModal;
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
margin: 30,
|
||||
padding: 20,
|
||||
borderRadius: 8,
|
||||
padding: 24,
|
||||
borderRadius: 28,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontSize: 24,
|
||||
marginBottom: 16,
|
||||
},
|
||||
errorText: {
|
||||
|
||||
@@ -18,7 +18,7 @@ const ScreenContainer = ({ children, theme }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Row = ({ children, style }) => (
|
||||
const Row = ({ children, style = {} }) => (
|
||||
<View style={[styles.row, style]}>{children}</View>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import Color from 'color';
|
||||
|
||||
interface ToggleButtonProps {
|
||||
icon: string;
|
||||
selected: boolean;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
color?: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
@@ -26,19 +27,21 @@ export const ToggleButton: React.FC<ToggleButtonProps> = ({
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: Color(theme.primary).alpha(0.12).string() }}
|
||||
style={{
|
||||
padding: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: selected ? theme.rippleColor : 'transparent',
|
||||
backgroundColor: selected
|
||||
? Color(theme.primary).alpha(0.12).string()
|
||||
: 'transparent',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={
|
||||
selected ? theme.colorAccent : color ? color : theme.textColorPrimary
|
||||
selected ? theme.primary : color ? color : theme.textColorPrimary
|
||||
}
|
||||
size={24}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { Dialog, Portal } from 'react-native-paper';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import { Button } from '@components/index';
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
import { getDialogBackground } from '@theme/colors';
|
||||
|
||||
interface ConfirmationDialogProps {
|
||||
title: string;
|
||||
visible: boolean;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
onSubmit: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
@@ -33,26 +34,28 @@ const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
style={[styles.container, { backgroundColor: theme.colorPrimary }]}
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: getDialogBackground(theme) },
|
||||
]}
|
||||
>
|
||||
<Dialog.Title style={[styles.title, { color: theme.textColorPrimary }]}>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<View style={styles.buttonCtn}>
|
||||
<Button
|
||||
theme={theme}
|
||||
onPress={handleOnSubmit}
|
||||
title={getString('common.ok')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
/>
|
||||
<Button
|
||||
theme={theme}
|
||||
onPress={onDismiss}
|
||||
title={getString('common.cancel')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
/>
|
||||
<Button
|
||||
theme={theme}
|
||||
onPress={handleOnSubmit}
|
||||
title={getString('common.ok')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
style={styles.button}
|
||||
/>
|
||||
</Dialog.Actions>
|
||||
</View>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
@@ -62,13 +65,14 @@ export default ConfirmationDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 8,
|
||||
borderRadius: 28,
|
||||
},
|
||||
title: {
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
marginLeft: 4,
|
||||
buttonCtn: {
|
||||
flexDirection: 'row-reverse',
|
||||
padding: 16,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
const EmptyView = ({ icon, description, style, children, iconStyle }) => {
|
||||
const theme = useSelector(state => state.settingsReducer.theme);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View style={styles.emptyViewContainer}>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface EmptyViewProps {
|
||||
icon?: string;
|
||||
description: string;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const EmptyView: React.FC<EmptyViewProps> = ({ icon, description, theme }) => (
|
||||
@@ -25,6 +25,7 @@ export default EmptyView;
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
|
||||
interface ErrorScreenProps {
|
||||
error?: string;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error, theme }) => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import Color from 'color';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
@@ -11,7 +12,7 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
padding?: number;
|
||||
onPress?: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
@@ -30,7 +31,11 @@ const IconButton: React.FC<Props> = ({
|
||||
style={[styles.pressable, { padding }]}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={
|
||||
onPress
|
||||
? { color: Color(theme.primary).alpha(0.12).string() }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={name}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
@@ -9,9 +10,7 @@ const ListSection = ({ children }) => (
|
||||
);
|
||||
|
||||
const ListSubHeader = ({ children, theme }) => (
|
||||
<PaperList.Subheader
|
||||
style={[styles.listSubHeader, { color: theme.colorAccent }]}
|
||||
>
|
||||
<PaperList.Subheader style={[styles.listSubHeader, { color: theme.primary }]}>
|
||||
{children}
|
||||
</PaperList.Subheader>
|
||||
);
|
||||
@@ -29,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.colorAccent}
|
||||
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
|
||||
@@ -58,12 +59,20 @@ const ListItem = ({
|
||||
)
|
||||
}
|
||||
onPress={onPress}
|
||||
rippleColor={theme.rippleColor}
|
||||
rippleColor={color(theme.primary).alpha(0.12).string()}
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({ theme }) => (
|
||||
<PaperDivider style={{ backgroundColor: theme.dividerColor, height: 1 }} />
|
||||
<PaperDivider
|
||||
style={{
|
||||
backgroundColor: color(theme.isDark ? '#FFFFFF' : '#000000')
|
||||
.alpha(0.12)
|
||||
.rgb()
|
||||
.string(),
|
||||
height: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const InfoItem = ({ title, icon, theme }) => (
|
||||
@@ -85,7 +94,7 @@ const ColorItem = ({ title, description, theme, onPress }) => (
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View>
|
||||
@@ -107,7 +116,7 @@ const ColorItem = ({ title, description, theme, onPress }) => (
|
||||
);
|
||||
|
||||
const Icon = ({ icon, theme }) => (
|
||||
<PaperList.Icon color={theme.colorAccent} icon={icon} style={{ margin: 0 }} />
|
||||
<PaperList.Icon color={theme.primary} icon={icon} style={{ margin: 0 }} />
|
||||
);
|
||||
|
||||
export const List = {
|
||||
@@ -126,6 +135,7 @@ const styles = StyleSheet.create({
|
||||
marginVertical: 0,
|
||||
},
|
||||
listSubHeader: {
|
||||
fontWeight: 'bold',
|
||||
paddingBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
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 { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import { dividerColor } from '../../theme/colors';
|
||||
|
||||
interface ListItemProps {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
icon?: string;
|
||||
onPress?: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const Section = ({ children }: { children: ReactNode }) => (
|
||||
@@ -21,9 +24,9 @@ const SubHeader = ({
|
||||
theme,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}) => (
|
||||
<PaperList.Subheader style={{ color: theme.colorAccent }}>
|
||||
<PaperList.Subheader style={{ color: theme.primary }}>
|
||||
{children}
|
||||
</PaperList.Subheader>
|
||||
);
|
||||
@@ -44,7 +47,7 @@ const Item: React.FC<ListItemProps> = ({
|
||||
<View style={styles.iconContainer}>
|
||||
{icon && (
|
||||
<PaperList.Icon
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
icon={icon}
|
||||
style={styles.icon}
|
||||
/>
|
||||
@@ -52,33 +55,35 @@ const Item: React.FC<ListItemProps> = ({
|
||||
</View>
|
||||
)}
|
||||
onPress={onPress}
|
||||
rippleColor={theme.rippleColor}
|
||||
rippleColor={color(theme.primary).alpha(0.12).string()}
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({ theme }: { theme: ThemeType }) => (
|
||||
const Divider = ({ theme }: { theme: MD3ThemeType }) => (
|
||||
<PaperDivider
|
||||
style={[styles.divider, { backgroundColor: theme.dividerColor }]}
|
||||
style={[styles.divider, { backgroundColor: dividerColor(theme.isDark) }]}
|
||||
/>
|
||||
);
|
||||
|
||||
const InfoItem = ({
|
||||
title,
|
||||
icon,
|
||||
icon = 'information-outline',
|
||||
theme,
|
||||
}: {
|
||||
title: string;
|
||||
icon: string;
|
||||
theme: ThemeType;
|
||||
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 {
|
||||
@@ -87,6 +92,7 @@ export default {
|
||||
Item,
|
||||
Divider,
|
||||
InfoItem,
|
||||
Icon,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -103,7 +109,12 @@ const styles = StyleSheet.create({
|
||||
icon: {
|
||||
marginVertical: 0,
|
||||
},
|
||||
infoItem: {
|
||||
fontSize: 14,
|
||||
infoCtn: {
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
infoMsg: {
|
||||
marginTop: 12,
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import FastImage from 'react-native-fast-image';
|
||||
import { defaultCoverUri } from '../sources/helpers/constants';
|
||||
import { coverPlaceholderColor } from '../theme/colors';
|
||||
|
||||
import color from 'color';
|
||||
|
||||
const ListView = ({
|
||||
item,
|
||||
downloadBadge,
|
||||
@@ -17,10 +19,12 @@ const ListView = ({
|
||||
}) => {
|
||||
return (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[
|
||||
styles.listView,
|
||||
isSelected && { backgroundColor: theme.rippleColor },
|
||||
isSelected && {
|
||||
backgroundColor: color(theme.primary).alpha(0.12).string(),
|
||||
},
|
||||
]}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, StyleSheet } from 'react-native';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface Props {
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const LoadingMoreIndicator: React.FC<Props> = ({ theme }) => (
|
||||
<ActivityIndicator color={theme.colorAccent} style={styles.indicator} />
|
||||
<ActivityIndicator color={theme.primary} style={styles.indicator} />
|
||||
);
|
||||
|
||||
export default LoadingMoreIndicator;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, View } from 'react-native';
|
||||
|
||||
export const LoadingScreen = ({ theme }) => (
|
||||
<View style={{ flex: 1, justifyContent: 'center' }}>
|
||||
<ActivityIndicator size={50} color={theme.colorAccent} />
|
||||
</View>
|
||||
);
|
||||
@@ -1,14 +1,10 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, ActivityIndicator } from 'react-native';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
const LoadingScreen: React.FC<{ theme: ThemeType }> = ({ theme }) => (
|
||||
<ActivityIndicator
|
||||
size={50}
|
||||
color={theme.colorAccent}
|
||||
style={styles.indicator}
|
||||
/>
|
||||
const LoadingScreen: React.FC<{ theme: MD3ThemeType }> = ({ theme }) => (
|
||||
<ActivityIndicator size={50} color={theme.primary} style={styles.indicator} />
|
||||
);
|
||||
|
||||
export default LoadingScreen;
|
||||
|
||||
@@ -2,11 +2,12 @@ import React, { useState } from 'react';
|
||||
import { StyleSheet, View, Text, Dimensions } from 'react-native';
|
||||
import { Portal, Modal } from 'react-native-paper';
|
||||
import * as Linking from 'expo-linking';
|
||||
import { useTheme } from '../hooks/reduxHooks';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import Button, { ButtonVariation } from './Button/Button';
|
||||
import { getString } from '@strings/translations';
|
||||
import Markdown from 'react-native-markdown-display';
|
||||
import { dividerColor, getDialogBackground } from '@theme/colors';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
|
||||
interface NewUpdateDialogProps {
|
||||
newVersion: {
|
||||
@@ -30,7 +31,7 @@ const NewUpdateDialog: React.FC<NewUpdateDialogProps> = ({ newVersion }) => {
|
||||
onDismiss={() => showNewUpdateDialog(false)}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
{ backgroundColor: getDialogBackground(theme) },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalHeader, { color: theme.textColorPrimary }]}>
|
||||
@@ -45,11 +46,11 @@ const NewUpdateDialog: React.FC<NewUpdateDialogProps> = ({ newVersion }) => {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
hr: {
|
||||
backgroundColor: theme.dividerColor,
|
||||
backgroundColor: dividerColor(theme.isDark),
|
||||
marginVertical: 16,
|
||||
},
|
||||
code_inline: {
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
backgroundColor: getDialogBackground(theme),
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
@@ -6,15 +6,16 @@ import {
|
||||
useWindowDimensions,
|
||||
Pressable,
|
||||
} from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import ListView from './ListView';
|
||||
|
||||
import { useDeviceOrientation } from '../services/utils/helpers';
|
||||
import { useSettings } from '../hooks/reduxHooks';
|
||||
import { coverPlaceholderColor } from '../theme/colors';
|
||||
import { useLibrarySettings } from '@hooks/useSettings';
|
||||
import { DisplayModes } from '@screens/library/constants/constants';
|
||||
|
||||
const NovelCover = ({
|
||||
item,
|
||||
@@ -25,43 +26,48 @@ const NovelCover = ({
|
||||
onLongPress,
|
||||
selectedNovels,
|
||||
}) => {
|
||||
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
|
||||
useSettings();
|
||||
const {
|
||||
displayMode = DisplayModes.Comfortable,
|
||||
showDownloadBadges = true,
|
||||
showUnreadBadges = true,
|
||||
novelsPerRow = 3,
|
||||
} = useLibrarySettings();
|
||||
|
||||
const window = useWindowDimensions();
|
||||
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const getNovelsPerRow = () =>
|
||||
orientation === 'landscape' ? 6 : novelsPerRow;
|
||||
const numColumns = useMemo(
|
||||
() => (orientation === 'landscape' ? 6 : novelsPerRow),
|
||||
[orientation, novelsPerRow],
|
||||
);
|
||||
|
||||
const getHeight = useCallback(
|
||||
() => (window.width / getNovelsPerRow()) * (4 / 3),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
const coverHeight = useMemo(
|
||||
() => (window.width / numColumns) * (4 / 3),
|
||||
[numColumns],
|
||||
);
|
||||
|
||||
const selectNovel = () => onLongPress && onLongPress(item.novelId);
|
||||
|
||||
const uri = item.novelCover;
|
||||
|
||||
return displayMode !== 2 ? (
|
||||
return displayMode !== DisplayModes.List ? (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flex: 1 / getNovelsPerRow(),
|
||||
flex: 1 / numColumns,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
margin: 2,
|
||||
},
|
||||
isSelected && {
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.primary,
|
||||
opacity: 0.8,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={styles.opac}
|
||||
onPress={
|
||||
selectedNovels && selectedNovels.length > 0 ? selectNovel : onPress
|
||||
@@ -75,6 +81,7 @@ const NovelCover = ({
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
{showUnreadBadges && item.chaptersUnread && (
|
||||
@@ -90,7 +97,7 @@ const NovelCover = ({
|
||||
source={{ uri }}
|
||||
style={[
|
||||
{
|
||||
height: getHeight(),
|
||||
height: coverHeight,
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
@@ -98,9 +105,11 @@ const NovelCover = ({
|
||||
]}
|
||||
/>
|
||||
<View style={styles.compactTitleContainer}>
|
||||
{displayMode === 0 && <CompactTitle novelName={item.novelName} />}
|
||||
{displayMode === DisplayModes.Compact && (
|
||||
<CompactTitle novelName={item.novelName} />
|
||||
)}
|
||||
</View>
|
||||
{displayMode === 1 && (
|
||||
{displayMode === DisplayModes.Comfortable && (
|
||||
<ComfortableTitle novelName={item.novelName} theme={theme} />
|
||||
)}
|
||||
</Pressable>
|
||||
@@ -112,6 +121,7 @@ const NovelCover = ({
|
||||
showDownloadBadges &&
|
||||
item.chaptersDownloaded && (
|
||||
<DownloadBadge
|
||||
theme={theme}
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
@@ -186,8 +196,8 @@ const InLibraryBadge = ({ theme }) => (
|
||||
style={[
|
||||
styles.inLibraryBadge,
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
backgroundColor: theme.primary,
|
||||
color: theme.onPrimary,
|
||||
borderRadius: 4,
|
||||
},
|
||||
]}
|
||||
@@ -213,8 +223,8 @@ const UnreadBadge = ({
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
backgroundColor: theme.primary,
|
||||
color: theme.onPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -226,6 +236,7 @@ const DownloadBadge = ({
|
||||
chaptersDownloaded,
|
||||
showUnreadBadges,
|
||||
chaptersUnread,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
@@ -237,6 +248,10 @@ const DownloadBadge = ({
|
||||
!showUnreadBadges && {
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
backgroundColor: theme.tertiary,
|
||||
color: theme.onTertiary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{chaptersDownloaded}
|
||||
@@ -276,16 +291,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
|
||||
downloadBadge: {
|
||||
backgroundColor: '#2979FF',
|
||||
color: '#FFFFFF',
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
paddingHorizontal: 5,
|
||||
fontSize: 12,
|
||||
},
|
||||
unreadBadge: {
|
||||
color: '#FFFFFF',
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
paddingTop: 2,
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { StyleSheet, FlatList } from 'react-native';
|
||||
|
||||
import { useSettings } from '../hooks/reduxHooks';
|
||||
import { useDeviceOrientation } from '../services/utils/helpers';
|
||||
|
||||
const NovelList = ({
|
||||
data,
|
||||
onScroll,
|
||||
onEndReached,
|
||||
renderItem,
|
||||
refreshControl,
|
||||
ListEmptyComponent,
|
||||
ListFooterComponent,
|
||||
}) => {
|
||||
const { displayMode, novelsPerRow } = useSettings();
|
||||
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const getNovelsPerRow = () => {
|
||||
if (displayMode === 2) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (orientation === 'landscape') {
|
||||
return 6;
|
||||
} else {
|
||||
return novelsPerRow;
|
||||
}
|
||||
};
|
||||
|
||||
const keyExtractor = useCallback(item => item.sourceId + item.novelUrl, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={[
|
||||
styles.flatListCont,
|
||||
displayMode !== 2 && { paddingHorizontal: 4 },
|
||||
]}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={[orientation, getNovelsPerRow()]}
|
||||
data={data}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
refreshControl={refreshControl}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onScroll={onScroll}
|
||||
onEndReached={onEndReached}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flatListCont: {
|
||||
flexGrow: 1,
|
||||
paddingBottom: 56,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useLibrarySettings } from '@hooks/useSettings';
|
||||
import { DisplayModes } from '@screens/library/constants/constants';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
FlatListProps,
|
||||
ListRenderItem,
|
||||
} from 'react-native';
|
||||
import { SourceNovelItem } from '../sources/types';
|
||||
import { LibraryNovelInfo, NovelInfo } from '../database/types';
|
||||
|
||||
import { useDeviceOrientation } from '../services/utils/helpers';
|
||||
|
||||
export type NovelListRenderItem = ListRenderItem<
|
||||
LibraryNovelInfo | NovelInfo | SourceNovelItem
|
||||
>;
|
||||
|
||||
const NovelList: React.FC<
|
||||
FlatListProps<LibraryNovelInfo | NovelInfo | SourceNovelItem>
|
||||
> = props => {
|
||||
const { displayMode = DisplayModes.Comfortable, novelsPerRow = 3 } =
|
||||
useLibrarySettings();
|
||||
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const isListView = displayMode === DisplayModes.List;
|
||||
|
||||
const numColumns = useMemo(() => {
|
||||
if (isListView) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (orientation === 'landscape') {
|
||||
return 6;
|
||||
} else {
|
||||
return novelsPerRow;
|
||||
}
|
||||
}, [isListView, orientation, novelsPerRow]);
|
||||
|
||||
const keyExtractor = useCallback(item => item.sourceId + item.novelUrl, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={[
|
||||
!isListView && styles.listView,
|
||||
styles.flatListCont,
|
||||
]}
|
||||
numColumns={numColumns}
|
||||
key={numColumns}
|
||||
keyExtractor={keyExtractor}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flatListCont: {
|
||||
flexGrow: 1,
|
||||
paddingBottom: 56,
|
||||
},
|
||||
listView: {
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FlatList, FlatListProps, StyleSheet } from 'react-native';
|
||||
|
||||
import { useAppearanceSettings } from '../../redux/hooks';
|
||||
import { useDeviceOrientation } from '../../hooks';
|
||||
|
||||
import { LibraryNovelInfo, NovelInfo } from '../../database/types';
|
||||
import { SourceNovelItem } from '../../sources/types';
|
||||
import { DeviceOrientation } from '../../hooks/useDeviceOrientation';
|
||||
|
||||
const NovelList: React.FC<
|
||||
FlatListProps<SourceNovelItem | NovelInfo | LibraryNovelInfo>
|
||||
> = props => {
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const { novelsPerRowPotrait, novelsPerRowLandscape } =
|
||||
useAppearanceSettings();
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.container}
|
||||
numColumns={
|
||||
orientation === DeviceOrientation.LANDSCAPE
|
||||
? novelsPerRowLandscape
|
||||
: novelsPerRowPotrait
|
||||
}
|
||||
key={[orientation, novelsPerRowPotrait] as any}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
padding: 4,
|
||||
},
|
||||
});
|
||||
@@ -12,7 +12,7 @@ export const RadioButton = ({ value, label, theme, labelStyle }) => (
|
||||
<View style={styles.radioButtonContainer}>
|
||||
<MaterialRadioButton
|
||||
value={value}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
/>
|
||||
<Text
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text } from 'react-native';
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { RadioButton as PaperRadioButton } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import color from 'color';
|
||||
|
||||
export const RadioButton = ({
|
||||
interface Props {
|
||||
label: string;
|
||||
status: boolean;
|
||||
onPress?: () => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
export const RadioButton: React.FC<Props> = ({
|
||||
label,
|
||||
status,
|
||||
onPress,
|
||||
@@ -11,19 +29,18 @@ export const RadioButton = ({
|
||||
theme,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[styles.pressable, style]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<PaperRadioButton
|
||||
status={status ? 'checked' : 'unchecked'}
|
||||
value={label}
|
||||
onPress={onPress}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
/>
|
||||
<Text
|
||||
style={[labelStyle, { color: theme.textColorPrimary, marginLeft: 12 }]}
|
||||
>
|
||||
<Text style={[styles.label, labelStyle, { color: theme.onSurface }]}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -41,4 +58,7 @@ const styles = StyleSheet.create({
|
||||
left: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
label: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
TextInput,
|
||||
StatusBar,
|
||||
Pressable,
|
||||
} from 'react-native';
|
||||
|
||||
import { Row } from '../Common';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
export const Searchbar = ({
|
||||
theme,
|
||||
placeholder,
|
||||
searchText,
|
||||
backAction,
|
||||
onBackAction,
|
||||
clearSearchbar,
|
||||
onChangeText,
|
||||
onSubmitEditing,
|
||||
actions,
|
||||
}) => {
|
||||
const searchRef = useRef(null);
|
||||
|
||||
return (
|
||||
<View style={styles.searchBar}>
|
||||
<Pressable
|
||||
onPress={() => searchRef.current.focus()}
|
||||
style={[styles.pressable, { backgroundColor: theme.searchBarColor }]}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Row style={{ flex: 1 }}>
|
||||
{backAction && (
|
||||
<IconButton
|
||||
icon={backAction}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={onBackAction}
|
||||
theme={theme}
|
||||
containerStyle={{ marginLeft: 0 }}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
color: theme.textColorPrimary,
|
||||
marginLeft: 4,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</Row>
|
||||
{searchText !== '' && (
|
||||
<IconButton
|
||||
icon="close"
|
||||
color={theme.textColorPrimary}
|
||||
onPress={clearSearchbar}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
{actions &&
|
||||
actions.map((action, index) => (
|
||||
<IconButton
|
||||
key={index}
|
||||
icon={action.icon}
|
||||
color={action.color ?? theme.textColorPrimary}
|
||||
onPress={action.onPress}
|
||||
theme={theme}
|
||||
containerStyle={{ marginRight: 0, marginLeft: 16 }}
|
||||
/>
|
||||
))}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchBar: {
|
||||
marginTop: StatusBar.currentHeight + 8,
|
||||
marginBottom: 8,
|
||||
marginHorizontal: 16,
|
||||
height: 46,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pressable: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Pressable, StyleSheet, View, TextInput } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import color from 'color';
|
||||
|
||||
import IconButtonV2 from '../IconButtonV2/IconButtonV2';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface RightIcon {
|
||||
iconName: string;
|
||||
@@ -21,7 +22,7 @@ interface SearcbarProps {
|
||||
handleBackAction?: () => void;
|
||||
clearSearchbar: () => void;
|
||||
onLeftIconPress?: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const Searchbar: React.FC<SearcbarProps> = ({
|
||||
@@ -39,24 +40,31 @@ 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
|
||||
style={[
|
||||
styles.searchbarContainer,
|
||||
{ marginTop, backgroundColor: theme.searchBarColor },
|
||||
{
|
||||
marginTop,
|
||||
marginRight,
|
||||
marginLeft,
|
||||
backgroundColor: color(theme.primary).alpha(0.08).string(),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
onPress={focusSearchbar}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(12).toString() }}
|
||||
style={[styles.searchbar]}
|
||||
>
|
||||
<IconButtonV2
|
||||
name={handleBackAction ? 'arrow-left' : leftIcon}
|
||||
color={theme.textColorPrimary}
|
||||
color={theme.onSurface}
|
||||
onPress={() => {
|
||||
if (handleBackAction) {
|
||||
handleBackAction();
|
||||
@@ -69,9 +77,9 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
|
||||
<TextInput
|
||||
ref={searchbarRef}
|
||||
style={[styles.textInput, { color: theme.textColorPrimary }]}
|
||||
style={[styles.textInput, { color: theme.onSurface }]}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
placeholderTextColor={theme.onSurface}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
defaultValue={searchText}
|
||||
@@ -79,7 +87,7 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
{searchText !== '' ? (
|
||||
<IconButtonV2
|
||||
name="close"
|
||||
color={theme.textColorPrimary}
|
||||
color={theme.onSurface}
|
||||
onPress={clearSearchbar}
|
||||
theme={theme}
|
||||
/>
|
||||
@@ -88,7 +96,7 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
<IconButtonV2
|
||||
key={index}
|
||||
name={icon.iconName}
|
||||
color={icon.color || theme.textColorPrimary}
|
||||
color={icon.color || theme.onSurface}
|
||||
onPress={icon.onPress}
|
||||
theme={theme}
|
||||
/>
|
||||
@@ -103,8 +111,8 @@ export default Searchbar;
|
||||
const styles = StyleSheet.create({
|
||||
searchbarContainer: {
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
minHeight: 46,
|
||||
marginBottom: 12,
|
||||
minHeight: 56,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
zIndex: 1,
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import color from 'color';
|
||||
import { Switch, List } from 'react-native-paper';
|
||||
|
||||
const SwitchSetting = ({ label, description, onPress, theme, icon, value }) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).rgb().string() }}
|
||||
style={[styles.container, { paddingVertical: description ? 16 : 12 }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', flex: 1 }}>
|
||||
{icon && (
|
||||
<List.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<List.Icon color={theme.primary} icon={icon} style={{ margin: 0 }} />
|
||||
)}
|
||||
<View style={[icon && { marginLeft: 16 }]}>
|
||||
<Text
|
||||
@@ -36,7 +32,7 @@ const SwitchSetting = ({ label, description, onPress, theme, icon, value }) => (
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onPress}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { Switch } from 'react-native-paper';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface SwitchItemProps {
|
||||
value: boolean;
|
||||
label: string;
|
||||
description?: string;
|
||||
onPress: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const SwitchItem: React.FC<SwitchItemProps> = ({
|
||||
@@ -20,7 +21,7 @@ const SwitchItem: React.FC<SwitchItemProps> = ({
|
||||
value,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[styles.container]}
|
||||
onPress={onPress}
|
||||
>
|
||||
@@ -35,7 +36,7 @@ const SwitchItem: React.FC<SwitchItemProps> = ({
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onPress}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
style={styles.switch}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { View, Text, /* StyleSheet, */ Pressable } from 'react-native';
|
||||
import { overlay } from 'react-native-paper';
|
||||
import color from 'color';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const ThemePicker = ({ theme, currentTheme, onPress }) => (
|
||||
@@ -12,12 +14,12 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
backgroundColor: theme.background,
|
||||
borderWidth: 3.6,
|
||||
borderColor:
|
||||
currentTheme.id === theme.id
|
||||
? theme.colorAccent
|
||||
: currentTheme.colorPrimaryDark,
|
||||
? theme.primary
|
||||
: currentTheme.background,
|
||||
width: 95,
|
||||
height: 140,
|
||||
borderRadius: 16,
|
||||
@@ -29,10 +31,10 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
|
||||
{currentTheme.id === theme.id && (
|
||||
<MaterialCommunityIcons
|
||||
name="check"
|
||||
color={theme.colorButtonText}
|
||||
color={theme.onPrimary}
|
||||
size={15}
|
||||
style={{
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.primary,
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
right: 5,
|
||||
@@ -45,7 +47,7 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
|
||||
<View
|
||||
style={{
|
||||
height: 20,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
backgroundColor: overlay(2, theme.surface),
|
||||
elevation: 1,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
@@ -82,7 +84,7 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
|
||||
height: 10,
|
||||
width: 16,
|
||||
marginLeft: 4,
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.primary,
|
||||
borderRadius: 50,
|
||||
}}
|
||||
/>
|
||||
@@ -114,7 +116,7 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 24,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
backgroundColor: color(theme.primary).alpha(0.08).string(),
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
@@ -141,7 +143,7 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
|
||||
height: 12,
|
||||
width: 12,
|
||||
borderRadius: 50,
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.primary,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
|
||||
@@ -4,7 +4,6 @@ export { default as LoadingScreenV2 } from './LoadingScreenV2/LoadingScreenV2';
|
||||
export { default as Container } from './Container/Container';
|
||||
export { default as ErrorScreenV2 } from './ErrorScreenV2/ErrorScreenV2';
|
||||
export { default as EmptyView } from './EmptyView/EmptyView';
|
||||
export { default as NovelList } from './NovelList/NovelList';
|
||||
export { default as Chip } from './Chip/Chip';
|
||||
export { default as Button } from './Button/Button';
|
||||
export { default as Appbar } from './Appbar/Appbar';
|
||||
@@ -12,3 +11,5 @@ export { default as SwitchItem } from './SwitchItem/SwitchItem';
|
||||
export { default as List } from './List/List';
|
||||
export { default as ColorPreferenceItem } from './ColorPreferenceItem/ColorPreferenceItem';
|
||||
export { default as LoadingMoreIndicator } from './LoadingMoreIndicator/LoadingMoreIndicator';
|
||||
export { Checkbox } from './Checkbox/Checkbox';
|
||||
export { RadioButton } from './RadioButton/RadioButton';
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createNovelTableQuery,
|
||||
createUrlIndexQuery,
|
||||
createLibraryIndexQuery,
|
||||
addCategoryColumnQuery,
|
||||
} from './tables/NovelTable';
|
||||
import {
|
||||
createChapterTableQuery,
|
||||
@@ -15,36 +16,83 @@ import {
|
||||
} from './tables/HistoryTable';
|
||||
import { createDownloadTableQuery } from './tables/DownloadTable';
|
||||
import { createUpdatesTableQuery } from './tables/UpdateTable';
|
||||
import {
|
||||
addCategorySortColumnQuery,
|
||||
createCategoriesTableQuery,
|
||||
createCategorydIndexQuery,
|
||||
createDefaultCategoryQuery,
|
||||
} from './tables/CategoryTable';
|
||||
import {
|
||||
dbTxnErrorCallback,
|
||||
txnErrorCallbackWithoutToast,
|
||||
} from './utils/helpers';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
const dbName = 'lnreader.db';
|
||||
|
||||
const db = SQLite.openDatabase(dbName);
|
||||
|
||||
export const createDB = () => {
|
||||
const createTables = () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(createNovelTableQuery);
|
||||
tx.executeSql(createCategoriesTableQuery, [], () => {
|
||||
tx.executeSql(
|
||||
addCategorySortColumnQuery,
|
||||
undefined,
|
||||
noop,
|
||||
txnErrorCallbackWithoutToast,
|
||||
);
|
||||
tx.executeSql(
|
||||
createDefaultCategoryQuery,
|
||||
undefined,
|
||||
noop,
|
||||
txnErrorCallbackWithoutToast,
|
||||
);
|
||||
});
|
||||
tx.executeSql(createNovelTableQuery, [], () => {
|
||||
tx.executeSql(
|
||||
addCategoryColumnQuery,
|
||||
undefined,
|
||||
noop,
|
||||
txnErrorCallbackWithoutToast,
|
||||
);
|
||||
});
|
||||
tx.executeSql(createChapterTableQuery);
|
||||
tx.executeSql(createHistoryTableQuery);
|
||||
tx.executeSql(createDownloadTableQuery);
|
||||
tx.executeSql(createUpdatesTableQuery);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Indexes
|
||||
*/
|
||||
const createIndexes = () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(createUrlIndexQuery);
|
||||
tx.executeSql(createLibraryIndexQuery);
|
||||
tx.executeSql(createNovelIdIndexQuery);
|
||||
tx.executeSql(createUnreadChaptersIndexQuery);
|
||||
tx.executeSql(createChapterIdIndexQuery);
|
||||
tx.executeSql(createCategorydIndexQuery);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteDb = () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql('DROP TABLE novels');
|
||||
tx.executeSql('DROP TABLE chapters');
|
||||
tx.executeSql('DROP TABLE history');
|
||||
tx.executeSql('DROP TABLE downloads');
|
||||
tx.executeSql('DROP TABLE updates');
|
||||
});
|
||||
export const createDatabase = async () => {
|
||||
createTables();
|
||||
createIndexes();
|
||||
};
|
||||
|
||||
/**
|
||||
* For Testing
|
||||
*/
|
||||
export const deleteDatabase = () => {
|
||||
db.transaction(
|
||||
tx => {
|
||||
tx.executeSql('DROP TABLE novels');
|
||||
tx.executeSql('DROP TABLE chapters');
|
||||
tx.executeSql('DROP TABLE history');
|
||||
tx.executeSql('DROP TABLE downloads');
|
||||
tx.executeSql('DROP TABLE updates');
|
||||
tx.executeSql('DROP TABLE categories');
|
||||
},
|
||||
dbTxnErrorCallback,
|
||||
noop,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { noop } from 'lodash';
|
||||
import { Category } from '../types';
|
||||
import { txnErrorCallback } from '../utils/helpers';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getCategoriesQuery = `
|
||||
SELECT * FROM categories ORDER BY CASE WHEN id > 1 THEN 1 ELSE 0 END, IFNULL(sort, 9999)
|
||||
`;
|
||||
|
||||
export const getCategoriesFromDb = (): Promise<Category[]> => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getCategoriesQuery,
|
||||
[],
|
||||
(txObj, { rows }) => resolve((rows as any)._array),
|
||||
txnErrorCallback,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const createCategoryQuery = `
|
||||
INSERT INTO categories (name)
|
||||
VALUES
|
||||
(?)
|
||||
`;
|
||||
|
||||
export const createCategory = (categoryName: string): void =>
|
||||
db.transaction(tx =>
|
||||
tx.executeSql(createCategoryQuery, [categoryName], noop, txnErrorCallback),
|
||||
);
|
||||
|
||||
const deleteCategoryQuery = `
|
||||
DELETE FROM categories
|
||||
WHERE
|
||||
id = ?
|
||||
`;
|
||||
|
||||
export const deleteCategoryById = (categoryId: number): void =>
|
||||
db.transaction(tx =>
|
||||
tx.executeSql(deleteCategoryQuery, [categoryId], noop, txnErrorCallback),
|
||||
);
|
||||
|
||||
const updateCategoryQuery = `
|
||||
UPDATE categories SET name = ? WHERE id = ?
|
||||
`;
|
||||
|
||||
export const updateCategory = (
|
||||
categoryId: number,
|
||||
categoryName: string,
|
||||
): void =>
|
||||
db.transaction(tx =>
|
||||
tx.executeSql(
|
||||
updateCategoryQuery,
|
||||
[categoryName, categoryId],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
),
|
||||
);
|
||||
|
||||
const isCategoryNameDuplicateQuery = `
|
||||
SELECT COUNT(*) as isDuplicate FROM categories WHERE name = ?
|
||||
`;
|
||||
|
||||
export const isCategoryNameDuplicate = (
|
||||
categoryName: string,
|
||||
): Promise<boolean> => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
isCategoryNameDuplicateQuery,
|
||||
[categoryName],
|
||||
(txObj, { rows: { _array } }) =>
|
||||
resolve(Boolean(_array[0]?.isDuplicate)),
|
||||
txnErrorCallback,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const updateCategoryOrderQuery = `
|
||||
UPDATE categories SET sort = ? WHERE id = ?
|
||||
`;
|
||||
|
||||
export const updateCategoryOrderInDb = (categories: Category[]): void =>
|
||||
db.transaction(tx => {
|
||||
categories.map(category => {
|
||||
tx.executeSql(
|
||||
updateCategoryOrderQuery,
|
||||
[category.sort, category.id],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,47 +5,37 @@ import { ChapterItem } from '../types';
|
||||
|
||||
import * as cheerio from 'cheerio';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
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 = '') =>
|
||||
@@ -61,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,
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -260,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}/`);
|
||||
@@ -317,30 +307,67 @@ export const downloadChapter = async (
|
||||
chapterUrl: string,
|
||||
chapterId: number,
|
||||
) => {
|
||||
const source = sourceManager(sourceId);
|
||||
try {
|
||||
const source = sourceManager(sourceId);
|
||||
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
|
||||
const imagedChapterText =
|
||||
chapter.chapterText &&
|
||||
(await downloadImages(chapter.chapterText, sourceId, novelId, chapterId));
|
||||
if (chapter.chapterText?.length) {
|
||||
const imagedChapterText =
|
||||
chapter.chapterText &&
|
||||
(await downloadImages(
|
||||
chapter.chapterText,
|
||||
sourceId,
|
||||
novelId,
|
||||
chapterId,
|
||||
));
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql('UPDATE chapters SET downloaded = 1 WHERE chapterId = ?', [
|
||||
chapterId,
|
||||
]);
|
||||
tx.executeSql(
|
||||
downloadChapterQuery,
|
||||
[chapterId, chapter.chapterName, imagedChapterText],
|
||||
(_txObj, _res) => {
|
||||
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
|
||||
},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'UPDATE chapters SET downloaded = 1 WHERE chapterId = ?',
|
||||
[chapterId],
|
||||
);
|
||||
tx.executeSql(
|
||||
downloadChapterQuery,
|
||||
[chapterId, chapter.chapterName, imagedChapterText],
|
||||
(_txObj, _res) => {
|
||||
return true;
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
throw new Error("Either chapter is empty or the app couldn't scrape it");
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDownloadedImages = async (
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
) => {
|
||||
try {
|
||||
const path = `${RNFetchBlob.fs.dirs.DownloadDir}/LNReader/`;
|
||||
const files = await RNFetchBlob.fs.ls(
|
||||
await createImageFolder(path, { sourceId, novelId, chapterId }),
|
||||
);
|
||||
});
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const ex = /(.*?)_(.*?)#(.*?)/.exec(files[i]);
|
||||
if (ex) {
|
||||
if (
|
||||
parseInt(ex[1], 10) === sourceId &&
|
||||
parseInt(ex[2], 10) === chapterId
|
||||
) {
|
||||
if (await RNFetchBlob.fs.exists(`${path}${files[i]}`)) {
|
||||
RNFetchBlob.fs.unlink(`${path}${files[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const deleteChapter = async (
|
||||
@@ -352,23 +379,7 @@ export const deleteChapter = async (
|
||||
'UPDATE chapters SET downloaded = 0 WHERE chapterId=?';
|
||||
const deleteChapterQuery = 'DELETE FROM downloads WHERE downloadChapterId=?';
|
||||
|
||||
const path = `${RNFetchBlob.fs.dirs.DownloadDir}/LNReader/`;
|
||||
const files = await RNFetchBlob.fs.ls(
|
||||
await createImageFolder(path, { sourceId, novelId, chapterId }),
|
||||
);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const ex = /(.*?)_(.*?)#(.*?)/.exec(files[i]);
|
||||
if (ex) {
|
||||
if (
|
||||
parseInt(ex[1], 10) === sourceId &&
|
||||
parseInt(ex[2], 10) === chapterId
|
||||
) {
|
||||
if (await RNFetchBlob.fs.exists(`${path}${files[i]}`)) {
|
||||
RNFetchBlob.fs.unlink(`${path}${files[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await deleteDownloadedImages(sourceId, novelId, chapterId);
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
@@ -394,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,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getLibraryQuery = (sort, filter) => `
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded, H.lastReadAt
|
||||
FROM novels
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
|
||||
FROM chapters
|
||||
WHERE chapters.read = 0
|
||||
GROUP BY chapters.novelId
|
||||
) AS C
|
||||
ON novels.novelId = C.novelId
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
|
||||
FROM chapters
|
||||
WHERE chapters.downloaded = 1
|
||||
GROUP BY chapters.novelId
|
||||
) AS D
|
||||
ON novels.novelId = D.novelId
|
||||
LEFT JOIN (
|
||||
SELECT history.historyNovelId as novelId, historyTimeRead AS lastReadAt
|
||||
FROM history
|
||||
GROUP BY history.historyNovelId
|
||||
HAVING history.historyTimeRead = MAX(history.historyTimeRead)
|
||||
ORDER BY history.historyTimeRead DESC
|
||||
) AS H
|
||||
ON novels.novelId = H.novelId
|
||||
WHERE novels.followed = 1 ${filter ? 'AND ' + filter : ''}
|
||||
${sort ? 'ORDER BY ' + sort : ''}
|
||||
`;
|
||||
|
||||
export const getLibrary = (sort, filter) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getLibraryQuery(sort, filter),
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
// console.log(JSON.stringify(_array, null, 2));
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const searchLibraryQuery = (searchText, sort, filter) =>
|
||||
`
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded, H.lastReadAt
|
||||
FROM novels
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
|
||||
FROM chapters
|
||||
WHERE chapters.read = 0
|
||||
GROUP BY chapters.novelId
|
||||
) AS C
|
||||
ON novels.novelId = C.novelId
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
|
||||
FROM chapters
|
||||
WHERE chapters.downloaded = 1
|
||||
GROUP BY chapters.novelId
|
||||
) AS D
|
||||
ON novels.novelId = D.novelId
|
||||
LEFT JOIN (
|
||||
SELECT history.historyNovelId as novelId, historyTimeRead AS lastReadAt
|
||||
FROM history
|
||||
GROUP BY history.historyNovelId
|
||||
HAVING history.historyTimeRead = MAX(history.historyTimeRead)
|
||||
ORDER BY history.historyTimeRead DESC
|
||||
) AS H
|
||||
ON novels.novelId = H.novelId
|
||||
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%' ${
|
||||
filter ? 'AND ' + filter : ''
|
||||
}
|
||||
${sort ? 'ORDER BY ' + sort : ''} `;
|
||||
|
||||
export const searchLibrary = (searchText, sort, filter) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
searchLibraryQuery(searchText, sort, filter),
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { LibraryFilter } from '@screens/library/constants/constants';
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { LibraryNovelInfo, NovelInfo } from '../types';
|
||||
import { txnErrorCallback } from '../utils/helpers';
|
||||
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getLibraryQuery = `
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded, H.lastReadAt
|
||||
FROM novels
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
|
||||
FROM chapters
|
||||
WHERE chapters.read = 0
|
||||
GROUP BY chapters.novelId
|
||||
) AS C
|
||||
ON novels.novelId = C.novelId
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
|
||||
FROM chapters
|
||||
WHERE chapters.downloaded = 1
|
||||
GROUP BY chapters.novelId
|
||||
) AS D
|
||||
ON novels.novelId = D.novelId
|
||||
LEFT JOIN (
|
||||
SELECT history.historyNovelId as novelId, historyTimeRead AS lastReadAt
|
||||
FROM history
|
||||
GROUP BY history.historyNovelId
|
||||
HAVING history.historyTimeRead = MAX(history.historyTimeRead)
|
||||
ORDER BY history.historyTimeRead DESC
|
||||
) AS H
|
||||
ON novels.novelId = H.novelId
|
||||
WHERE novels.followed = 1
|
||||
`;
|
||||
|
||||
export const getLibrary = ({
|
||||
filter,
|
||||
searchText,
|
||||
sortOrder,
|
||||
downloadedOnlyMode,
|
||||
}: {
|
||||
sortOrder?: string;
|
||||
filter?: string;
|
||||
searchText?: string;
|
||||
downloadedOnlyMode?: boolean;
|
||||
}): Promise<LibraryNovelInfo[]> => {
|
||||
let query = getLibraryQuery;
|
||||
|
||||
if (filter) {
|
||||
query += ` AND ${filter}`;
|
||||
}
|
||||
|
||||
if (downloadedOnlyMode) {
|
||||
query += ' ' + LibraryFilter.DownloadedOnly;
|
||||
}
|
||||
|
||||
if (searchText) {
|
||||
query += ` AND novelName LIKE '%${searchText}%'`;
|
||||
}
|
||||
|
||||
if (sortOrder) {
|
||||
query += ` ORDER BY ${sortOrder}`;
|
||||
}
|
||||
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
query,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
txnErrorCallback,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const getLibraryNovelsFromDb = (
|
||||
onlyOngoingNovels?: boolean,
|
||||
): Promise<NovelInfo[]> => {
|
||||
let getLibraryNovelsQuery = 'SELECT * FROM novels WHERE followed = 1';
|
||||
|
||||
if (onlyOngoingNovels) {
|
||||
getLibraryNovelsQuery += " AND status NOT LIKE 'Completed'";
|
||||
}
|
||||
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getLibraryNovelsQuery,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
txnErrorCallback,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -8,9 +8,11 @@ import { fetchChapters, fetchNovel } from '../../services/Source/source';
|
||||
import { insertChapters } from './ChapterQueries';
|
||||
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import { txnErrorCallback } from '../utils/helpers';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
const insertNovelQuery =
|
||||
'INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
'INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, categoryIds) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
|
||||
export const insertNovel = async novel => {
|
||||
return new Promise((resolve, reject) =>
|
||||
@@ -29,11 +31,10 @@ export const insertNovel = async novel => {
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
JSON.stringify(novel.categoryIds),
|
||||
],
|
||||
(txObj, { insertId }) => resolve(insertId),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
txnErrorCallback,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -142,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,205 @@
|
||||
import { noop, xor } from 'lodash';
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
import { txnErrorCallback } from '../utils/helpers';
|
||||
import { LibraryNovelInfo } from '../types';
|
||||
import { fetchNovel } from '../../services/Source/source';
|
||||
import { showToast } from '@hooks/showToast';
|
||||
import { getString } from '@strings/translations';
|
||||
import { checkNovelInCache } from './NovelQueries';
|
||||
|
||||
export const getCategoryNovelsFromDb = async (
|
||||
categoryId: number,
|
||||
onlyOngoingNovels?: boolean,
|
||||
): Promise<LibraryNovelInfo[]> => {
|
||||
let query = `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
novels
|
||||
WHERE (
|
||||
categoryIds LIKE '[${categoryId}]'
|
||||
OR
|
||||
categoryIds LIKE '[${categoryId},%'
|
||||
OR
|
||||
categoryIds LIKE '%,${categoryId}]'
|
||||
OR
|
||||
categoryIds LIKE '%,${categoryId},%'
|
||||
)
|
||||
AND
|
||||
followed = 1
|
||||
`;
|
||||
|
||||
if (onlyOngoingNovels) {
|
||||
query += ' AND status NOT LIKE "Completed"';
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
query,
|
||||
undefined,
|
||||
(txObj, { rows }) => resolve((rows as any)._array),
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const resetCategoryIdsToDefault = async (deletedCategoryId: number) => {
|
||||
const categoryNovels = await getCategoryNovelsFromDb(deletedCategoryId);
|
||||
|
||||
db.transaction(tx => {
|
||||
categoryNovels.forEach(novel => {
|
||||
let categoryIds = xor(JSON.parse(novel.categoryIds), [deletedCategoryId]);
|
||||
|
||||
categoryIds = categoryIds.length ? categoryIds : [1];
|
||||
|
||||
tx.executeSql('UPDATE novels SET categoryIds = ? WHERE novelId = ?', [
|
||||
JSON.stringify(categoryIds),
|
||||
novel.novelId,
|
||||
]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const insertNovelInLibrary = async (
|
||||
sourceId: number,
|
||||
novelUrl: string,
|
||||
inLibrary: boolean,
|
||||
defaultCategoryId: number,
|
||||
) => {
|
||||
if (inLibrary) {
|
||||
showToast(getString('browseScreen.removeFromLibrary'));
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
`
|
||||
UPDATE novels SET
|
||||
followed = 0
|
||||
WHERE
|
||||
sourceId = ?
|
||||
AND
|
||||
novelUrl = ?
|
||||
`,
|
||||
[sourceId, novelUrl],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
showToast(getString('browseScreen.addedToLibrary'));
|
||||
|
||||
const novelSavedInDb = await checkNovelInCache(novelUrl);
|
||||
|
||||
if (novelSavedInDb) {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
`
|
||||
UPDATE novels SET
|
||||
followed = 1
|
||||
WHERE
|
||||
sourceId = ?
|
||||
AND
|
||||
novelUrl = ?
|
||||
`,
|
||||
[sourceId, novelUrl],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const novel = await fetchNovel(sourceId, novelUrl);
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
`
|
||||
INSERT INTO novels
|
||||
(
|
||||
novelUrl,
|
||||
sourceUrl,
|
||||
sourceId,
|
||||
source,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelSummary,
|
||||
author,
|
||||
artist,
|
||||
status,
|
||||
genre,
|
||||
followed,
|
||||
categoryIds
|
||||
)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
||||
[
|
||||
novel.novelUrl,
|
||||
novel.sourceUrl,
|
||||
novel.sourceId,
|
||||
novel.source,
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelSummary,
|
||||
novel.author,
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
JSON.stringify([defaultCategoryId]),
|
||||
],
|
||||
(txObj, { insertId }) => {
|
||||
novel.chapters?.map(chapter => {
|
||||
tx.executeSql(
|
||||
`
|
||||
INSERT INTO chapters
|
||||
(chapterUrl, chapterName, releaseDate, novelId)
|
||||
VALUES
|
||||
(?, ?, ?, ?)`,
|
||||
[
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.releaseDate,
|
||||
insertId,
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateNovelCategoryById = async (
|
||||
novelId: number,
|
||||
categoryIds: number[],
|
||||
) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'UPDATE novels SET categoryIds = ? WHERE novelId = ?',
|
||||
[JSON.stringify(categoryIds.length ? categoryIds : [1]), novelId],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const updateNovelCategoryByIds = async (
|
||||
novelIds: number[],
|
||||
categoryIds: number[],
|
||||
) => {
|
||||
db.transaction(tx => {
|
||||
novelIds.map(novelId =>
|
||||
tx.executeSql(
|
||||
'UPDATE novels SET categoryIds = ? WHERE novelId = ?',
|
||||
[JSON.stringify(categoryIds.length ? categoryIds : [1]), novelId],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
updates.updateTime DESC, chapters.chapterName DESC, chapters.releaseDate DESC
|
||||
`;
|
||||
|
||||
export const getUpdatesFromDb = async (): Promise<Update[]> => {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export const createCategoriesTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS categories(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
lastUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
sort INTEGER DEFAULT NULL
|
||||
)
|
||||
`;
|
||||
|
||||
export const createDefaultCategoryQuery =
|
||||
'INSERT INTO categories (id, name) VALUES (1, "Default");';
|
||||
|
||||
export const createCategorydIndexQuery =
|
||||
'CREATE INDEX IF NOT EXISTS categoryIdIndex ON categories(id)';
|
||||
|
||||
export const addCategorySortColumnQuery =
|
||||
'ALTER TABLE categories ADD COLUMN sort INTEGER DEFAULT NULL';
|
||||
@@ -13,7 +13,8 @@ export const createNovelTableQuery = `
|
||||
status TEXT,
|
||||
genre TEXT,
|
||||
followed INTEGER DEFAULT 0,
|
||||
unread INTEGER DEFAULT 1
|
||||
unread INTEGER DEFAULT 1,
|
||||
categoryIds TEXT DEFAULT "[1]"
|
||||
)`;
|
||||
|
||||
export const createUrlIndexQuery =
|
||||
@@ -21,3 +22,6 @@ export const createUrlIndexQuery =
|
||||
|
||||
export const createLibraryIndexQuery =
|
||||
'CREATE INDEX IF NOT EXISTS novelFollowedIndex ON novels(followed)';
|
||||
|
||||
export const addCategoryColumnQuery =
|
||||
'ALTER TABLE novels ADD COLUMN categoryIds TEXT DEFAULT "[1]"';
|
||||
|
||||
@@ -11,11 +11,13 @@ export interface NovelInfo {
|
||||
author?: string;
|
||||
status?: string;
|
||||
followed: number;
|
||||
categoryIds: string;
|
||||
}
|
||||
|
||||
export interface LibraryNovelInfo extends NovelInfo {
|
||||
chaptersUnread: number;
|
||||
chaptersDownloaded: number;
|
||||
categoryIds: string;
|
||||
}
|
||||
|
||||
export interface ChapterItem {
|
||||
@@ -66,3 +68,21 @@ export interface Update {
|
||||
bookmark: number;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
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>;
|
||||
}
|
||||
|
||||