Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| fd6a907c5f | |||
| c7561fc3dd | |||
| e1401ba39f | |||
| cbb3c7cfa2 | |||
| 4b515502bf | |||
| baba8814f0 | |||
| 5b01f61c9f | |||
| c02a72018d | |||
| 4d446d7c0b | |||
| e580f8f151 | |||
| 92f164fc23 | |||
| 44c805b6f2 | |||
| d3f5619276 | |||
| 0261c6373d | |||
| fd5b9a7c17 | |||
| b884600dfb | |||
| 8355907a76 | |||
| b391e09e53 | |||
| a4c27924f8 | |||
| 221c77d2cb | |||
| 3a0047ee25 | |||
| 3afbb5c45d | |||
| 47102ff65e | |||
| c10eac6cd6 | |||
| b4c234ef93 | |||
| 3487bbd4cf | |||
| bece88663f | |||
| 054124a004 | |||
| d2b62c0e8b | |||
| e98980bdb9 | |||
| 62a847acb2 | |||
| 13f394c76d | |||
| 71f6d863c0 | |||
| d5a8ca2b47 | |||
| c969916cd5 | |||
| e68c72ce02 | |||
| 5f83104171 | |||
| 7837d7cdaf | |||
| b243d764ce | |||
| 56c7716595 | |||
| d44abc9039 | |||
| 6f30bfeeab | |||
| 3d90e04415 | |||
| 23e27e8d40 | |||
| d0f7e25956 | |||
| 9b297620fe | |||
| df851bf9d1 | |||
| 5fc411e14c | |||
| 0f221b2823 | |||
| 9e50159589 | |||
| 58752a5589 | |||
| c1a76458a8 | |||
| 5b3a6ceff4 | |||
| c4a14784f0 | |||
| d64e2e7fdb | |||
| 5d8463509c | |||
| 5b4e32e184 | |||
| a7fb3962f1 | |||
| d333381959 | |||
| d599e6669f | |||
| 5436bf8aaf | |||
| 9f6f9e71ca | |||
| 6caba807df | |||
| 9dabf1a60e | |||
| 7716ab75de | |||
| 94b2dd51cb | |||
| 010d49daea | |||
| 6eb733eb66 | |||
| 33a33842c6 | |||
| 1bfd24a1b4 | |||
| 7e1bf4dd3f | |||
| 955e552597 | |||
| 8b586754d2 | |||
| b921f9b764 | |||
| 4d30546b9a | |||
| 58eac42b14 | |||
| 872ec15ca5 | |||
| e013b3cc55 | |||
| 8e58ce2eb1 | |||
| a9e2d872f2 | |||
| ccdb150d6a | |||
| 4e1d8533e3 | |||
| da18cd9ade | |||
| 802aa6d71a | |||
| 3fbca6c14d | |||
| 5a07255b95 | |||
| 9ed1d5c8b6 | |||
| f07a4845b8 | |||
| 922ffbe7b0 |
@@ -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.12"
|
||||
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 an source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.1.12](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
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.12](https://github.com/LNReader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
@@ -5,13 +5,13 @@ on:
|
||||
- main
|
||||
jobs:
|
||||
build-android:
|
||||
name: Build app
|
||||
name: Build App
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
npm install --legacy-peer-deps
|
||||
|
||||
- name: Cache Gradle Wrapper
|
||||
uses: actions/cache@v2
|
||||
|
||||
@@ -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,5 +1,12 @@
|
||||
name: Lint
|
||||
on: [push, pull_request]
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ESLint
|
||||
@@ -7,6 +14,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: npm i
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
@@ -13,13 +13,13 @@ 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';
|
||||
|
||||
const App = () => {
|
||||
useEffect(() => {
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
createDB();
|
||||
createDatabase();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -84,7 +84,6 @@ project.ext.react = [
|
||||
apply from: '../../node_modules/react-native-unimodules/gradle.groovy'
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
apply from: "../../node_modules/expo-constants/scripts/get-app-config-android.gradle"
|
||||
apply from: "../../node_modules/expo-updates/scripts/create-manifest-android.gradle"
|
||||
|
||||
/**
|
||||
* Set this to true to create two separate APKs instead of one:
|
||||
@@ -136,7 +135,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.1.8"
|
||||
versionName "1.1.12"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<!-- Internet -->
|
||||
<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"/>
|
||||
@@ -21,6 +22,12 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:largeHeap="true"
|
||||
@@ -30,26 +37,6 @@
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.updates.EXPO_UPDATE_URL"
|
||||
android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.updates.EXPO_SDK_VERSION"
|
||||
android:value="41.0.0"/>
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.updates.ENABLED"
|
||||
android:value="false"/>
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH"
|
||||
android:value="ALWAYS"/>
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS"
|
||||
android:value="0"/>
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.notifications.default_notification_icon"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.content.Intent;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.graphics.Color;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.WindowManager;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
@@ -12,6 +14,7 @@ import android.view.Window;
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListener;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
|
||||
import expo.modules.splashscreen.singletons.SplashScreen;
|
||||
@@ -31,24 +34,53 @@ 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
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
|
||||
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
getWindow().setAttributes(layoutParams);
|
||||
}
|
||||
// SplashScreen.show(...) has to be called after super.onCreate(...)
|
||||
// Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
|
||||
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
getWindow().setAttributes(layoutParams);
|
||||
}
|
||||
|
||||
Window w = getWindow();
|
||||
w.setStatusBarColor(Color.TRANSPARENT);
|
||||
w.setNavigationBarColor(Color.TRANSPARENT);
|
||||
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
Window w = getWindow();
|
||||
w.setStatusBarColor(Color.TRANSPARENT);
|
||||
w.setNavigationBarColor(Color.TRANSPARENT);
|
||||
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
|
||||
super.onCreate(null);
|
||||
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
|
||||
super.onCreate(null);
|
||||
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (VolumeButtonListener.isActive()) {
|
||||
|
||||
int action = event.getAction();
|
||||
int keyCode = event.getKeyCode();
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
// volup
|
||||
VolumeButtonListener.up();
|
||||
if (!VolumeButtonListener.prevent)
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
return true;
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
//voldown
|
||||
VolumeButtonListener.down();
|
||||
if (!VolumeButtonListener.prevent)
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
@@ -68,4 +100,5 @@ public class MainActivity extends ReactActivity {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,34 +2,24 @@ package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.rajarsheechatterjee.LNReader.generated.BasePackageList;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage;
|
||||
|
||||
import org.unimodules.adapters.react.ReactAdapterPackage;
|
||||
import org.unimodules.adapters.react.ModuleRegistryAdapter;
|
||||
import org.unimodules.adapters.react.ReactModuleRegistryProvider;
|
||||
import org.unimodules.core.interfaces.Package;
|
||||
import org.unimodules.core.interfaces.SingletonModule;
|
||||
import expo.modules.constants.ConstantsPackage;
|
||||
import expo.modules.permissions.PermissionsPackage;
|
||||
import expo.modules.filesystem.FileSystemPackage;
|
||||
import expo.modules.updates.UpdatesController;
|
||||
|
||||
import com.facebook.react.bridge.JSIModulePackage;
|
||||
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(
|
||||
@@ -46,7 +36,8 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
protected List<ReactPackage> getPackages() {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
|
||||
packages.add(new NavigationBarColorPackage());
|
||||
packages.add(new com.rajarsheechatterjee.LNReader.NavigationBarColorPackage());
|
||||
packages.add(new VolumeButtonListenerPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@@ -59,24 +50,6 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
protected JSIModulePackage getJSIModulePackage() {
|
||||
return new ReanimatedJSIModulePackage();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable String getJSBundleFile() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
return super.getJSBundleFile();
|
||||
} else {
|
||||
return UpdatesController.getInstance().getLaunchAssetFile();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable String getBundleAssetName() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
return super.getBundleAssetName();
|
||||
} else {
|
||||
return UpdatesController.getInstance().getBundleAssetName();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
@@ -89,10 +62,6 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
super.onCreate();
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
|
||||
if (!BuildConfig.DEBUG) {
|
||||
UpdatesController.initialize(this);
|
||||
}
|
||||
|
||||
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -22,7 +22,6 @@ public class BasePackageList {
|
||||
new expo.modules.permissions.PermissionsPackage(),
|
||||
new expo.modules.splashscreen.SplashScreenPackage(),
|
||||
new expo.modules.sqlite.SQLitePackage(),
|
||||
new expo.modules.updates.UpdatesPackage(),
|
||||
new expo.modules.webbrowser.WebBrowserPackage()
|
||||
);
|
||||
}
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.rajarsheechatterjee.VolumeButtonListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
|
||||
public class VolumeButtonListener extends ReactContextBaseJavaModule {
|
||||
|
||||
static ReactApplicationContext appContext;
|
||||
|
||||
public VolumeButtonListener(ReactApplicationContext context) {
|
||||
super(context);
|
||||
appContext = context;
|
||||
}
|
||||
|
||||
private static boolean active = false;
|
||||
|
||||
public static boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public static boolean prevent = false;
|
||||
|
||||
public static void sendEvent(boolean up) {
|
||||
if (active) {
|
||||
WritableMap args = Arguments.createMap();
|
||||
if (up)
|
||||
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeUp", args);
|
||||
else
|
||||
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeDown", args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void down() {
|
||||
sendEvent(false);
|
||||
}
|
||||
|
||||
public static void up() {
|
||||
sendEvent(true);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void connect() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
void disconnect() {
|
||||
active = false;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void pause() {
|
||||
active = false;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void unpause() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void preventDefault() {
|
||||
prevent = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void noPreventDefault() {
|
||||
prevent = false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "VolumeButtonListener";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void addListener(String eventName) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeListeners(Integer count) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.rajarsheechatterjee.VolumeButtonListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class VolumeButtonListenerPackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<NativeModule>();
|
||||
|
||||
modules.add(new VolumeButtonListener(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ buildscript {
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
ndkVersion = "21.1.6352462"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
|
||||
@@ -2,5 +2,24 @@ module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
plugins: [
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pull_request_title: 'chore: Update translations'
|
||||
commit_message: '[ci skip]'
|
||||
files:
|
||||
- source: /strings/languages/en/strings.json
|
||||
translation: /strings/languages/%two_letters_code%/strings.json
|
||||
|
||||
Generated
+769
-440
File diff suppressed because it is too large
Load Diff
+15
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lnreader",
|
||||
"version": "1.1.8",
|
||||
"version": "1.1.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
@@ -9,26 +9,29 @@
|
||||
"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}",
|
||||
"sources:madara": "node scripts/generateMadaraSource.cjs",
|
||||
"source:generator": "node scripts/generateSource.cjs",
|
||||
"strings": "node scripts/stringTypes.cjs",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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/netinfo": "^7.1.1",
|
||||
"@react-native-community/slider": "^4.1.12",
|
||||
"@react-native-picker/picker": "^2.4.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",
|
||||
"@types/react-native-vector-icons": "^6.4.10",
|
||||
"@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",
|
||||
"expo-font": "~9.1.0",
|
||||
"expo-haptics": "~10.0.0",
|
||||
"expo-keep-awake": "~9.1.2",
|
||||
"expo-linear-gradient": "~9.1.0",
|
||||
@@ -37,11 +40,9 @@
|
||||
"expo-notifications": "~0.11.6",
|
||||
"expo-splash-screen": "^0.11.4",
|
||||
"expo-sqlite": "~9.1.0",
|
||||
"expo-updates": "~0.5.4",
|
||||
"expo-web-browser": "~9.1.0",
|
||||
"i18n-js": "^3.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.10.1",
|
||||
"react": "16.13.1",
|
||||
@@ -52,38 +53,41 @@
|
||||
"react-native-easing-gradient": "^1.0.0",
|
||||
"react-native-fast-image": "^8.5.11",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"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-paper": "^5.0.0-rc.4",
|
||||
"react-native-reanimated": "^2.2.4",
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"react-native-safe-area-context": "^3.2.0",
|
||||
"react-native-screens": "^3.9.0",
|
||||
"react-native-skeleton-placeholder": "^5.0.0",
|
||||
"react-native-swipe-gestures": "^1.0.5",
|
||||
"react-native-tab-view": "^3.1.1",
|
||||
"react-native-tts": "^4.1.0",
|
||||
"react-native-unimodules": "^0.13.3",
|
||||
"react-native-vector-icons": "^9.0.0",
|
||||
"react-native-web": "^0.17.5",
|
||||
"react-native-webview": "^11.14.3",
|
||||
"react-redux": "^7.2.6",
|
||||
"redux": "^4.1.2",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.4.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",
|
||||
"@types/react-native": "^0.66.15",
|
||||
"@types/react-native-vector-icons": "^6.4.10",
|
||||
"@types/react-test-renderer": "^17.0.1",
|
||||
"@types/sanitize-html": "^2.6.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-react-native": "^3.11.0",
|
||||
"husky": "^7.0.4",
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
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');
|
||||
|
||||
const getScraperName = scraperName => {
|
||||
let name = scraperName.replace(/\./g, 'Dot').replace('1st', 'First');
|
||||
let name = scraperName
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/-/g, 'Dash')
|
||||
.replace(/\./g, 'Dot')
|
||||
.replace('1st', 'First');
|
||||
|
||||
return name;
|
||||
};
|
||||
@@ -23,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,
|
||||
@@ -40,13 +51,21 @@ MadaraSources.forEach(madaraSource => {
|
||||
allSourcesContent.push({
|
||||
sourceId: madaraSource.sourceId,
|
||||
sourceName: madaraSource.sourceName,
|
||||
icon: `https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/${madaraSource.sourceName.toLowerCase()}.png?raw=true`,
|
||||
icon: `https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/${madaraSource.sourceName
|
||||
.replace(/\s+/g, '')
|
||||
.toLowerCase()}.png?raw=true`,
|
||||
url: madaraSource.baseUrl,
|
||||
lang: madaraSource.options.lang || 'English',
|
||||
});
|
||||
|
||||
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,
|
||||
|
||||
@@ -254,7 +254,7 @@ try {
|
||||
|
||||
return novels;`
|
||||
: `
|
||||
showToast('Search is not available in this source')
|
||||
showToast('${source.sourceName}: Search is not available in this source')
|
||||
|
||||
return [];
|
||||
`
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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 { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface AppbarProps {
|
||||
title: string;
|
||||
handleGoBack: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
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>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
const BottomSheetHandle = ({ theme }) => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: `${theme.textColorPrimary}66`,
|
||||
height: 4,
|
||||
width: 40,
|
||||
borderRadius: 50,
|
||||
top: 8,
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default BottomSheetHandle;
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Pressable, StyleSheet, Text, View } 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 { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
export enum ButtonVariation {
|
||||
DEFAULT = 'default',
|
||||
@@ -14,50 +23,59 @@ interface Props {
|
||||
title: string;
|
||||
textColor?: string;
|
||||
onPress?: () => void;
|
||||
margin?: number;
|
||||
theme: ThemeTypeV1;
|
||||
theme: MD3ThemeType;
|
||||
style?: ViewStyle;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Button: React.FC<Props> = ({
|
||||
variation = ButtonVariation.DEFAULT,
|
||||
title,
|
||||
textColor,
|
||||
margin = 0,
|
||||
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={[
|
||||
styles.button,
|
||||
{ margin },
|
||||
variation === 'outlined'
|
||||
variation === ButtonVariation.OUTLINED
|
||||
? {
|
||||
...styles.outlined,
|
||||
borderColor: theme.textColorHint,
|
||||
}
|
||||
: variation !== 'clear' && {
|
||||
backgroundColor: theme.colorAccent,
|
||||
: variation !== ButtonVariation.CLEAR && {
|
||||
backgroundColor: theme.secondary,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<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 === 'outlined'
|
||||
? theme.colorAccent
|
||||
: variation === 'default'
|
||||
? theme.colorButtonText
|
||||
: theme.textColorPrimary,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text style={[textStyles, labelStyle]} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,13 +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 = ({ label, status, onPress, disabled, theme }) => (
|
||||
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,
|
||||
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}
|
||||
>
|
||||
@@ -20,11 +47,14 @@ export const Checkbox = ({ label, status, onPress, disabled, theme }) => (
|
||||
: 'unchecked'
|
||||
}
|
||||
onPress={onPress}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
theme={{
|
||||
colors: { disabled: theme.textColorSecondary },
|
||||
}}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text style={{ color: theme.textColorPrimary, marginLeft: 12 }}>
|
||||
<Text style={[styles.defaultLabel, { color: theme.onSurface }, labelStyle]}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -32,19 +62,19 @@ export const Checkbox = ({ label, status, onPress, disabled, theme }) => (
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -60,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 { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
interface ChipProps {
|
||||
label: string;
|
||||
theme: ThemeTypeV1;
|
||||
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: {
|
||||
|
||||
+36
-32
@@ -1,48 +1,52 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { Modal, Portal, TextInput } from 'react-native-paper';
|
||||
import { Modal, overlay, Portal, TextInput } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
const ColorPickerModal = ({
|
||||
interface ColorPickerModalProps {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
color: string;
|
||||
onSubmit: (val: string) => void;
|
||||
closeModal: () => void;
|
||||
theme: MD3ThemeType;
|
||||
showAccentColors?: boolean;
|
||||
}
|
||||
|
||||
const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
|
||||
theme,
|
||||
color,
|
||||
title,
|
||||
onSubmit,
|
||||
hideModal,
|
||||
modalVisible,
|
||||
closeModal,
|
||||
visible,
|
||||
showAccentColors,
|
||||
}) => {
|
||||
const [text, setText] = useState(color);
|
||||
const [error, setError] = useState();
|
||||
const [text, setText] = useState<string>(color);
|
||||
const [error, setError] = useState<string | null>();
|
||||
|
||||
const onDismiss = () => {
|
||||
hideModal();
|
||||
setText();
|
||||
setError();
|
||||
closeModal();
|
||||
if (error) {
|
||||
setText(color);
|
||||
}
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const onChangeText = txt => setText(txt);
|
||||
const onChangeText = (txt: string) => setText(txt);
|
||||
|
||||
const onSubmitEditing = () => {
|
||||
const re = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
|
||||
const re = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3})$/i;
|
||||
|
||||
if (text.match(re)) {
|
||||
onSubmit(text);
|
||||
hideModal();
|
||||
closeModal();
|
||||
} else {
|
||||
setError('Enter a valid hex color code');
|
||||
}
|
||||
};
|
||||
|
||||
const textInputTheme = {
|
||||
colors: {
|
||||
primary: color,
|
||||
placeholder: theme.textColorHint,
|
||||
text: theme.textColorPrimary,
|
||||
background: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
const accentColors = [
|
||||
'#EF5350',
|
||||
'#EC407A',
|
||||
@@ -69,17 +73,17 @@ const ColorPickerModal = ({
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
{ backgroundColor: overlay(2, theme.surface) },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalTitle, { color: theme.textColorPrimary }]}>
|
||||
{title}
|
||||
</Text>
|
||||
{showAccentColors && (
|
||||
{showAccentColors ? (
|
||||
<FlatList
|
||||
contentContainerStyle={{ marginBottom: 8 }}
|
||||
data={accentColors}
|
||||
@@ -104,24 +108,24 @@ const ColorPickerModal = ({
|
||||
}}
|
||||
onPress={() => {
|
||||
onSubmit(item);
|
||||
hideModal();
|
||||
closeModal();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<TextInput
|
||||
value={text}
|
||||
defaultValue={color}
|
||||
defaultValue={typeof color === 'string' ? color : ''}
|
||||
placeholder="Hex Color Code (E.g. #3399FF)"
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
mode="outlined"
|
||||
theme={textInputTheme}
|
||||
theme={{ colors: { ...theme } }}
|
||||
underlineColor={theme.textColorHint}
|
||||
dense
|
||||
error={error}
|
||||
error={Boolean(error)}
|
||||
/>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</Modal>
|
||||
@@ -134,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: {
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
interface ColorPreferenceItemProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
onPress: () => void;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const ColorPreferenceItem: React.FC<ColorPreferenceItemProps> = ({
|
||||
label,
|
||||
description,
|
||||
theme,
|
||||
onPress,
|
||||
}) => (
|
||||
<Pressable
|
||||
style={styles.container}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View>
|
||||
<Text style={[styles.label, { color: theme.textColorPrimary }]}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
{description?.toUpperCase?.()}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[{ backgroundColor: description }, styles.colorPreview]} />
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default ColorPreferenceItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
},
|
||||
colorPreview: {
|
||||
height: 24,
|
||||
width: 24,
|
||||
borderRadius: 50,
|
||||
marginRight: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 4,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={
|
||||
selected ? theme.colorAccent : color ? color : theme.textColorPrimary
|
||||
}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export const ToggleColorButton = ({
|
||||
selected,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
onPress,
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 6,
|
||||
height: 44,
|
||||
width: 44,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: textColor }}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
backgroundColor: backgroundColor,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
{selected && (
|
||||
<MaterialCommunityIcons name="check" color={textColor} size={24} />
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import Color from 'color';
|
||||
|
||||
interface ToggleButtonProps {
|
||||
icon: string;
|
||||
selected: boolean;
|
||||
theme: MD3ThemeType;
|
||||
color?: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export const ToggleButton: React.FC<ToggleButtonProps> = ({
|
||||
icon,
|
||||
selected,
|
||||
theme,
|
||||
color,
|
||||
onPress,
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 6,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: Color(theme.primary).alpha(0.12).string() }}
|
||||
style={{
|
||||
padding: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: selected
|
||||
? Color(theme.primary).alpha(0.12).string()
|
||||
: 'transparent',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={
|
||||
selected ? theme.primary : color ? color : theme.textColorPrimary
|
||||
}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
interface ToggleColorButtonProps {
|
||||
selected: boolean;
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export const ToggleColorButton: React.FC<ToggleColorButtonProps> = ({
|
||||
selected,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
onPress,
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 6,
|
||||
height: 44,
|
||||
width: 44,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: textColor }}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
backgroundColor: backgroundColor,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={selected ? 'check' : 'format-color-text'}
|
||||
color={textColor}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { Dialog, Portal } from 'react-native-paper';
|
||||
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: MD3ThemeType;
|
||||
onSubmit: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
|
||||
title,
|
||||
visible,
|
||||
onDismiss,
|
||||
theme,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const handleOnSubmit = () => {
|
||||
onSubmit();
|
||||
onDismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: getDialogBackground(theme) },
|
||||
]}
|
||||
>
|
||||
<Dialog.Title style={[styles.title, { color: theme.textColorPrimary }]}>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<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}
|
||||
/>
|
||||
</View>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 28,
|
||||
},
|
||||
title: {
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
},
|
||||
buttonCtn: {
|
||||
flexDirection: 'row-reverse',
|
||||
padding: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const Dialog = ({ visible, theme }) => {};
|
||||
|
||||
const styles = StyleSheet.create({});
|
||||
@@ -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 { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface EmptyViewProps {
|
||||
icon?: string;
|
||||
description: string;
|
||||
theme: ThemeTypeV1;
|
||||
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,24 +1,21 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { Container } from '..';
|
||||
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
|
||||
interface ErrorScreenProps {
|
||||
error?: string;
|
||||
theme: ThemeTypeV1;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error, theme }) => {
|
||||
return (
|
||||
<Container>
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.icon, { color: theme.textColorHint }]}>ಥ_ಥ</Text>
|
||||
<Text style={[styles.error, { color: theme.textColorHint }]}>
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
</Container>
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.icon, { color: theme.textColorHint }]}>ಥ_ಥ</Text>
|
||||
<Text style={[styles.error, { color: theme.textColorHint }]}>
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,5 +32,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
error: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import { IconButton } from 'react-native-paper';
|
||||
// import {Row} from './Common';
|
||||
|
||||
import EmptyView from './EmptyView';
|
||||
|
||||
const ErrorView = ({ theme, onRetry, error, openWebView }) => (
|
||||
<View style={{ flex: 1, justifyContent: 'center' }}>
|
||||
<EmptyView
|
||||
icon="(; ̄Д ̄)"
|
||||
description={error}
|
||||
style={{ color: theme.textColorSecondary }}
|
||||
>
|
||||
<IconButton
|
||||
icon="reload"
|
||||
size={25}
|
||||
style={{ margin: 0, marginTop: 24 }}
|
||||
color={theme.text}
|
||||
onPress={onRetry}
|
||||
/>
|
||||
<Text style={{ color: theme.textColorPrimary }}>Retry</Text>
|
||||
</EmptyView>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default ErrorView;
|
||||
@@ -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 { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
@@ -11,7 +12,7 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
padding?: number;
|
||||
onPress?: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
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}
|
||||
|
||||
+16
-8
@@ -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>
|
||||
);
|
||||
@@ -38,7 +37,7 @@ const ListItem = ({
|
||||
<View style={{ justifyContent: 'center' }}>
|
||||
{icon && (
|
||||
<PaperList.Icon
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
icon={icon}
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
@@ -58,12 +57,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 +92,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 +114,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 +133,7 @@ const styles = StyleSheet.create({
|
||||
marginVertical: 0,
|
||||
},
|
||||
listSubHeader: {
|
||||
fontWeight: 'bold',
|
||||
paddingBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import { dividerColor } from '../../theme/colors';
|
||||
|
||||
interface ListItemProps {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
icon?: string;
|
||||
onPress?: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const Section = ({ children }: { children: ReactNode }) => (
|
||||
@@ -21,9 +23,9 @@ const SubHeader = ({
|
||||
theme,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
theme: ThemeTypeV1;
|
||||
theme: MD3ThemeType;
|
||||
}) => (
|
||||
<PaperList.Subheader style={{ color: theme.colorAccent }}>
|
||||
<PaperList.Subheader style={{ color: theme.primary }}>
|
||||
{children}
|
||||
</PaperList.Subheader>
|
||||
);
|
||||
@@ -44,7 +46,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,13 +54,13 @@ const Item: React.FC<ListItemProps> = ({
|
||||
</View>
|
||||
)}
|
||||
onPress={onPress}
|
||||
rippleColor={theme.rippleColor}
|
||||
rippleColor={color(theme.primary).alpha(0.12).string()}
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({ theme }: { theme: ThemeTypeV1 }) => (
|
||||
const Divider = ({ theme }: { theme: MD3ThemeType }) => (
|
||||
<PaperDivider
|
||||
style={[styles.divider, { backgroundColor: theme.dividerColor }]}
|
||||
style={[styles.divider, { backgroundColor: dividerColor(theme.isDark) }]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -69,7 +71,7 @@ const InfoItem = ({
|
||||
}: {
|
||||
title: string;
|
||||
icon: string;
|
||||
theme: ThemeTypeV1;
|
||||
theme: MD3ThemeType;
|
||||
}) => (
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, StyleSheet } from 'react-native';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface Props {
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const LoadingMoreIndicator: React.FC<Props> = ({ theme }) => (
|
||||
<ActivityIndicator color={theme.primary} style={styles.indicator} />
|
||||
);
|
||||
|
||||
export default LoadingMoreIndicator;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
indicator: {
|
||||
padding: 32,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -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,17 +1,10 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, ActivityIndicator } from 'react-native';
|
||||
|
||||
import { Container } from '..';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
const LoadingScreen: React.FC<{ theme: ThemeTypeV1 }> = ({ theme }) => (
|
||||
<Container>
|
||||
<ActivityIndicator
|
||||
size={50}
|
||||
color={theme.colorAccent}
|
||||
style={styles.indicator}
|
||||
/>
|
||||
</Container>
|
||||
const LoadingScreen: React.FC<{ theme: MD3ThemeType }> = ({ theme }) => (
|
||||
<ActivityIndicator size={50} color={theme.primary} style={styles.indicator} />
|
||||
);
|
||||
|
||||
export default LoadingScreen;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
import { Portal, Modal, Button } from 'react-native-paper';
|
||||
import * as Linking from 'expo-linking';
|
||||
import { useTheme } from '../hooks/reduxHooks';
|
||||
|
||||
const NewUpdateDialog = ({ newVersion }) => {
|
||||
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={newUpdateDialog}
|
||||
onDismiss={() => showNewUpdateDialog(false)}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalHeader, { color: theme.textColorPrimary }]}>
|
||||
{`New update available ${newVersion.tag_name}`}
|
||||
</Text>
|
||||
<Text style={{ color: theme.textColorSecondary, fontSize: 16 }}>
|
||||
{newVersion.body}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginTop: 30,
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
labelStyle={{
|
||||
color: theme.colorAccent,
|
||||
letterSpacing: 0,
|
||||
textTransform: 'none',
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={() => showNewUpdateDialog(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
labelStyle={{
|
||||
color: theme.colorAccent,
|
||||
letterSpacing: 0,
|
||||
textTransform: 'none',
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={() => Linking.openURL(newVersion.downloadUrl)}
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewUpdateDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
modalHeader: {
|
||||
fontSize: 20,
|
||||
marginBottom: 10,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
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 { 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: {
|
||||
tag_name: string;
|
||||
body: string;
|
||||
downloadUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
const NewUpdateDialog: React.FC<NewUpdateDialogProps> = ({ newVersion }) => {
|
||||
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const modalHeight = Dimensions.get('window').height / 2;
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={newUpdateDialog}
|
||||
onDismiss={() => showNewUpdateDialog(false)}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: getDialogBackground(theme) },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalHeader, { color: theme.textColorPrimary }]}>
|
||||
{`${getString('common.newUpdateAvailable')} ${newVersion.tag_name}`}
|
||||
</Text>
|
||||
<ScrollView style={{ height: modalHeight }}>
|
||||
<Markdown
|
||||
style={{
|
||||
body: {
|
||||
color: theme.textColorSecondary,
|
||||
lineHeight: 20,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
hr: {
|
||||
backgroundColor: dividerColor(theme.isDark),
|
||||
marginVertical: 16,
|
||||
},
|
||||
code_inline: {
|
||||
backgroundColor: getDialogBackground(theme),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{newVersion.body}
|
||||
</Markdown>
|
||||
</ScrollView>
|
||||
<View style={styles.buttonCtn}>
|
||||
<Button
|
||||
title={getString('common.cancel')}
|
||||
theme={theme}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
onPress={() => showNewUpdateDialog(false)}
|
||||
/>
|
||||
<Button
|
||||
title={getString('common.install')}
|
||||
theme={theme}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
onPress={() => Linking.openURL(newVersion.downloadUrl)}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewUpdateDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
modalHeader: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
body: {},
|
||||
buttonCtn: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 16,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
@@ -6,16 +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 { defaultCoverUri } from '../sources/helpers/constants';
|
||||
import { coverPlaceholderColor } from '../theme/colors';
|
||||
import { useLibrarySettings } from '@hooks/useSettings';
|
||||
import { DisplayModes } from '@screens/library/constants/constants';
|
||||
|
||||
const NovelCover = ({
|
||||
item,
|
||||
@@ -26,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
|
||||
@@ -76,6 +81,7 @@ const NovelCover = ({
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
{showUnreadBadges && item.chaptersUnread && (
|
||||
@@ -91,7 +97,7 @@ const NovelCover = ({
|
||||
source={{ uri }}
|
||||
style={[
|
||||
{
|
||||
height: getHeight(),
|
||||
height: coverHeight,
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
@@ -99,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>
|
||||
@@ -113,6 +121,7 @@ const NovelCover = ({
|
||||
showDownloadBadges &&
|
||||
item.chaptersDownloaded && (
|
||||
<DownloadBadge
|
||||
theme={theme}
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
@@ -187,8 +196,8 @@ const InLibraryBadge = ({ theme }) => (
|
||||
style={[
|
||||
styles.inLibraryBadge,
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
backgroundColor: theme.primary,
|
||||
color: theme.onPrimary,
|
||||
borderRadius: 4,
|
||||
},
|
||||
]}
|
||||
@@ -214,8 +223,8 @@ const UnreadBadge = ({
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
backgroundColor: theme.primary,
|
||||
color: theme.onPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -227,6 +236,7 @@ const DownloadBadge = ({
|
||||
chaptersDownloaded,
|
||||
showUnreadBadges,
|
||||
chaptersUnread,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
@@ -238,6 +248,10 @@ const DownloadBadge = ({
|
||||
!showUnreadBadges && {
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
backgroundColor: theme.tertiary,
|
||||
color: theme.onTertiary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{chaptersDownloaded}
|
||||
@@ -277,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,180 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Pressable,
|
||||
PressableProps,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { useAppearanceSettings, useLibrarySettings } from '../../redux/hooks';
|
||||
|
||||
import { LibraryNovelInfo, NovelInfo } from '../../database/types';
|
||||
import { SourceNovelItem } from '../../sources/types';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import {
|
||||
UnreadBadge,
|
||||
DownloadsBadge,
|
||||
InLibraryBadge,
|
||||
} from './NovelCoverBadges';
|
||||
import { DisplayModes } from '../../redux/settings/settingsSlice';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useDeviceOrientation, useNovelCoverHeight } from '../../hooks';
|
||||
import { DeviceOrientation } from '../../hooks/useDeviceOrientation';
|
||||
|
||||
interface Props extends PressableProps {
|
||||
item: LibraryNovelInfo | SourceNovelItem | NovelInfo;
|
||||
isSelected: boolean;
|
||||
theme: ThemeType;
|
||||
showInLibraryBadge?: boolean;
|
||||
}
|
||||
|
||||
const NovelItem: React.FC<Props> = props => {
|
||||
const { item, showInLibraryBadge = false, theme, isSelected = false } = props;
|
||||
|
||||
const { displayMode } = useAppearanceSettings();
|
||||
const { novelsPerRowLandscape, novelsPerRowPotrait } =
|
||||
useAppearanceSettings();
|
||||
const { showUnreadBadge, showDownloadsBadge } = useLibrarySettings();
|
||||
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const coverHeight = useNovelCoverHeight();
|
||||
|
||||
const coverSelectedStyle = {
|
||||
backgroundColor: theme.primary,
|
||||
opacity: 0.8,
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flex:
|
||||
1 /
|
||||
(orientation === DeviceOrientation.POTRAIT
|
||||
? novelsPerRowPotrait
|
||||
: novelsPerRowLandscape),
|
||||
},
|
||||
styles.container,
|
||||
isSelected && coverSelectedStyle,
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={styles.pressable}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
{...props}
|
||||
>
|
||||
<View style={styles.badgeContainer}>
|
||||
{'chaptersDownloaded' in item &&
|
||||
item.chaptersDownloaded &&
|
||||
showDownloadsBadge ? (
|
||||
<DownloadsBadge
|
||||
unreadCount={item.chaptersUnread}
|
||||
downloadCount={item.chaptersDownloaded}
|
||||
showUnreadBadge={showUnreadBadge}
|
||||
theme={theme}
|
||||
/>
|
||||
) : null}
|
||||
{'chaptersUnread' in item &&
|
||||
item.chaptersUnread &&
|
||||
showUnreadBadge ? (
|
||||
<UnreadBadge
|
||||
unreadCount={item.chaptersUnread}
|
||||
downloadCount={item.chaptersDownloaded}
|
||||
showDownloadsBadge={showDownloadsBadge}
|
||||
theme={theme}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
{showInLibraryBadge ? <InLibraryBadge theme={theme} /> : null}
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={[
|
||||
{ height: coverHeight },
|
||||
styles.image,
|
||||
showInLibraryBadge && styles.inLibrary,
|
||||
]}
|
||||
>
|
||||
{displayMode === DisplayModes.Compact ? (
|
||||
<CompactTitle name={item.novelName} />
|
||||
) : null}
|
||||
</FastImage>
|
||||
{displayMode === DisplayModes.Comfortable ? (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[{ color: theme.textColorPrimary }, styles.title]}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelItem;
|
||||
|
||||
const CompactTitle = ({ name }: { name: string }) => (
|
||||
<View style={styles.compactTitleContainer}>
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.7)']}
|
||||
style={styles.linearGradient}
|
||||
>
|
||||
<Text numberOfLines={2} style={[styles.title, styles.compactTitle]}>
|
||||
{name}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 6,
|
||||
margin: 2,
|
||||
},
|
||||
pressable: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
},
|
||||
badgeContainer: {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 12,
|
||||
left: 12,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
image: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
inLibrary: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
title: {
|
||||
padding: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
compactTitleContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flex: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
linearGradient: {
|
||||
borderRadius: 4,
|
||||
padding: 4,
|
||||
paddingTop: 16,
|
||||
},
|
||||
compactTitle: {
|
||||
color: 'rgba(255,255,255,1)',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: {
|
||||
width: -1,
|
||||
height: 1,
|
||||
},
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text } from 'react-native';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
interface UnreadBadgeProps {
|
||||
unreadCount: number;
|
||||
downloadCount: number;
|
||||
showDownloadsBadge: boolean;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const UnreadBadge: React.FC<UnreadBadgeProps> = ({
|
||||
unreadCount,
|
||||
downloadCount,
|
||||
showDownloadsBadge,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.unreadBadge,
|
||||
(!downloadCount || !showDownloadsBadge) && styles.borderLeftRadius,
|
||||
{ backgroundColor: theme.primary, color: theme.onPrimary },
|
||||
]}
|
||||
>
|
||||
{unreadCount}
|
||||
</Text>
|
||||
);
|
||||
|
||||
interface DownloadsBadgeProps {
|
||||
unreadCount: number;
|
||||
downloadCount: number;
|
||||
showUnreadBadge: boolean;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const DownloadsBadge: React.FC<DownloadsBadgeProps> = ({
|
||||
unreadCount,
|
||||
downloadCount,
|
||||
showUnreadBadge,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.downloadBadge,
|
||||
(!unreadCount || !showUnreadBadge) && styles.borderRightRadius,
|
||||
{ backgroundColor: theme.tertiary, color: theme.onTertiary },
|
||||
]}
|
||||
>
|
||||
{downloadCount}
|
||||
</Text>
|
||||
);
|
||||
|
||||
interface InLibraryBadgeProps {
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const InLibraryBadge: React.FC<InLibraryBadgeProps> = ({ theme }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.inLibraryBadge,
|
||||
{ backgroundColor: theme.primary, color: theme.onPrimary },
|
||||
]}
|
||||
>
|
||||
In library
|
||||
</Text>
|
||||
);
|
||||
|
||||
export { UnreadBadge, DownloadsBadge, InLibraryBadge };
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
borderLeftRadius: {
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
},
|
||||
borderRightRadius: {
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
unreadBadge: {
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
downloadBadge: {
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
inLibraryBadge: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
borderRadius: 4,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
zIndex: 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
|
||||
|
||||
+27
-7
@@ -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,96 +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={{
|
||||
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 '..';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import IconButtonV2 from '../IconButtonV2/IconButtonV2';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface RightIcon {
|
||||
iconName: string;
|
||||
@@ -21,7 +22,7 @@ interface SearcbarProps {
|
||||
handleBackAction?: () => void;
|
||||
clearSearchbar: () => void;
|
||||
onLeftIconPress?: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const Searchbar: React.FC<SearcbarProps> = ({
|
||||
@@ -46,17 +47,20 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
<View
|
||||
style={[
|
||||
styles.searchbarContainer,
|
||||
{ marginTop, backgroundColor: theme.searchBarColor },
|
||||
{
|
||||
marginTop,
|
||||
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 +73,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 +83,7 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
{searchText !== '' ? (
|
||||
<IconButtonV2
|
||||
name="close"
|
||||
color={theme.textColorPrimary}
|
||||
color={theme.onSurface}
|
||||
onPress={clearSearchbar}
|
||||
theme={theme}
|
||||
/>
|
||||
@@ -88,7 +92,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,7 +107,7 @@ export default Searchbar;
|
||||
const styles = StyleSheet.create({
|
||||
searchbarContainer: {
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
marginBottom: 12,
|
||||
minHeight: 46,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
|
||||
@@ -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 { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface SwitchItemProps {
|
||||
value: boolean;
|
||||
label: string;
|
||||
description?: string;
|
||||
onPress: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
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>
|
||||
@@ -52,10 +53,10 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
labelContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
textAlignVertical: 'center',
|
||||
},
|
||||
switch: {
|
||||
marginLeft: 8,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text as NativeText, TextPropTypes } from 'react-native';
|
||||
|
||||
const Text = ({ type, children, style, theme }) => {
|
||||
const color =
|
||||
type === 'primary'
|
||||
? theme.textColorPrimary
|
||||
: type === 'secondary'
|
||||
? theme.textColorSecondary
|
||||
: theme.textColorHint;
|
||||
|
||||
return <NativeText style={[style, { color }]}>{children}</NativeText>;
|
||||
};
|
||||
|
||||
Text.propType = {
|
||||
style: TextPropTypes.style,
|
||||
type: PropTypes.oneOf(['primary', 'secondary', 'hint']),
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -4,9 +4,12 @@ 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';
|
||||
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';
|
||||
|
||||
+61
-13
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,24 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import { sourceManager } from '../../sources/sourceManager';
|
||||
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 (?, ?, ?, ?)';
|
||||
|
||||
export const insertChapters = async (novelId, chapters) => {
|
||||
db.transaction(tx => {
|
||||
chapters.map(
|
||||
chapter =>
|
||||
export const insertChapters = async (
|
||||
novelId: number,
|
||||
chapters: ChapterItem[],
|
||||
) => {
|
||||
db.transaction(
|
||||
tx => {
|
||||
chapters.map(chapter =>
|
||||
tx.executeSql(
|
||||
insertChaptersQuery,
|
||||
[
|
||||
@@ -21,8 +27,8 @@ export const insertChapters = async (novelId, chapters) => {
|
||||
chapter.releaseDate,
|
||||
novelId,
|
||||
],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {},
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => false,
|
||||
// console.log(
|
||||
// "Error ",
|
||||
// error,
|
||||
@@ -32,30 +38,33 @@ export const insertChapters = async (novelId, chapters) => {
|
||||
// novelId
|
||||
// )
|
||||
),
|
||||
(txObj, res) => {
|
||||
// console.log('Success');
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
_err => {
|
||||
// console.log('Error', err);
|
||||
},
|
||||
() => {
|
||||
// console.log('Success ')
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const getChaptersQuery = (sort = 'ORDER BY chapterId ASC', filter = '') =>
|
||||
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
|
||||
|
||||
export const getChapters = (novelId, sort, filter) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
export const getChapters = (novelId: number, sort: string, filter: string) => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChaptersQuery(sort, filter),
|
||||
[novelId],
|
||||
(txObj, { rows: { _array } }) => {
|
||||
(txObj, { rows }) => {
|
||||
const _array = (rows as any)._array;
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
@@ -64,8 +73,8 @@ export const getChapters = (novelId, sort, filter) => {
|
||||
|
||||
const getChapterQuery = 'SELECT * FROM downloads WHERE downloadChapterId = ?';
|
||||
|
||||
export const getChapterFromDB = async chapterId => {
|
||||
return new Promise((resolve, reject) =>
|
||||
export const getChapterFromDB = async (chapterId: number) => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChapterQuery,
|
||||
@@ -73,8 +82,9 @@ export const getChapterFromDB = async chapterId => {
|
||||
(txObj, results) => {
|
||||
resolve(results.rows.item(0));
|
||||
},
|
||||
(txObj, error) => {
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
@@ -96,13 +106,17 @@ export const getPrevChapter = (
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
): Promise<ChapterItem> => {
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getPrevChapterQuery,
|
||||
[novelId, chapterId],
|
||||
(txObj, results) => resolve(results.rows.item(results.rows.length - 1)),
|
||||
(txObj, error) => showToast(error.message),
|
||||
(_txObj, results) =>
|
||||
resolve(results.rows.item(results.rows.length - 1)),
|
||||
(_txObj, error) => {
|
||||
showToast(error.message);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -123,13 +137,16 @@ export const getNextChapter = (
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
): Promise<ChapterItem> => {
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getNextChapterQuery,
|
||||
[novelId, chapterId],
|
||||
(txObj, results) => resolve(results.rows.item(0)),
|
||||
(txObj, error) => showToast(error.message),
|
||||
(_txObj, results) => resolve(results.rows.item(0)),
|
||||
(_txObj, error) => {
|
||||
showToast(error.message);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -138,14 +155,15 @@ export const getNextChapter = (
|
||||
const markChapterReadQuery =
|
||||
'UPDATE chapters SET `read` = 1 WHERE chapterId = ?';
|
||||
|
||||
export const markChapterRead = async chapterId => {
|
||||
export const markChapterRead = async (chapterId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markChapterReadQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -154,14 +172,15 @@ export const markChapterRead = async chapterId => {
|
||||
const markChapterUnreadQuery =
|
||||
'UPDATE chapters SET `read` = 0 WHERE chapterId = ?';
|
||||
|
||||
export const markChapterUnread = async chapterId => {
|
||||
export const markChapterUnread = async (chapterId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markChapterUnreadQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -170,14 +189,15 @@ export const markChapterUnread = async chapterId => {
|
||||
const markAllChaptersReadQuery =
|
||||
'UPDATE chapters SET `read` = 1 WHERE novelId = ?';
|
||||
|
||||
export const markAllChaptersRead = async novelId => {
|
||||
export const markAllChaptersRead = async (novelId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markAllChaptersReadQuery,
|
||||
[novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -186,14 +206,15 @@ export const markAllChaptersRead = async novelId => {
|
||||
const markAllChaptersUnreadQuery =
|
||||
'UPDATE chapters SET `read` = 0 WHERE novelId = ?';
|
||||
|
||||
export const markAllChaptersUnread = async novelId => {
|
||||
export const markAllChaptersUnread = async (novelId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markAllChaptersUnreadQuery,
|
||||
[novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -202,8 +223,8 @@ export const markAllChaptersUnread = async novelId => {
|
||||
const isChapterDownloadedQuery =
|
||||
'SELECT * FROM downloads WHERE downloadChapterId=?';
|
||||
|
||||
export const isChapterDownloaded = async chapterId => {
|
||||
return new Promise((resolve, reject) =>
|
||||
export const isChapterDownloaded = async (chapterId: number) => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
isChapterDownloadedQuery,
|
||||
@@ -215,8 +236,9 @@ export const isChapterDownloaded = async chapterId => {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
(txObj, error) => {
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
@@ -226,55 +248,171 @@ export const isChapterDownloaded = async chapterId => {
|
||||
const downloadChapterQuery =
|
||||
'INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)';
|
||||
|
||||
export const downloadChapter = async (
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterId,
|
||||
) => {
|
||||
const source = sourceManager(sourceId);
|
||||
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql('UPDATE chapters SET downloaded = 1 WHERE chapterId = ?', [
|
||||
chapterId,
|
||||
]);
|
||||
tx.executeSql(
|
||||
downloadChapterQuery,
|
||||
[chapterId, chapter.chapterName, chapter.chapterText],
|
||||
(txObj, res) => {
|
||||
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
const createImageFolder = async (
|
||||
path: string,
|
||||
data?: {
|
||||
sourceId: number;
|
||||
novelId: number;
|
||||
chapterId: number;
|
||||
},
|
||||
): Promise<string> => {
|
||||
const mkdirIfNot = async (p: string) => {
|
||||
if (!(await RNFetchBlob.fs.exists(p))) {
|
||||
await RNFetchBlob.fs.mkdir(p);
|
||||
}
|
||||
};
|
||||
await mkdirIfNot(path);
|
||||
if (data) {
|
||||
const { sourceId, novelId, chapterId } = data;
|
||||
await mkdirIfNot(`${path}/${sourceId}/`);
|
||||
await mkdirIfNot(`${path}/${sourceId}/${novelId}/`);
|
||||
await mkdirIfNot(`${path}/${sourceId}/${novelId}/${chapterId}/`);
|
||||
return `${path}/${sourceId}/${novelId}/${chapterId}/`;
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteChapter = async chapterId => {
|
||||
const downloadImages = async (
|
||||
html: string,
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const loadedCheerio = cheerio.load(html);
|
||||
const imgs = loadedCheerio('img').toArray();
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
const elem = loadedCheerio(imgs[i]);
|
||||
const url = elem.attr('src');
|
||||
if (url) {
|
||||
const imageb64 = (await RNFetchBlob.fetch('GET', url)).base64();
|
||||
const fileurl =
|
||||
(await createImageFolder(
|
||||
`${RNFetchBlob.fs.dirs.DownloadDir}/LNReader`,
|
||||
{ sourceId, novelId, chapterId },
|
||||
)) +
|
||||
i +
|
||||
'.b64.png';
|
||||
elem.replaceWith(
|
||||
`<img type="file" src="${url}" file-src="${fileurl}" file-id="${i}">`,
|
||||
);
|
||||
const exists = await RNFetchBlob.fs.exists(fileurl);
|
||||
if (!exists) {
|
||||
RNFetchBlob.fs.createFile(fileurl, imageb64, 'utf8');
|
||||
} else {
|
||||
RNFetchBlob.fs.writeFile(fileurl, imageb64, 'utf8');
|
||||
}
|
||||
}
|
||||
}
|
||||
return loadedCheerio.html();
|
||||
} catch (e) {
|
||||
return html;
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadChapter = async (
|
||||
sourceId: number,
|
||||
novelUrl: string,
|
||||
novelId: number,
|
||||
chapterUrl: string,
|
||||
chapterId: number,
|
||||
) => {
|
||||
try {
|
||||
const source = sourceManager(sourceId);
|
||||
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
|
||||
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) => {
|
||||
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) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteChapter = async (
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
) => {
|
||||
const updateIsDownloadedQuery =
|
||||
'UPDATE chapters SET downloaded = 0 WHERE chapterId=?';
|
||||
const deleteChapterQuery = 'DELETE FROM downloads WHERE downloadChapterId=?';
|
||||
|
||||
await deleteDownloadedImages(sourceId, novelId, chapterId);
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
updateIsDownloadedQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
tx.executeSql(
|
||||
deleteChapterQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {
|
||||
(_txObj, _res) => {
|
||||
// console.log('Chapter deleted');
|
||||
},
|
||||
(txObj, error) => {
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -288,15 +426,16 @@ const getLastReadChapterQuery = `
|
||||
WHERE history.historyNovelId = ?
|
||||
`;
|
||||
|
||||
export const getLastReadChapter = async novelId => {
|
||||
return new Promise((resolve, reject) => {
|
||||
export const getLastReadChapter = async (novelId: number) => {
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getLastReadChapterQuery,
|
||||
[novelId],
|
||||
(txObj, { rows }) => resolve(rows.item(0)),
|
||||
(txObj, error) => {
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -306,14 +445,15 @@ export const getLastReadChapter = async novelId => {
|
||||
const bookmarkChapterQuery =
|
||||
'UPDATE chapters SET bookmark = ? WHERE chapterId = ?';
|
||||
|
||||
export const bookmarkChapter = async (bookmark, chapterId) => {
|
||||
export const bookmarkChapter = async (bookmark: boolean, chapterId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
bookmarkChapterQuery,
|
||||
[!bookmark, chapterId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -322,14 +462,18 @@ export const bookmarkChapter = async (bookmark, chapterId) => {
|
||||
const markPreviuschaptersReadQuery =
|
||||
'UPDATE chapters SET `read` = 1 WHERE chapterId < ? AND novelId = ?';
|
||||
|
||||
export const markPreviuschaptersRead = async (chapterId, novelId) => {
|
||||
export const markPreviuschaptersRead = async (
|
||||
chapterId: number,
|
||||
novelId: number,
|
||||
) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markPreviuschaptersReadQuery,
|
||||
[chapterId, novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -338,14 +482,18 @@ export const markPreviuschaptersRead = async (chapterId, novelId) => {
|
||||
const markPreviousChaptersUnreadQuery =
|
||||
'UPDATE chapters SET `read` = 0 WHERE chapterId < ? AND novelId = ?';
|
||||
|
||||
export const markPreviousChaptersUnread = async (chapterId, novelId) => {
|
||||
export const markPreviousChaptersUnread = async (
|
||||
chapterId: number,
|
||||
novelId: number,
|
||||
) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markPreviousChaptersUnreadQuery,
|
||||
[chapterId, novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -359,16 +507,18 @@ const getDownloadedChaptersQuery = `
|
||||
WHERE chapters.downloaded = 1`;
|
||||
|
||||
export const getDownloadedChapters = () => {
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getDownloadedChaptersQuery,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
undefined,
|
||||
(txObj, { rows }) => {
|
||||
const _array = (rows as any)._array;
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -83,3 +83,52 @@ export const getChapterFromDb = async (
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteReadChaptersFromDbQuery = `
|
||||
DELETE FROM
|
||||
downloads
|
||||
WHERE
|
||||
downloads.downloadChapterId IN (
|
||||
SELECT
|
||||
chapters.chapterId
|
||||
FROM
|
||||
downloads
|
||||
INNER JOIN chapters ON chapters.chapterId = downloads.downloadChapterId
|
||||
WHERE
|
||||
chapters.read = 1
|
||||
);
|
||||
`;
|
||||
|
||||
let updateChaptersDeletedQuery = `
|
||||
UPDATE
|
||||
chapters
|
||||
SET
|
||||
downloaded = 0
|
||||
WHERE
|
||||
chapters.chapterId IN (
|
||||
SELECT
|
||||
downloads.downloadChapterId
|
||||
FROM
|
||||
downloads
|
||||
INNER JOIN chapters ON chapters.chapterId = downloads.downloadChapterId
|
||||
WHERE
|
||||
chapters.read = 1
|
||||
);
|
||||
`;
|
||||
|
||||
export const deleteReadChaptersFromDb = (chapterId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
updateChaptersDeletedQuery,
|
||||
[],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => showToast(error.message),
|
||||
);
|
||||
tx.executeSql(
|
||||
deleteReadChaptersFromDbQuery,
|
||||
[],
|
||||
(txObj, res) => showToast('Deleted read chapters.'),
|
||||
(txObj, error) => showToast(error.message),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getLibraryQuery = (sort, filter) => `
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
|
||||
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
|
||||
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(_array);
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const searchLibraryQuery = (searchText, sort, filter) =>
|
||||
`
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
|
||||
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
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -35,7 +35,7 @@ const getUpdatesFromDbQuery = `
|
||||
WHERE
|
||||
date(updates.updateTime) > date('now','-3 months')
|
||||
ORDER BY
|
||||
updates.updateTime DESC
|
||||
updates.updateTime 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';
|
||||
@@ -1,9 +0,0 @@
|
||||
export const createDownloadTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS downloads(
|
||||
downloadId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
downloadChapterId INTEGER NOT NULL,
|
||||
chapterName TEXT,
|
||||
chapterText TEXT,
|
||||
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
@@ -0,0 +1,9 @@
|
||||
export const createDownloadTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS downloads(
|
||||
downloadId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
downloadChapterId INTEGER NOT NULL,
|
||||
chapterName TEXT,
|
||||
chapterText TEXT,
|
||||
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId) ON DELETE CASCADE
|
||||
)
|
||||
`;
|
||||
@@ -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]"';
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface NovelInfo {
|
||||
export interface LibraryNovelInfo extends NovelInfo {
|
||||
chaptersUnread: number;
|
||||
chaptersDownloaded: number;
|
||||
categoryIds: string;
|
||||
}
|
||||
|
||||
export interface ChapterItem {
|
||||
@@ -66,3 +67,10 @@ export interface Update {
|
||||
bookmark: number;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
lastUpdatedAt: Date;
|
||||
sort: number | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { showToast } from '@hooks/showToast';
|
||||
import { SQLError, SQLTransaction } from 'expo-sqlite';
|
||||
|
||||
export const txnErrorCallback = (
|
||||
txnObj: SQLTransaction,
|
||||
error: SQLError,
|
||||
): boolean => {
|
||||
showToast(error.message);
|
||||
return false;
|
||||
};
|
||||
|
||||
export const txnErrorCallbackWithoutToast = (): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export const dbTxnErrorCallback = (error: SQLError): void =>
|
||||
showToast(error.message);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { appversion } from '../utils/utils';
|
||||
import { appVersion } from '../utils/versionUtils';
|
||||
|
||||
export const useGithubUpdateChecker = () => {
|
||||
const latestReleaseUrl =
|
||||
@@ -23,7 +23,7 @@ export const useGithubUpdateChecker = () => {
|
||||
};
|
||||
|
||||
const isNewVersion = versionTag => {
|
||||
let currentVersion = `${appversion}`;
|
||||
let currentVersion = `${appVersion}`;
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
let newVersion = versionTag.replace(regex, '');
|
||||
|
||||
+4
-2
@@ -4,6 +4,8 @@ export { default as useUpdates } from './useUpdates';
|
||||
export { default as useFullscreenMode } from './useFullscreenMode';
|
||||
export { default as useChapter } from './useChapter';
|
||||
export { default as useNextAndPrevChapter } from './useNextAndPrevChapter';
|
||||
export { default as useDeviceOrientation } from './useDeviceOrientation';
|
||||
export { default as useNovelCoverHeight } from './useNovelCoverHeight';
|
||||
export { default as useNovelTrackerInfo } from './useNovelTrackerInfo';
|
||||
export { default as useBoolean } from './useBoolean';
|
||||
export { useTheme } from './useTheme';
|
||||
export { default as useCategories } from './useCategories';
|
||||
export { useCategorySettings, useLibrarySettings } from './useSettings';
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { darkTheme } from '../theme/v1/theme/theme';
|
||||
|
||||
const useTheme = () => {
|
||||
const theme = useSelector(state => state.settingsReducer.theme);
|
||||
|
||||
return theme || darkTheme;
|
||||
};
|
||||
|
||||
const useReaderSettings = () => {
|
||||
const readerSettings = useSelector(state => state.settingsReducer.reader);
|
||||
|
||||
return readerSettings;
|
||||
};
|
||||
|
||||
const useSettings = () => {
|
||||
const settings = useSelector(state => state.settingsReducer);
|
||||
@@ -19,12 +6,6 @@ const useSettings = () => {
|
||||
return settings;
|
||||
};
|
||||
|
||||
const useLibrary = () => {
|
||||
const library = useSelector(state => state.libraryReducer.novels);
|
||||
|
||||
return library;
|
||||
};
|
||||
|
||||
const useNovel = () => {
|
||||
const novel = useSelector(state => state.novelReducer);
|
||||
|
||||
@@ -110,16 +91,7 @@ const useTrackingStatus = () => {
|
||||
return tracker;
|
||||
};
|
||||
|
||||
const useLibraryFilters = () => {
|
||||
const filters = useSelector(state => state.libraryReducer.filters);
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
export {
|
||||
useTheme,
|
||||
useReaderSettings,
|
||||
useLibrary,
|
||||
useSettings,
|
||||
usePreferences,
|
||||
useContinueReading,
|
||||
@@ -127,6 +99,5 @@ export {
|
||||
useNovel,
|
||||
useChapter,
|
||||
useSavedSettings,
|
||||
useLibraryFilters,
|
||||
usePosition,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { BackHandler } from 'react-native';
|
||||
|
||||
export function useBackHandler(handler: () => boolean) {
|
||||
useEffect(() => {
|
||||
BackHandler.addEventListener('hardwareBackPress', handler);
|
||||
|
||||
return () => BackHandler.removeEventListener('hardwareBackPress', handler);
|
||||
}, [handler]);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
||||
|
||||
interface ReturnType {
|
||||
value: boolean;
|
||||
setValue: Dispatch<SetStateAction<boolean>>;
|
||||
setTrue: () => void;
|
||||
setFalse: () => void;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
const useBoolean = (defaultValue?: boolean): ReturnType => {
|
||||
const [value, setValue] = useState(!!defaultValue);
|
||||
|
||||
const setTrue = useCallback(() => setValue(true), []);
|
||||
const setFalse = useCallback(() => setValue(false), []);
|
||||
const toggle = useCallback(() => setValue(x => !x), []);
|
||||
|
||||
return { value, setValue, setTrue, setFalse, toggle };
|
||||
};
|
||||
|
||||
export default useBoolean;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getCategoriesFromDb } from '@database/queries/CategoryQueries';
|
||||
import { Category } from '@database/types';
|
||||
|
||||
const useCategories = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const getCategories = async () => {
|
||||
try {
|
||||
const res = await getCategoriesFromDb();
|
||||
setCategories(res);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getCategories();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
categories,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCategories;
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useAppDispatch, useSavedChapterData } from '../redux/hooks';
|
||||
import { saveChapterProgress } from '../redux/localStorage/localStorageSlice';
|
||||
|
||||
export const useChapterProgress = (chapterId: number, readerRef: any) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { progressPercentage = 0, progressLocation = 0 } =
|
||||
useSavedChapterData(chapterId);
|
||||
|
||||
const [percentage, setPercentage] = useState(progressPercentage);
|
||||
const [location, setLocation] = useState(progressLocation);
|
||||
|
||||
const percentageRef = useRef(0);
|
||||
percentageRef.current = percentage;
|
||||
|
||||
const locationRef = useRef(0);
|
||||
locationRef.current = location;
|
||||
|
||||
const setProgress = ({ nativeEvent }: { nativeEvent: any }) => {
|
||||
const currentOffsetY = nativeEvent.contentOffset.y;
|
||||
const currentPosition =
|
||||
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height;
|
||||
|
||||
const currentProgressPercentage = Math.round(
|
||||
(currentPosition / nativeEvent.contentSize.height) * 100,
|
||||
);
|
||||
|
||||
setPercentage(currentProgressPercentage);
|
||||
setLocation(currentOffsetY);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(
|
||||
saveChapterProgress({
|
||||
chapterId,
|
||||
progressLocation: locationRef.current,
|
||||
progressPercentage: percentageRef.current,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [firstLayout, setFirstLayout] = useState(true);
|
||||
|
||||
const scrollToSavedProgress = () => {
|
||||
if (firstLayout) {
|
||||
percentage < 100 &&
|
||||
readerRef.current.scrollTo({
|
||||
x: 0,
|
||||
y: location,
|
||||
animated: false,
|
||||
});
|
||||
setFirstLayout(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
progressLocation,
|
||||
progressPercentage: percentage,
|
||||
setProgress,
|
||||
scrollToSavedProgress,
|
||||
};
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
export enum DeviceOrientation {
|
||||
POTRAIT = 'potrait',
|
||||
LANDSCAPE = 'landscape',
|
||||
}
|
||||
|
||||
const useDeviceOrientation = (): DeviceOrientation => {
|
||||
const window = useWindowDimensions();
|
||||
|
||||
if (window.width > window.height) {
|
||||
return DeviceOrientation.LANDSCAPE;
|
||||
} else {
|
||||
return DeviceOrientation.POTRAIT;
|
||||
}
|
||||
};
|
||||
|
||||
export default useDeviceOrientation;
|
||||
@@ -1,29 +1,33 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useReaderSettingsV1, useSettingsV1, useThemeV1 } from '../redux/hooks';
|
||||
import color from 'color';
|
||||
import { useReaderSettings, useSettingsV1 } from '../redux/hooks';
|
||||
import {
|
||||
changeNavigationBarColor,
|
||||
hideNavigationBar,
|
||||
showNavigationBar,
|
||||
} from '../theme/NativeModules/NavigationBarColor';
|
||||
import { readerBackground } from '../screens/reader/utils/readerStyles';
|
||||
import { useTheme } from './useTheme';
|
||||
|
||||
const useFullscreenMode = () => {
|
||||
const { addListener } = useNavigation();
|
||||
const readerSettings = useReaderSettingsV1();
|
||||
const readerSettings = useReaderSettings();
|
||||
const { fullScreenMode } = useSettingsV1();
|
||||
|
||||
const backgroundColor = readerBackground(readerSettings.theme);
|
||||
|
||||
const theme = useThemeV1();
|
||||
const theme = useTheme();
|
||||
|
||||
const setImmersiveMode = useCallback(() => {
|
||||
if (fullScreenMode) {
|
||||
StatusBar.setHidden(true);
|
||||
hideNavigationBar();
|
||||
} else {
|
||||
StatusBar.setBarStyle(
|
||||
color(backgroundColor).isDark() ? 'light-content' : 'dark-content',
|
||||
);
|
||||
StatusBar.setBackgroundColor(backgroundColor);
|
||||
changeNavigationBarColor(backgroundColor as any);
|
||||
}
|
||||
@@ -41,7 +45,7 @@ const useFullscreenMode = () => {
|
||||
useEffect(() => {
|
||||
const unsubscribe = addListener('beforeRemove', () => {
|
||||
showStatusAndNavBar();
|
||||
StatusBar.setBarStyle(theme.statusBar);
|
||||
StatusBar.setBarStyle(theme.isDark ? 'light-content' : 'dark-content');
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
|
||||
@@ -27,7 +27,10 @@ const useHistory = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllHistory = () => deleteAllHistory();
|
||||
const clearAllHistory = () => {
|
||||
deleteAllHistory();
|
||||
getHistory();
|
||||
};
|
||||
|
||||
const removeChapterFromHistory = async (historyId: number) => {
|
||||
deleteChapterHistory(historyId);
|
||||
@@ -39,6 +42,7 @@ const useHistory = () => {
|
||||
getHistory();
|
||||
}, []),
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
history,
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
|
||||
import { LibraryNovelInfo } from '../database/types';
|
||||
import { getLibraryNovelsFromDb } from '../database/queries/LibraryQueries';
|
||||
import { useLibrarySettings } from '../redux/hooks';
|
||||
|
||||
const useLibrary = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [novels, setNovels] = useState<LibraryNovelInfo[]>([]);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const { filters, sort } = useLibrarySettings();
|
||||
|
||||
const getLibraryNovels = useCallback(async () => {
|
||||
try {
|
||||
const res = await getLibraryNovelsFromDb(filters, sort);
|
||||
setNovels(res);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [filters, sort]);
|
||||
|
||||
const getLibrarySearchResults = useCallback(
|
||||
async (searchText: string) => {
|
||||
try {
|
||||
const res = await getLibraryNovelsFromDb(filters, sort, searchText);
|
||||
setNovels(res);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[filters, sort],
|
||||
);
|
||||
|
||||
const clearSearchResults = () => getLibraryNovels();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
getLibraryNovels();
|
||||
}, [getLibraryNovels]),
|
||||
);
|
||||
return {
|
||||
isLoading,
|
||||
novels,
|
||||
error,
|
||||
getLibrarySearchResults,
|
||||
clearSearchResults,
|
||||
getLibraryNovels,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLibrary;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const showModal = () => setVisible(true);
|
||||
const hideModal = () => setVisible(false);
|
||||
|
||||
return { visible, showModal, hideModal };
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
import { useDeviceOrientation } from '../hooks';
|
||||
import { DeviceOrientation } from '../hooks/useDeviceOrientation';
|
||||
import { useAppearanceSettings } from '../redux/hooks';
|
||||
|
||||
const useNovelCoverHeight = () => {
|
||||
const window = useWindowDimensions();
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const { novelsPerRowLandscape, novelsPerRowPotrait } =
|
||||
useAppearanceSettings();
|
||||
|
||||
const [coverHeight, setCoverHeight] = useState<number>();
|
||||
|
||||
const getCoverHeight = useCallback(
|
||||
() =>
|
||||
(window.width /
|
||||
(orientation === DeviceOrientation.POTRAIT
|
||||
? novelsPerRowPotrait
|
||||
: novelsPerRowLandscape)) *
|
||||
(4 / 3),
|
||||
[window.width, novelsPerRowPotrait, novelsPerRowLandscape, orientation],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCoverHeight(getCoverHeight());
|
||||
}, [getCoverHeight]);
|
||||
|
||||
return coverHeight;
|
||||
};
|
||||
|
||||
export default useNovelCoverHeight;
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useAppSelector } from '../redux/hooks';
|
||||
|
||||
const useNovelTrackerInfo = (novelId: number) => {
|
||||
const { trackedNovels } = useAppSelector(state => state.trackerReducer);
|
||||
const tracker = useAppSelector(state => state.trackerReducer) as any;
|
||||
|
||||
const isTracked = trackedNovels.find(
|
||||
const isTracked = tracker.trackedNovels.find(
|
||||
(novel: any) => novel.novelId === novelId,
|
||||
);
|
||||
|
||||
return { isTracked };
|
||||
return { isTracked, isTrackerAvailable: tracker.tracker };
|
||||
};
|
||||
|
||||
export default useNovelTrackerInfo;
|
||||
|
||||
+13
-3
@@ -1,9 +1,19 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
const useSearch = (defaultSearchText?: string) => {
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const [searchText, setSearchText] = useState<string>(defaultSearchText || '');
|
||||
|
||||
const useSearch = () => {
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const clearSearchbar = useCallback(() => setSearchText(''), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocused) {
|
||||
clearSearchbar();
|
||||
}
|
||||
}, [isFocused, clearSearchbar]);
|
||||
|
||||
return { searchText, setSearchText, clearSearchbar };
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useMMKVObject } from 'react-native-mmkv';
|
||||
import { CategorySettings, LibrarySettings } from '../utils/constants/settings';
|
||||
|
||||
export const useLibrarySettings = () => {
|
||||
const [librarySettings, setSettings] =
|
||||
useMMKVObject<LibrarySettings>('LibrarySettings');
|
||||
|
||||
const setLibrarySettings = (value: Partial<LibrarySettings>) =>
|
||||
setSettings({ ...librarySettings, ...value });
|
||||
|
||||
return {
|
||||
...librarySettings,
|
||||
setLibrarySettings,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCategorySettings = () => {
|
||||
const [categorySettings, setSettings] =
|
||||
useMMKVObject<CategorySettings>('CategorySettings');
|
||||
|
||||
const setCategorySettings = (value: Partial<CategorySettings>) =>
|
||||
setSettings({ ...categorySettings, ...value });
|
||||
|
||||
return {
|
||||
...categorySettings,
|
||||
setCategorySettings,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
useMMKVBoolean,
|
||||
useMMKVObject,
|
||||
useMMKVString,
|
||||
} from 'react-native-mmkv';
|
||||
import { Appearance } from 'react-native';
|
||||
import { defaultTheme } from '../theme/md3/defaultTheme';
|
||||
import { MD3ThemeType } from '../theme/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useTheme = (): MD3ThemeType => {
|
||||
const [appTheme] = useMMKVObject<MD3ThemeType>('APP_THEME');
|
||||
const [isAmoledBlack] = useMMKVBoolean('AMOLED_BLACK');
|
||||
const [customAccent] = useMMKVString('CUSTOM_ACCENT_COLOR');
|
||||
|
||||
const theme: MD3ThemeType = useMemo(() => {
|
||||
let colors =
|
||||
appTheme ||
|
||||
((Appearance.getColorScheme() === 'dark'
|
||||
? defaultTheme.dark
|
||||
: defaultTheme.light) as MD3ThemeType);
|
||||
|
||||
if (isAmoledBlack && colors.isDark) {
|
||||
colors = { ...colors, background: '#000000', surface: '#000000' };
|
||||
}
|
||||
|
||||
if (customAccent) {
|
||||
colors = { ...colors, primary: customAccent, secondary: customAccent };
|
||||
}
|
||||
|
||||
return colors;
|
||||
}, [appTheme?.id, isAmoledBlack, customAccent]);
|
||||
|
||||
return theme;
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
import { SafeAreaProviderCompat } from '@react-navigation/elements';
|
||||
import {
|
||||
MaterialBottomTabDescriptorMap,
|
||||
MaterialBottomTabNavigationConfig,
|
||||
MaterialBottomTabNavigationHelpers,
|
||||
} from '@react-navigation/material-bottom-tabs/lib/typescript/src/types';
|
||||
import {
|
||||
CommonActions,
|
||||
Link,
|
||||
ParamListBase,
|
||||
Route,
|
||||
TabNavigationState,
|
||||
useLinkBuilder,
|
||||
} from '@react-navigation/native';
|
||||
import * as React from 'react';
|
||||
import { Platform, StyleSheet, Text } from 'react-native';
|
||||
import { BottomNavigation } from 'react-native-paper';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
type Props = MaterialBottomTabNavigationConfig & {
|
||||
state: TabNavigationState<ParamListBase>;
|
||||
navigation: MaterialBottomTabNavigationHelpers;
|
||||
descriptors: MaterialBottomTabDescriptorMap;
|
||||
};
|
||||
|
||||
type Scene = { route: { key: string } };
|
||||
|
||||
// Optionally require vector-icons referenced from react-native-paper:
|
||||
// https://github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx#L14
|
||||
let MaterialCommunityIcons: React.ComponentType<
|
||||
React.ComponentProps<
|
||||
typeof import('react-native-vector-icons/MaterialCommunityIcons').default
|
||||
>
|
||||
>;
|
||||
|
||||
try {
|
||||
// Optionally require vector-icons
|
||||
MaterialCommunityIcons =
|
||||
require('react-native-vector-icons/MaterialCommunityIcons').default;
|
||||
} catch (e: any) {
|
||||
let isErrorLogged = false;
|
||||
|
||||
// Fallback component for icons
|
||||
MaterialCommunityIcons = ({
|
||||
name,
|
||||
color,
|
||||
size,
|
||||
selectionColor: _0,
|
||||
onLayout: _1,
|
||||
...rest
|
||||
}) => {
|
||||
if (!isErrorLogged) {
|
||||
if (
|
||||
!/(Cannot find module|Module not found|Cannot resolve module)/.test(
|
||||
e.message,
|
||||
)
|
||||
) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Tried to use the icon '${name}' in a component from '@react-navigation/material-bottom-tabs', but 'react-native-vector-icons/MaterialCommunityIcons' could not be loaded.`,
|
||||
"To remove this warning, try installing 'react-native-vector-icons' or use another method to specify icon: https://reactnavigation.org/docs/material-bottom-tab-navigator/#tabbaricon.",
|
||||
);
|
||||
|
||||
isErrorLogged = true;
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-expect-error: we're passing icon props to text, but we don't care about it since it's just fallback
|
||||
<Text
|
||||
{...rest}
|
||||
style={[
|
||||
styles.icon,
|
||||
{
|
||||
color: typeof color !== 'number' ? color : undefined,
|
||||
fontSize: size,
|
||||
},
|
||||
]}
|
||||
>
|
||||
□
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function MaterialBottomTabViewInner({
|
||||
state,
|
||||
navigation,
|
||||
descriptors,
|
||||
...rest
|
||||
}: Props) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const buildLink = useLinkBuilder();
|
||||
|
||||
return (
|
||||
<BottomNavigation
|
||||
{...rest}
|
||||
navigationState={state}
|
||||
onIndexChange={(index: number) =>
|
||||
navigation.dispatch({
|
||||
...CommonActions.navigate({
|
||||
name: state.routes[index].name,
|
||||
merge: true,
|
||||
}),
|
||||
target: state.key,
|
||||
})
|
||||
}
|
||||
renderScene={({ route }) => descriptors[route.key].render()}
|
||||
renderTouchable={
|
||||
Platform.OS === 'web'
|
||||
? ({
|
||||
onPress,
|
||||
route,
|
||||
accessibilityRole: _0,
|
||||
borderless: _1,
|
||||
centered: _2,
|
||||
rippleColor: _3,
|
||||
style,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<Link
|
||||
{...rest}
|
||||
// @ts-expect-error: to could be undefined, but it doesn't affect functionality
|
||||
to={buildLink(route.name, route.params)}
|
||||
accessibilityRole="link"
|
||||
onPress={(e: any) => {
|
||||
if (
|
||||
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
|
||||
(e.button == null || e.button === 0) // ignore everything but left clicks
|
||||
) {
|
||||
e.preventDefault();
|
||||
onPress?.(e);
|
||||
}
|
||||
}}
|
||||
style={[styles.touchable, style]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
renderIcon={({ route, focused, color }) => {
|
||||
const { options } = descriptors[route.key];
|
||||
|
||||
if (typeof options.tabBarIcon === 'string') {
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
name={options.tabBarIcon}
|
||||
color={color}
|
||||
size={24}
|
||||
style={styles.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof options.tabBarIcon === 'function') {
|
||||
return options.tabBarIcon({ focused, color });
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
getLabelText={({ route }: Scene) => {
|
||||
const { options } = descriptors[route.key];
|
||||
|
||||
return options.tabBarLabel !== undefined
|
||||
? options.tabBarLabel
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: (route as Route<string>).name;
|
||||
}}
|
||||
getColor={({ route }) => descriptors[route.key].options.tabBarColor}
|
||||
getBadge={({ route }) => descriptors[route.key].options.tabBarBadge}
|
||||
getAccessibilityLabel={({ route }) =>
|
||||
descriptors[route.key].options.tabBarAccessibilityLabel
|
||||
}
|
||||
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
|
||||
onTabPress={({ route, preventDefault }) => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
preventDefault();
|
||||
}
|
||||
}}
|
||||
safeAreaInsets={insets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MaterialBottomTabView(props: Props) {
|
||||
return (
|
||||
<SafeAreaProviderCompat>
|
||||
<MaterialBottomTabViewInner {...props} />
|
||||
</SafeAreaProviderCompat>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
touchable: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
createNavigatorFactory,
|
||||
DefaultNavigatorOptions,
|
||||
ParamListBase,
|
||||
TabActionHelpers,
|
||||
TabNavigationState,
|
||||
TabRouter,
|
||||
TabRouterOptions,
|
||||
useNavigationBuilder,
|
||||
} from '@react-navigation/native';
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
MaterialBottomTabNavigationConfig,
|
||||
MaterialBottomTabNavigationEventMap,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
} from '@react-navigation/material-bottom-tabs/lib/typescript/src/types';
|
||||
import MaterialBottomTabView from './MaterialBottomTabView';
|
||||
|
||||
type Props = DefaultNavigatorOptions<
|
||||
ParamListBase,
|
||||
TabNavigationState<ParamListBase>,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationEventMap
|
||||
> &
|
||||
TabRouterOptions &
|
||||
MaterialBottomTabNavigationConfig;
|
||||
|
||||
function MaterialBottomTabNavigator({
|
||||
id,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenListeners,
|
||||
screenOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { state, descriptors, navigation, NavigationContent } =
|
||||
useNavigationBuilder<
|
||||
TabNavigationState<ParamListBase>,
|
||||
TabRouterOptions,
|
||||
TabActionHelpers<ParamListBase>,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationEventMap
|
||||
>(TabRouter, {
|
||||
id,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenListeners,
|
||||
screenOptions,
|
||||
});
|
||||
|
||||
return (
|
||||
<NavigationContent>
|
||||
<MaterialBottomTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
</NavigationContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default createNavigatorFactory<
|
||||
TabNavigationState<ParamListBase>,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationEventMap,
|
||||
typeof MaterialBottomTabNavigator
|
||||
>(MaterialBottomTabNavigator);
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import Library from '../screens/library/LibraryScreen';
|
||||
@@ -9,8 +8,11 @@ import History from '../screens/history/HistoryScreen';
|
||||
import Browse from '../screens/browse/BrowseScreen';
|
||||
import More from '../screens/more/MoreScreen';
|
||||
|
||||
import { useSettings, useTheme } from '../hooks/reduxHooks';
|
||||
import { useSettings } from '../hooks/reduxHooks';
|
||||
import { getString } from '../../strings/translations';
|
||||
import createMaterialBottomTabNavigator from './BottomNavigation/createMaterialBottomTabNavigator';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import Color from 'color';
|
||||
|
||||
const Tab = createMaterialBottomTabNavigator();
|
||||
|
||||
@@ -25,21 +27,22 @@ const BottomNavigator = () => {
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
barStyle={{ backgroundColor: theme.colorPrimary }}
|
||||
activeColor={theme.colorAccent}
|
||||
theme={{
|
||||
colors: {
|
||||
...theme,
|
||||
background: theme.background,
|
||||
elevation: {
|
||||
level2: Color(theme.primary).alpha(0.08).string(),
|
||||
},
|
||||
},
|
||||
}}
|
||||
shifting={!showLabelsInNav}
|
||||
>
|
||||
<Tab.Screen
|
||||
name={getString('library')}
|
||||
component={Library}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="book-variant-multiple"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
tabBarIcon: 'book-variant-multiple',
|
||||
}}
|
||||
/>
|
||||
{showUpdatesTab && (
|
||||
@@ -47,13 +50,7 @@ const BottomNavigator = () => {
|
||||
name={getString('updates')}
|
||||
component={Updates}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="alert-decagram-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
tabBarIcon: 'alert-decagram-outline',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -62,9 +59,7 @@ const BottomNavigator = () => {
|
||||
name={getString('history')}
|
||||
component={History}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons name="history" color={color} size={24} />
|
||||
),
|
||||
tabBarIcon: 'history',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -72,26 +67,14 @@ const BottomNavigator = () => {
|
||||
name={getString('browse')}
|
||||
component={Browse}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="compass-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
tabBarIcon: 'compass-outline',
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={getString('more')}
|
||||
component={More}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="dots-horizontal"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
tabBarIcon: 'dots-horizontal',
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
|
||||
+26
-10
@@ -4,7 +4,7 @@ import { NavigationContainer } from '@react-navigation/native';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
|
||||
import { setBarColor } from '../theme/utils/setBarColor';
|
||||
import { useTheme } from '../hooks/reduxHooks';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useGithubUpdateChecker } from '../hooks/githubUpdateChecker';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
|
||||
@@ -19,8 +19,8 @@ import MoreStack from './MoreStack';
|
||||
*/
|
||||
import Novel from '../screens/novel/NovelScreen';
|
||||
import Reader from '../screens/reader/ReaderScreen';
|
||||
import SourceScreen from '../screens/source/SourceScreen';
|
||||
import GlobalSearch from '../screens/browse/globalsearch/GlobalSearch';
|
||||
import BrowseSourceScreen from '../screens/BrowseSourceScreen/BrowseSourceScreen';
|
||||
import GlobalSearchScreen from '../screens/GlobalSearchScreen/GlobalSearchScreen';
|
||||
import Migration from '../screens/browse/migration/Migration';
|
||||
import SourceNovels from '../screens/browse/SourceNovels';
|
||||
import MigrateNovel from '../screens/browse/migration/MigrationNovels';
|
||||
@@ -28,35 +28,51 @@ import MigrateNovel from '../screens/browse/migration/MigrationNovels';
|
||||
import MalTopNovels from '../screens/browse/discover/MalTopNovels';
|
||||
import NewUpdateDialog from '../components/NewUpdateDialog';
|
||||
import BrowseSettings from '../screens/browse/BrowseSettings';
|
||||
import { useAppDispatch } from '@redux/hooks';
|
||||
import { updateLibraryAction } from '@redux/updates/updates.actions';
|
||||
import { useSettings } from '@hooks/reduxHooks';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
const MainNavigator = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
const { updateLibraryOnLaunch = false } = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(async () => {
|
||||
const timer = setTimeout(async () => {
|
||||
await SplashScreen.hideAsync();
|
||||
setBarColor(theme);
|
||||
}, 500);
|
||||
}, [theme]);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [theme.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (updateLibraryOnLaunch) {
|
||||
dispatch(updateLibraryAction());
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { isNewVersion, latestRelease } = useGithubUpdateChecker() || {};
|
||||
|
||||
return (
|
||||
<NavigationContainer
|
||||
theme={{ colors: { background: theme.colorPrimaryDark } }}
|
||||
>
|
||||
<NavigationContainer theme={{ colors: { background: theme.background } }}>
|
||||
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="BottomNavigator" component={BottomNavigator} />
|
||||
<Stack.Screen name="Novel" component={Novel} />
|
||||
<Stack.Screen name="Chapter" component={Reader} />
|
||||
<Stack.Screen name="MoreStack" component={MoreStack} />
|
||||
<Stack.Screen name="SourceScreen" component={SourceScreen} />
|
||||
<Stack.Screen name="SourceScreen" component={BrowseSourceScreen} />
|
||||
<Stack.Screen name="BrowseMal" component={MalTopNovels} />
|
||||
<Stack.Screen name="BrowseSettings" component={BrowseSettings} />
|
||||
<Stack.Screen name="GlobalSearch" component={GlobalSearch} />
|
||||
<Stack.Screen
|
||||
name="GlobalSearchScreen"
|
||||
component={GlobalSearchScreen}
|
||||
/>
|
||||
<Stack.Screen name="Migration" component={Migration} />
|
||||
<Stack.Screen name="SourceNovels" component={SourceNovels} />
|
||||
<Stack.Screen name="MigrateNovel" component={MigrateNovel} />
|
||||
|
||||
@@ -4,15 +4,17 @@ import { createStackNavigator } from '@react-navigation/stack';
|
||||
|
||||
// Screens
|
||||
import About from '../screens/more/About';
|
||||
import Settings from '../screens/setting/SettingsScreen';
|
||||
import TrackerSettings from '../screens/setting/SettingsTrackerScreen';
|
||||
import ReaderSettings from '../screens/setting/SettingsReaderScreen';
|
||||
import BackupSettings from '../screens/setting/SettingsBackupScreen';
|
||||
import AdvancedSettings from '../screens/setting/SettingsAdvancedScreen';
|
||||
import GeneralSettings from '../screens/setting/SettingsGeneralScreen';
|
||||
import Settings from '../screens/settings/SettingsScreen';
|
||||
import TrackerSettings from '../screens/settings/SettingsTrackerScreen';
|
||||
import ReaderSettings from '../screens/settings/SettingsReaderScreen/SettingsReaderScreen';
|
||||
import BackupSettings from '../screens/settings/SettingsBackupScreen';
|
||||
import AdvancedSettings from '../screens/settings/SettingsAdvancedScreen';
|
||||
import GeneralSettings from '../screens/settings/SettingsGeneralScreen';
|
||||
import DownloadQueue from '../screens/more/DownloadQueueScreen';
|
||||
import Downloads from '../screens/more/DownloadsScreen';
|
||||
import AppearanceSettings from '../screens/setting/SettingsAppearanceScreen';
|
||||
import AppearanceSettings from '../screens/settings/SettingsAppearanceScreen';
|
||||
import CategoriesScreen from '@screens/Categories/CategoriesScreen';
|
||||
import LibrarySettings from '@screens/settings/SettingsLibraryScreen/SettingsLibraryScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
@@ -27,6 +29,7 @@ const SettingsStack = () => (
|
||||
<Stack.Screen name="BackupSettings" component={BackupSettings} />
|
||||
<Stack.Screen name="AppearanceSettings" component={AppearanceSettings} />
|
||||
<Stack.Screen name="AdvancedSettings" component={AdvancedSettings} />
|
||||
<Stack.Screen name="LibrarySettings" component={LibrarySettings} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
|
||||
@@ -36,6 +39,7 @@ const MoreStack = () => (
|
||||
<Stack.Screen name="About" component={About} />
|
||||
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
|
||||
<Stack.Screen name="Downloads" component={Downloads} />
|
||||
<Stack.Screen name="Categories" component={CategoriesScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user