Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.9"
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,21 @@ module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
plugins: [
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
root: ['./src'],
|
||||
alias: {
|
||||
'@components': './src/components',
|
||||
'@hooks': './src/hooks',
|
||||
'@screens': './src/screens',
|
||||
'@strings': './strings',
|
||||
'@redux': './src/redux',
|
||||
},
|
||||
extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
+536
-391
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lnreader",
|
||||
"version": "1.1.9",
|
||||
"version": "1.1.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
@@ -16,19 +16,19 @@
|
||||
},
|
||||
"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",
|
||||
"buffer": "^6.0.3",
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"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 +37,10 @@
|
||||
"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",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.10.1",
|
||||
"react": "16.13.1",
|
||||
@@ -52,24 +51,23 @@
|
||||
"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-pager-view": "^5.4.9",
|
||||
"react-native-paper": "^4.10.1",
|
||||
"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"
|
||||
},
|
||||
@@ -80,10 +78,12 @@
|
||||
"@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",
|
||||
|
||||
@@ -5,7 +5,10 @@ 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, 'Dot')
|
||||
.replace('1st', 'First');
|
||||
|
||||
return name;
|
||||
};
|
||||
@@ -40,7 +43,9 @@ 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',
|
||||
});
|
||||
|
||||
@@ -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 [];
|
||||
`
|
||||
|
||||
@@ -2,12 +2,12 @@ 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 { ThemeType } from '../../theme/types';
|
||||
|
||||
interface AppbarProps {
|
||||
title: string;
|
||||
handleGoBack: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const Appbar: React.FC<AppbarProps> = ({ title, handleGoBack, theme }) => (
|
||||
|
||||
@@ -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,7 @@
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { Pressable, StyleSheet, Text, View, ViewStyle } from 'react-native';
|
||||
import React from 'react';
|
||||
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
export enum ButtonVariation {
|
||||
DEFAULT = 'default',
|
||||
@@ -14,31 +14,31 @@ interface Props {
|
||||
title: string;
|
||||
textColor?: string;
|
||||
onPress?: () => void;
|
||||
margin?: number;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
const Button: React.FC<Props> = ({
|
||||
variation = ButtonVariation.DEFAULT,
|
||||
title,
|
||||
textColor,
|
||||
margin = 0,
|
||||
onPress,
|
||||
theme,
|
||||
style,
|
||||
}) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.button,
|
||||
{ margin },
|
||||
variation === 'outlined'
|
||||
variation === ButtonVariation.OUTLINED
|
||||
? {
|
||||
...styles.outlined,
|
||||
borderColor: theme.textColorHint,
|
||||
}
|
||||
: variation !== 'clear' && {
|
||||
: variation !== ButtonVariation.CLEAR && {
|
||||
backgroundColor: theme.colorAccent,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
@@ -50,10 +50,10 @@ const Button: React.FC<Props> = ({
|
||||
style={{
|
||||
color: textColor
|
||||
? textColor
|
||||
: variation === 'outlined'
|
||||
: variation === ButtonVariation.OUTLINED
|
||||
? theme.colorAccent
|
||||
: variation === ButtonVariation.CLEAR
|
||||
? theme.colorAccent
|
||||
: variation === 'default'
|
||||
? theme.colorButtonText
|
||||
: theme.textColorPrimary,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
|
||||
@@ -4,7 +4,13 @@ import { Checkbox as PaperCheckbox } from 'react-native-paper';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const Checkbox = ({ label, status, onPress, disabled, theme }) => (
|
||||
export const Checkbox = ({
|
||||
label,
|
||||
status,
|
||||
onPress,
|
||||
disabled = false,
|
||||
theme,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressable}
|
||||
@@ -24,7 +30,7 @@ export const Checkbox = ({ label, status, onPress, disabled, theme }) => (
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text style={{ color: theme.textColorPrimary, marginLeft: 12 }}>
|
||||
<Text style={{ color: theme.textColorSecondary, marginLeft: 12 }}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
interface ChipProps {
|
||||
label: string;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const Chip: React.FC<ChipProps> = ({ label, theme }) => (
|
||||
|
||||
+30
-17
@@ -2,33 +2,46 @@ import React, { useState } from 'react';
|
||||
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { Modal, Portal, TextInput } from 'react-native-paper';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
const ColorPickerModal = ({
|
||||
interface ColorPickerModalProps {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
color: string;
|
||||
onSubmit: (val: string) => void;
|
||||
closeModal: () => void;
|
||||
theme: ThemeType;
|
||||
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');
|
||||
}
|
||||
@@ -69,7 +82,7 @@ const ColorPickerModal = ({
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
@@ -79,7 +92,7 @@ const ColorPickerModal = ({
|
||||
<Text style={[styles.modalTitle, { color: theme.textColorPrimary }]}>
|
||||
{title}
|
||||
</Text>
|
||||
{showAccentColors && (
|
||||
{showAccentColors ? (
|
||||
<FlatList
|
||||
contentContainerStyle={{ marginBottom: 8 }}
|
||||
data={accentColors}
|
||||
@@ -104,16 +117,16 @@ 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}
|
||||
@@ -121,7 +134,7 @@ const ColorPickerModal = ({
|
||||
theme={textInputTheme}
|
||||
underlineColor={theme.textColorHint}
|
||||
dense
|
||||
error={error}
|
||||
error={Boolean(error)}
|
||||
/>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</Modal>
|
||||
@@ -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,21 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
|
||||
interface ToggleButtonProps {
|
||||
icon: string;
|
||||
selected: boolean;
|
||||
theme: ThemeType;
|
||||
color?: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export const ToggleButton: React.FC<ToggleButtonProps> = ({
|
||||
icon,
|
||||
selected,
|
||||
theme,
|
||||
color,
|
||||
onPress,
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 4,
|
||||
marginHorizontal: 6,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 10,
|
||||
padding: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: selected ? theme.rippleColor : 'transparent',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
@@ -30,7 +46,14 @@ export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
|
||||
</View>
|
||||
);
|
||||
|
||||
export const ToggleColorButton = ({
|
||||
interface ToggleColorButtonProps {
|
||||
selected: boolean;
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export const ToggleColorButton: React.FC<ToggleColorButtonProps> = ({
|
||||
selected,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
@@ -56,9 +79,11 @@ export const ToggleColorButton = ({
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
{selected && (
|
||||
<MaterialCommunityIcons name="check" color={textColor} size={24} />
|
||||
)}
|
||||
<MaterialCommunityIcons
|
||||
name={selected ? 'check' : 'format-color-text'}
|
||||
color={textColor}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { Dialog, Portal } from 'react-native-paper';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { Button } from '@components/index';
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
|
||||
interface ConfirmationDialogProps {
|
||||
title: string;
|
||||
visible: boolean;
|
||||
theme: ThemeType;
|
||||
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: theme.colorPrimary }]}
|
||||
>
|
||||
<Dialog.Title style={[styles.title, { color: theme.textColorPrimary }]}>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<Button
|
||||
theme={theme}
|
||||
onPress={onDismiss}
|
||||
title={getString('common.cancel')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
/>
|
||||
<Button
|
||||
theme={theme}
|
||||
onPress={handleOnSubmit}
|
||||
title={getString('common.ok')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
style={styles.button}
|
||||
/>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
title: {
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
marginLeft: 4,
|
||||
},
|
||||
});
|
||||
@@ -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,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
interface EmptyViewProps {
|
||||
icon?: string;
|
||||
description: string;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const EmptyView: React.FC<EmptyViewProps> = ({ icon, description, theme }) => (
|
||||
|
||||
@@ -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 { ThemeType } from '../../theme/types';
|
||||
|
||||
interface ErrorScreenProps {
|
||||
error?: string;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Pressable, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
@@ -11,7 +11,7 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
padding?: number;
|
||||
onPress?: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ import React, { ReactNode } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
interface ListItemProps {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
icon?: string;
|
||||
onPress?: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const Section = ({ children }: { children: ReactNode }) => (
|
||||
@@ -21,7 +21,7 @@ const SubHeader = ({
|
||||
theme,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}) => (
|
||||
<PaperList.Subheader style={{ color: theme.colorAccent }}>
|
||||
{children}
|
||||
@@ -56,7 +56,7 @@ const Item: React.FC<ListItemProps> = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({ theme }: { theme: ThemeTypeV1 }) => (
|
||||
const Divider = ({ theme }: { theme: ThemeType }) => (
|
||||
<PaperDivider
|
||||
style={[styles.divider, { backgroundColor: theme.dividerColor }]}
|
||||
/>
|
||||
@@ -69,7 +69,7 @@ const InfoItem = ({
|
||||
}: {
|
||||
title: string;
|
||||
icon: string;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}) => (
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, StyleSheet } from 'react-native';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
interface Props {
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const LoadingMoreIndicator: React.FC<Props> = ({ theme }) => (
|
||||
<ActivityIndicator color={theme.colorAccent} style={styles.indicator} />
|
||||
);
|
||||
|
||||
export default LoadingMoreIndicator;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
indicator: {
|
||||
padding: 32,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,17 +1,14 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, ActivityIndicator } from 'react-native';
|
||||
|
||||
import { Container } from '..';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { ThemeType } 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: ThemeType }> = ({ theme }) => (
|
||||
<ActivityIndicator
|
||||
size={50}
|
||||
color={theme.colorAccent}
|
||||
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,97 @@
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, View, Text, Dimensions } from 'react-native';
|
||||
import { Portal, Modal } from 'react-native-paper';
|
||||
import * as Linking from 'expo-linking';
|
||||
import { useTheme } from '../hooks/reduxHooks';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import Button, { ButtonVariation } from './Button/Button';
|
||||
import { getString } from '@strings/translations';
|
||||
import Markdown from 'react-native-markdown-display';
|
||||
|
||||
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: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<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: theme.dividerColor,
|
||||
marginVertical: 16,
|
||||
},
|
||||
code_inline: {
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{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',
|
||||
},
|
||||
});
|
||||
@@ -14,7 +14,6 @@ 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';
|
||||
|
||||
const NovelCover = ({
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -43,6 +43,7 @@ export const Searchbar = ({
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
color: theme.textColorPrimary,
|
||||
marginLeft: 4,
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useRef } from 'react';
|
||||
import { Pressable, StyleSheet, View, TextInput } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { IconButtonV2 } from '..';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import IconButtonV2 from '../IconButtonV2/IconButtonV2';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
interface RightIcon {
|
||||
iconName: string;
|
||||
@@ -21,7 +21,7 @@ interface SearcbarProps {
|
||||
handleBackAction?: () => void;
|
||||
clearSearchbar: () => void;
|
||||
onLeftIconPress?: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const Searchbar: React.FC<SearcbarProps> = ({
|
||||
|
||||
@@ -2,14 +2,14 @@ import React from 'react';
|
||||
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import { Switch } from 'react-native-paper';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
interface SwitchItemProps {
|
||||
value: boolean;
|
||||
label: string;
|
||||
description?: string;
|
||||
onPress: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const SwitchItem: React.FC<SwitchItemProps> = ({
|
||||
@@ -52,10 +52,10 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
labelContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
textAlignVertical: 'center',
|
||||
},
|
||||
switch: {
|
||||
marginLeft: 8,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -10,3 +10,5 @@ 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';
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
/* 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';
|
||||
|
||||
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 +26,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 +37,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 +72,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 +81,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 +105,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 +136,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 +154,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 +171,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 +188,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 +205,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 +222,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 +235,9 @@ export const isChapterDownloaded = async chapterId => {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
(txObj, error) => {
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
@@ -226,55 +247,148 @@ export const isChapterDownloaded = async chapterId => {
|
||||
const downloadChapterQuery =
|
||||
'INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)';
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterId,
|
||||
sourceId: number,
|
||||
novelUrl: string,
|
||||
novelId: number,
|
||||
chapterUrl: string,
|
||||
chapterId: number,
|
||||
) => {
|
||||
const source = sourceManager(sourceId);
|
||||
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
|
||||
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, chapter.chapterText],
|
||||
(txObj, res) => {
|
||||
[chapterId, chapter.chapterName, imagedChapterText],
|
||||
(_txObj, _res) => {
|
||||
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
|
||||
},
|
||||
(txObj, error) => {
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteChapter = async chapterId => {
|
||||
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=?';
|
||||
|
||||
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]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +402,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 +421,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 +438,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 +458,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 +483,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),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as SQLite from 'expo-sqlite';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getLibraryQuery = (sort, filter) => `
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded, H.lastReadAt
|
||||
FROM novels
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
|
||||
@@ -18,6 +18,14 @@ const getLibraryQuery = (sort, filter) => `
|
||||
GROUP BY chapters.novelId
|
||||
) AS D
|
||||
ON novels.novelId = D.novelId
|
||||
LEFT JOIN (
|
||||
SELECT history.historyNovelId as novelId, historyTimeRead AS lastReadAt
|
||||
FROM history
|
||||
GROUP BY history.historyNovelId
|
||||
HAVING history.historyTimeRead = MAX(history.historyTimeRead)
|
||||
ORDER BY history.historyTimeRead DESC
|
||||
) AS H
|
||||
ON novels.novelId = H.novelId
|
||||
WHERE novels.followed = 1 ${filter ? 'AND ' + filter : ''}
|
||||
${sort ? 'ORDER BY ' + sort : ''}
|
||||
`;
|
||||
@@ -29,7 +37,7 @@ export const getLibrary = (sort, filter) => {
|
||||
getLibraryQuery(sort, filter),
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
// console.log(_array);
|
||||
// console.log(JSON.stringify(_array, null, 2));
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
@@ -42,7 +50,7 @@ export const getLibrary = (sort, filter) => {
|
||||
|
||||
const searchLibraryQuery = (searchText, sort, filter) =>
|
||||
`
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded, H.lastReadAt
|
||||
FROM novels
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
|
||||
@@ -58,6 +66,14 @@ const searchLibraryQuery = (searchText, sort, filter) =>
|
||||
GROUP BY chapters.novelId
|
||||
) AS D
|
||||
ON novels.novelId = D.novelId
|
||||
LEFT JOIN (
|
||||
SELECT history.historyNovelId as novelId, historyTimeRead AS lastReadAt
|
||||
FROM history
|
||||
GROUP BY history.historyNovelId
|
||||
HAVING history.historyTimeRead = MAX(history.historyTimeRead)
|
||||
ORDER BY history.historyTimeRead DESC
|
||||
) AS H
|
||||
ON novels.novelId = H.novelId
|
||||
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%' ${
|
||||
filter ? 'AND ' + filter : ''
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
`;
|
||||
@@ -7,3 +7,4 @@ 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';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { darkTheme } from '../theme/v1/theme/theme';
|
||||
import { darkTheme } from '../theme/theme';
|
||||
|
||||
const useTheme = () => {
|
||||
const theme = useSelector(state => state.settingsReducer.theme);
|
||||
@@ -7,12 +7,6 @@ const useTheme = () => {
|
||||
return theme || darkTheme;
|
||||
};
|
||||
|
||||
const useReaderSettings = () => {
|
||||
const readerSettings = useSelector(state => state.settingsReducer.reader);
|
||||
|
||||
return readerSettings;
|
||||
};
|
||||
|
||||
const useSettings = () => {
|
||||
const settings = useSelector(state => state.settingsReducer);
|
||||
|
||||
@@ -118,7 +112,6 @@ const useLibraryFilters = () => {
|
||||
|
||||
export {
|
||||
useTheme,
|
||||
useReaderSettings,
|
||||
useLibrary,
|
||||
useSettings,
|
||||
usePreferences,
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect } from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useReaderSettingsV1, useSettingsV1, useThemeV1 } from '../redux/hooks';
|
||||
import { useReaderSettings, useSettingsV1, useTheme } from '../redux/hooks';
|
||||
import {
|
||||
changeNavigationBarColor,
|
||||
hideNavigationBar,
|
||||
@@ -12,12 +12,12 @@ import { readerBackground } from '../screens/reader/utils/readerStyles';
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
import { useDeviceOrientation } from '../hooks';
|
||||
import useDeviceOrientation from './useDeviceOrientation';
|
||||
import { DeviceOrientation } from '../hooks/useDeviceOrientation';
|
||||
import { useAppearanceSettings } from '../redux/hooks';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useSearch = () => {
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const useSearch = (defaultSearchText?: string) => {
|
||||
const [searchText, setSearchText] = useState<string>(defaultSearchText || '');
|
||||
const clearSearchbar = useCallback(() => setSearchText(''), []);
|
||||
|
||||
return { searchText, setSearchText, clearSearchbar };
|
||||
|
||||
+12
-5
@@ -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';
|
||||
@@ -35,10 +35,14 @@ const MainNavigator = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(async () => {
|
||||
const timer = setTimeout(async () => {
|
||||
await SplashScreen.hideAsync();
|
||||
setBarColor(theme);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
const { isNewVersion, latestRelease } = useGithubUpdateChecker() || {};
|
||||
@@ -53,10 +57,13 @@ const MainNavigator = () => {
|
||||
<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,15 @@ 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';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export const resumeDownloads = chapters => async dispatch => {
|
||||
await downloadChapter(
|
||||
chapters[i].sourceId,
|
||||
chapters[i].novelUrl,
|
||||
chapters[i].novelId,
|
||||
chapters[i].chapterUrl,
|
||||
chapters[i].chapterId,
|
||||
);
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import type { RootState, AppDispatch } from '../store';
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
||||
export const useTheme = () =>
|
||||
useAppSelector(
|
||||
(state: RootState) => state.settingsReducerV2.appearance.theme,
|
||||
);
|
||||
|
||||
export const useThemeV1 = () =>
|
||||
useAppSelector(
|
||||
(state: RootState) => state.settingsReducer.theme,
|
||||
) as ThemeTypeV1;
|
||||
) as ThemeType;
|
||||
|
||||
export const useSettingsV1 = () =>
|
||||
useAppSelector((state: RootState) => state.settingsReducer);
|
||||
@@ -28,9 +23,6 @@ export const useAppearanceSettings = () =>
|
||||
useAppSelector((state: RootState) => state.settingsReducerV2.appearance);
|
||||
|
||||
export const useReaderSettings = () =>
|
||||
useAppSelector((state: RootState) => state.settingsReducerV2.reader);
|
||||
|
||||
export const useReaderSettingsV1 = () =>
|
||||
useAppSelector((state: RootState) => state.settingsReducer.reader);
|
||||
|
||||
export const useLibrarySettings = () =>
|
||||
|
||||
@@ -269,8 +269,18 @@ export const markChapterUnreadAction =
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} sourceId
|
||||
* @param {string} novelUrl
|
||||
* @param {number} novelId
|
||||
* @param {string} chapterUrl
|
||||
* @param {string} chapterName
|
||||
* @param {number} chapterId
|
||||
* @returns
|
||||
*/
|
||||
export const downloadChapterAction =
|
||||
(sourceId, novelUrl, chapterUrl, chapterName, chapterId) =>
|
||||
(sourceId, novelUrl, novelId, chapterUrl, chapterName, chapterId) =>
|
||||
async dispatch => {
|
||||
// dispatch({
|
||||
// type: CHAPTER_DOWNLOADING,
|
||||
@@ -281,7 +291,7 @@ export const downloadChapterAction =
|
||||
payload: [{ chapterId, chapterName }],
|
||||
});
|
||||
|
||||
await downloadChapter(sourceId, novelUrl, chapterUrl, chapterId);
|
||||
await downloadChapter(sourceId, novelUrl, novelId, chapterUrl, chapterId);
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
@@ -291,6 +301,13 @@ export const downloadChapterAction =
|
||||
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} sourceId
|
||||
* @param {string} novelUrl
|
||||
* @param {import("./../../database/types").ChapterItem[]} chaps
|
||||
* @returns
|
||||
*/
|
||||
export const downloadAllChaptersAction =
|
||||
(sourceId, novelUrl, chaps) => async (dispatch, getState) => {
|
||||
try {
|
||||
@@ -356,6 +373,7 @@ export const downloadAllChaptersAction =
|
||||
await downloadChapter(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapters[i].novelId,
|
||||
chapters[i].chapterUrl,
|
||||
chapters[i].chapterId,
|
||||
);
|
||||
@@ -400,8 +418,8 @@ export const downloadAllChaptersAction =
|
||||
};
|
||||
|
||||
export const deleteChapterAction =
|
||||
(chapterId, chapterName) => async dispatch => {
|
||||
await deleteChapter(chapterId);
|
||||
(sourceId, novelId, chapterId, chapterName) => async dispatch => {
|
||||
await deleteChapter(sourceId, novelId, chapterId);
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DELETED,
|
||||
@@ -411,18 +429,26 @@ export const deleteChapterAction =
|
||||
showToast(`Deleted ${chapterName}`);
|
||||
};
|
||||
|
||||
export const deleteAllChaptersAction = chapters => async dispatch => {
|
||||
await chapters.map(chapter => {
|
||||
deleteChapter(chapter.chapterId);
|
||||
/**
|
||||
*
|
||||
* @param {number} sourceId
|
||||
* @param {import("../../database/types").ChapterItem[]} chapters
|
||||
* @returns
|
||||
*/
|
||||
export const deleteAllChaptersAction =
|
||||
(sourceId, chapters) => async dispatch => {
|
||||
await Promise.all(
|
||||
chapters.map(async chapter => {
|
||||
await deleteChapter(sourceId, chapter.novelId, chapter.chapterId);
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DELETED,
|
||||
payload: chapter.chapterId,
|
||||
});
|
||||
});
|
||||
|
||||
showToast('Chapters deleted');
|
||||
};
|
||||
dispatch({
|
||||
type: CHAPTER_DELETED,
|
||||
payload: chapter.chapterId,
|
||||
});
|
||||
}),
|
||||
);
|
||||
showToast('Chapters deleted');
|
||||
};
|
||||
|
||||
export const updateNovelAction =
|
||||
(sourceId, novelUrl, novelId, sort, filter) => async (dispatch, getState) => {
|
||||
|
||||
@@ -23,7 +23,9 @@ import {
|
||||
yotsubaTheme,
|
||||
takoLightTheme,
|
||||
tealTheme,
|
||||
} from '../../theme/v1/theme/theme';
|
||||
sapphireDuskTheme,
|
||||
chocolateStrawberriesTheme,
|
||||
} from '../../theme/theme.ts';
|
||||
|
||||
const themes = {
|
||||
1: lightTheme,
|
||||
@@ -40,6 +42,8 @@ const themes = {
|
||||
12: yotsubaTheme,
|
||||
13: takoLightTheme,
|
||||
14: tealTheme,
|
||||
15: sapphireDuskTheme,
|
||||
16: chocolateStrawberriesTheme,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -51,12 +55,13 @@ const themes = {
|
||||
* 3 -> No title
|
||||
*/
|
||||
|
||||
const initialState = {
|
||||
export const initialState = {
|
||||
/**
|
||||
* General settings
|
||||
*/
|
||||
|
||||
incognitoMode: false,
|
||||
disableHapticFeedback: false,
|
||||
|
||||
/**
|
||||
* Appearence settings
|
||||
@@ -107,18 +112,22 @@ const initialState = {
|
||||
*/
|
||||
|
||||
fullScreenMode: true,
|
||||
swipeGestures: true,
|
||||
swipeGestures: false,
|
||||
showScrollPercentage: true,
|
||||
useWebViewForChapter: false,
|
||||
wvUseNewSwipes: false,
|
||||
wvShowSwipeMargins: true,
|
||||
wvUseVolumeButtons: false,
|
||||
showBatteryAndTime: false,
|
||||
autoScroll: false,
|
||||
autoScrollInterval: 10,
|
||||
textSelectable: false,
|
||||
autoScrollOffset: null,
|
||||
verticalSeekbar: true,
|
||||
removeExtraParagraphSpacing: false,
|
||||
|
||||
reader: {
|
||||
theme: 1,
|
||||
textColor: 'rgba(255,255,255,0.7)',
|
||||
theme: '#292832',
|
||||
textColor: '#CCCCCC',
|
||||
textSize: 16,
|
||||
textAlign: 'left',
|
||||
padding: 5,
|
||||
|
||||
@@ -1,69 +1,20 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { defaultDarkTheme } from '../../theme/dark';
|
||||
import { BrowseSettingsMap } from './types';
|
||||
|
||||
export enum DisplayModes {
|
||||
Compact = 'compact',
|
||||
Comfortable = 'comfortable',
|
||||
NoTitle = 'no title',
|
||||
List = 'list',
|
||||
export interface ReaderTheme {
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
}
|
||||
|
||||
export enum TextAlignments {
|
||||
Left = 'left',
|
||||
Center = 'center',
|
||||
Justify = 'justfy',
|
||||
Right = 'right',
|
||||
}
|
||||
|
||||
// interface ReaderTheme {
|
||||
// backgroundColor: string;
|
||||
// textColor: string;
|
||||
// }
|
||||
|
||||
interface SettingsState {
|
||||
appearance: {
|
||||
theme: ThemeType;
|
||||
displayMode: DisplayModes;
|
||||
novelsPerRowPotrait: number;
|
||||
novelsPerRowLandscape: number;
|
||||
showLabelsInNav: boolean;
|
||||
showHistoryTab: boolean;
|
||||
showUpdatesTab: boolean;
|
||||
};
|
||||
appearance: {};
|
||||
reader: {
|
||||
fontFamily: string | null;
|
||||
fontSize: number;
|
||||
lineHeight: number;
|
||||
textAlign: TextAlignments;
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
paddingHorizontal: number;
|
||||
customCSS: string | null;
|
||||
fullScreenMode: boolean;
|
||||
useWebViewReader: boolean;
|
||||
showProgressPercentage: boolean;
|
||||
useSwipeGestures: boolean;
|
||||
};
|
||||
library: {
|
||||
filters: string[];
|
||||
sort: string;
|
||||
showDownloadsBadge: boolean;
|
||||
showUnreadBadge: boolean;
|
||||
updateLibraryOnLaunch: boolean;
|
||||
};
|
||||
updates: {
|
||||
lastUpdateTime: Date | null;
|
||||
onlyUpdateOngoingNovels: boolean;
|
||||
updateNovelMetadata: boolean;
|
||||
showLastUpdateTime: boolean;
|
||||
};
|
||||
app: {
|
||||
incognitoMode: boolean;
|
||||
downloadOnlyMode: boolean;
|
||||
customThemes: ReaderTheme[];
|
||||
};
|
||||
library: {};
|
||||
updates: {};
|
||||
app: {};
|
||||
browse: {
|
||||
onlyShowPinnedSources: boolean;
|
||||
showMyAnimeList: boolean;
|
||||
@@ -72,46 +23,13 @@ interface SettingsState {
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
appearance: {
|
||||
theme: defaultDarkTheme,
|
||||
displayMode: DisplayModes.Compact,
|
||||
novelsPerRowPotrait: 3,
|
||||
novelsPerRowLandscape: 6,
|
||||
showLabelsInNav: true,
|
||||
showHistoryTab: true,
|
||||
showUpdatesTab: true,
|
||||
},
|
||||
appearance: {},
|
||||
reader: {
|
||||
fontFamily: '',
|
||||
fontSize: 16,
|
||||
lineHeight: 1.5,
|
||||
textAlign: TextAlignments.Left,
|
||||
backgroundColor: '#000000',
|
||||
textColor: 'rgba(255,255,255,0.7)',
|
||||
paddingHorizontal: 5,
|
||||
customCSS: null,
|
||||
fullScreenMode: true,
|
||||
useWebViewReader: false,
|
||||
showProgressPercentage: false,
|
||||
useSwipeGestures: false,
|
||||
},
|
||||
library: {
|
||||
filters: [],
|
||||
sort: 'novelId DESC',
|
||||
showDownloadsBadge: true,
|
||||
showUnreadBadge: true,
|
||||
updateLibraryOnLaunch: false,
|
||||
},
|
||||
updates: {
|
||||
lastUpdateTime: null,
|
||||
onlyUpdateOngoingNovels: false,
|
||||
updateNovelMetadata: false,
|
||||
showLastUpdateTime: false,
|
||||
},
|
||||
app: {
|
||||
incognitoMode: false,
|
||||
downloadOnlyMode: false,
|
||||
customThemes: [],
|
||||
},
|
||||
library: {},
|
||||
updates: {},
|
||||
app: {},
|
||||
browse: {
|
||||
onlyShowPinnedSources: false,
|
||||
showMyAnimeList: true,
|
||||
@@ -133,9 +51,35 @@ export const settingsSlice = createSlice({
|
||||
|
||||
state.browse[action.payload.key] = action.payload.value;
|
||||
},
|
||||
saveCustomReaderTheme: (
|
||||
state,
|
||||
action: PayloadAction<{ theme: ReaderTheme }>,
|
||||
) => {
|
||||
state.reader.customThemes = [
|
||||
...(state.reader.customThemes || []),
|
||||
action.payload.theme,
|
||||
];
|
||||
},
|
||||
deleteCustomReaderTheme: (
|
||||
state,
|
||||
action: PayloadAction<{ theme: ReaderTheme }>,
|
||||
) => {
|
||||
const customTheme = action.payload.theme;
|
||||
state.reader.customThemes = state.reader.customThemes.filter(
|
||||
theme =>
|
||||
!(
|
||||
theme.backgroundColor === customTheme.backgroundColor &&
|
||||
theme.textColor === customTheme.textColor
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setBrowseSettings } = settingsSlice.actions;
|
||||
export const {
|
||||
setBrowseSettings,
|
||||
saveCustomReaderTheme,
|
||||
deleteCustomReaderTheme,
|
||||
} = settingsSlice.actions;
|
||||
|
||||
export default settingsSlice.reducer;
|
||||
|
||||
@@ -1,35 +1,3 @@
|
||||
// export interface SettingsMap {
|
||||
// 'app.downloadOnlyMode': 'string';
|
||||
// 'app.incognitoMode': 'string';
|
||||
// 'appearance.displayMode': 'string';
|
||||
// 'appearance.novelsPerRowLandscape': 'string';
|
||||
// 'appearance.novelsPerRowPotrait': 'string';
|
||||
// 'appearance.showHistoryTab': 'string';
|
||||
// 'appearance.showLabelsInNav': 'string';
|
||||
// 'appearance.showUpdatesTab': 'string';
|
||||
// 'appearance.theme': 'string';
|
||||
// 'library.showDownloadsBadge': 'string';
|
||||
// 'library.showUnreadBadge': 'string';
|
||||
// 'library.sort': 'string';
|
||||
// 'library.updateLibraryOnLaunch': 'string';
|
||||
// 'reader.backgroundColor': 'string';
|
||||
// 'reader.customCSS': 'string';
|
||||
// 'reader.fontFamily': 'string';
|
||||
// 'reader.fontSize': 'string';
|
||||
// 'reader.fullScreenMode': 'string';
|
||||
// 'reader.lineHeight': 'string';
|
||||
// 'reader.paddingHorizontal': 'string';
|
||||
// 'reader.showProgressPercentage': 'string';
|
||||
// 'reader.textAlign': 'string';
|
||||
// 'reader.textColor': 'string';
|
||||
// 'reader.useSwipeGestures': 'string';
|
||||
// 'reader.useWebViewReader': 'string';
|
||||
// 'updates.lastUpdateTime': 'string';
|
||||
// 'updates.onlyUpdateOngoingNovels': 'string';
|
||||
// 'updates.showLastUpdateTime': 'string';
|
||||
// 'updates.updateNovelMetadata': 'string';
|
||||
// }
|
||||
|
||||
export interface BrowseSettingsMap {
|
||||
onlyShowPinnedSources: 'string';
|
||||
searchAllSources: 'string';
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import {
|
||||
ENABLE_DISCOVER,
|
||||
FILTER_LANGUAGE,
|
||||
GET_SOURCES,
|
||||
PIN_SOURCES,
|
||||
SEARCH_SOURCES,
|
||||
} from './source.types';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import sources from '../../sources/sources.json';
|
||||
|
||||
export const getSourcesAction = () => async dispatch => {
|
||||
try {
|
||||
/**
|
||||
* Sort sources aplhabetically
|
||||
*/
|
||||
let sortedSources = sources.sort((a, b) =>
|
||||
a.sourceName.localeCompare(b.sourceName),
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: GET_SOURCES,
|
||||
payload: sortedSources,
|
||||
});
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const searchSourcesAction = searchText => async dispatch => {
|
||||
const res = searchText.toLowerCase();
|
||||
|
||||
dispatch({
|
||||
type: SEARCH_SOURCES,
|
||||
payload: res,
|
||||
});
|
||||
};
|
||||
|
||||
export const pinSourceAction = sourceId => async dispatch => {
|
||||
dispatch({
|
||||
type: PIN_SOURCES,
|
||||
payload: sourceId,
|
||||
});
|
||||
};
|
||||
|
||||
export const filterLanguage = language => async dispatch => {
|
||||
dispatch({
|
||||
type: FILTER_LANGUAGE,
|
||||
payload: language,
|
||||
});
|
||||
};
|
||||
|
||||
export const enableDiscover = key => async dispatch => {
|
||||
dispatch({
|
||||
type: ENABLE_DISCOVER,
|
||||
payload: key,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,182 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
|
||||
import { FAB, Portal } from 'react-native-paper';
|
||||
import Bottomsheet from 'rn-sliding-up-panel';
|
||||
import {
|
||||
Container,
|
||||
LoadingScreenV2,
|
||||
LoadingMoreIndicator,
|
||||
ErrorScreenV2,
|
||||
SearchbarV2,
|
||||
} from '@components/index';
|
||||
import NovelList from '@components/NovelList';
|
||||
import NovelCover from '@components/NovelCover';
|
||||
import FilterBottomSheet from './components/BottomSheet';
|
||||
|
||||
import { useSearch } from '../../hooks';
|
||||
import { useTheme } from '../../redux/hooks';
|
||||
import { useBrowseSource, useSearchSource } from './useBrowseSource';
|
||||
import { useLibrary } from '@hooks/reduxHooks';
|
||||
|
||||
import { LibraryNovelInfo } from '../../database/types';
|
||||
import { SourceNovelItem } from '../../sources/types';
|
||||
import { getString } from '@strings/translations';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
interface BrowseSourceScreenProps {
|
||||
route: {
|
||||
params: {
|
||||
sourceId: number;
|
||||
sourceName: string;
|
||||
url: string;
|
||||
showLatestNovels?: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
const theme = useTheme();
|
||||
const { navigate, goBack } = useNavigation();
|
||||
|
||||
const {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url: sourceUrl,
|
||||
showLatestNovels,
|
||||
} = route.params;
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
novels,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
error,
|
||||
filterValues,
|
||||
setFilters,
|
||||
clearFilters,
|
||||
} = useBrowseSource(sourceId, showLatestNovels);
|
||||
|
||||
const {
|
||||
isSearching,
|
||||
searchResults,
|
||||
searchSource,
|
||||
clearSearchResults,
|
||||
searchError,
|
||||
} = useSearchSource(sourceId);
|
||||
|
||||
const novelList = searchResults.length > 0 ? searchResults : novels;
|
||||
const errorMessage = error || searchError;
|
||||
|
||||
const { searchText, setSearchText, clearSearchbar } = useSearch();
|
||||
const onChangeText = (text: string) => setSearchText(text);
|
||||
const onSubmitEditing = () => searchSource(searchText);
|
||||
const handleClearSearchbar = () => {
|
||||
clearSearchbar();
|
||||
clearSearchResults();
|
||||
};
|
||||
|
||||
const handleOpenWebView = async () => {
|
||||
WebBrowser.openBrowserAsync(sourceUrl);
|
||||
};
|
||||
|
||||
const library = useLibrary();
|
||||
|
||||
const novelInLibrary = (novelUrl: string) =>
|
||||
library?.some(
|
||||
(novel: LibraryNovelInfo) =>
|
||||
novel.novelUrl === novelUrl && novel.sourceId === sourceId,
|
||||
);
|
||||
|
||||
const navigateToNovel = useCallback(
|
||||
(item: SourceNovelItem) =>
|
||||
navigate(
|
||||
'Novel' as never,
|
||||
{
|
||||
novelName: item.novelName,
|
||||
novelCover: item.novelCover,
|
||||
novelUrl: item.novelUrl,
|
||||
sourceId,
|
||||
} as never,
|
||||
),
|
||||
[sourceId],
|
||||
);
|
||||
|
||||
const filterSheetRef = useRef<Bottomsheet | null>(null);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SearchbarV2
|
||||
searchText={searchText}
|
||||
leftIcon="magnify"
|
||||
placeholder={`${getString('common.search')} ${sourceName}`}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
clearSearchbar={handleClearSearchbar}
|
||||
handleBackAction={goBack}
|
||||
rightIcons={[{ iconName: 'earth', onPress: handleOpenWebView }]}
|
||||
theme={theme}
|
||||
/>
|
||||
{isLoading || isSearching ? (
|
||||
<LoadingScreenV2 theme={theme} />
|
||||
) : errorMessage || novelList.length === 0 ? (
|
||||
<ErrorScreenV2
|
||||
error={errorMessage || getString('sourceScreen.noResultsFound')}
|
||||
theme={theme}
|
||||
/>
|
||||
) : (
|
||||
<NovelList
|
||||
data={novelList}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
theme={theme}
|
||||
libraryStatus={novelInLibrary(item.novelUrl)}
|
||||
onPress={() => navigateToNovel(item)}
|
||||
/>
|
||||
)}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage && !searchText) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}}
|
||||
ListFooterComponent={
|
||||
hasNextPage && !searchText && <LoadingMoreIndicator theme={theme} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!showLatestNovels && filterValues?.length ? (
|
||||
<>
|
||||
<FAB
|
||||
icon={'filter-variant'}
|
||||
style={[styles.filterFab, { backgroundColor: theme.colorAccent }]}
|
||||
label={'Filter'}
|
||||
uppercase={false}
|
||||
color={theme.colorButtonText}
|
||||
onPress={() => filterSheetRef?.current?.show()}
|
||||
/>
|
||||
<Portal>
|
||||
<FilterBottomSheet
|
||||
filterSheetRef={filterSheetRef}
|
||||
filtersValues={filterValues}
|
||||
setFilters={setFilters}
|
||||
clearFilters={clearFilters}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
) : null}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowseSourceScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
filterFab: {
|
||||
position: 'absolute',
|
||||
margin: 16,
|
||||
right: 0,
|
||||
bottom: 16,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
import React, { LegacyRef, useState } from 'react';
|
||||
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import Bottomsheet from 'rn-sliding-up-panel';
|
||||
|
||||
import { useTheme } from '@redux/hooks';
|
||||
import {
|
||||
FilterInputs,
|
||||
SelectedFilter,
|
||||
SourceFilter,
|
||||
} from '../../../sources/types/filterTypes';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import { Button, IconButtonV2 } from '@components/index';
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
import { coverPlaceholderColor } from '../../../theme/colors';
|
||||
import { Checkbox } from '@components/Checkbox/Checkbox';
|
||||
import { defaultTo } from 'lodash';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
|
||||
const insertOrRemoveIntoArray = (array: string[], val: string): string[] =>
|
||||
array.indexOf(val) > -1 ? array.filter(ele => ele !== val) : [...array, val];
|
||||
|
||||
interface FilterItemProps {
|
||||
filter: SourceFilter;
|
||||
selectedFilters: SelectedFilter | undefined;
|
||||
setSelectedFilters: React.Dispatch<
|
||||
React.SetStateAction<SelectedFilter | undefined>
|
||||
>;
|
||||
}
|
||||
|
||||
const FilterItem: React.FC<FilterItemProps> = ({
|
||||
filter,
|
||||
selectedFilters,
|
||||
setSelectedFilters,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { value: isVisible, toggle: toggleCard } = useBoolean();
|
||||
|
||||
if (filter.inputType === FilterInputs.Picker) {
|
||||
return (
|
||||
<View style={styles.pickerContainer}>
|
||||
<Text style={[styles.pickerLabel, { color: theme.textColorSecondary }]}>
|
||||
{filter.label}
|
||||
</Text>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
mode="dropdown"
|
||||
selectedValue={selectedFilters?.[filter.key] as string}
|
||||
dropdownIconRippleColor={theme.colorAccent}
|
||||
onValueChange={(itemValue: string) =>
|
||||
setSelectedFilters(prevFilters => ({
|
||||
...prevFilters,
|
||||
[filter.key]: itemValue,
|
||||
}))
|
||||
}
|
||||
placeholder={filter.label}
|
||||
>
|
||||
{filter.values.map(val => {
|
||||
return <Picker.Item label={val.label} value={val.value} />;
|
||||
})}
|
||||
</Picker>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.inputType === FilterInputs.Checkbox) {
|
||||
const isChecked = (val: string) =>
|
||||
(selectedFilters?.[filter.key] as string[])?.includes(val);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Pressable
|
||||
style={styles.checkboxHeader}
|
||||
onPress={toggleCard}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Text
|
||||
style={[styles.filterLabel, { color: theme.textColorSecondary }]}
|
||||
>
|
||||
{filter.label}
|
||||
</Text>
|
||||
<IconButtonV2
|
||||
name={isVisible ? 'chevron-up' : 'chevron-down'}
|
||||
onPress={toggleCard}
|
||||
theme={theme}
|
||||
/>
|
||||
</Pressable>
|
||||
{isVisible
|
||||
? filter.values.map(val => {
|
||||
return (
|
||||
<Checkbox
|
||||
label={val.label}
|
||||
theme={theme}
|
||||
status={isChecked(val.value)}
|
||||
onPress={() =>
|
||||
setSelectedFilters(prevFilters => ({
|
||||
...prevFilters,
|
||||
[filter.key]: insertOrRemoveIntoArray(
|
||||
defaultTo(prevFilters?.[filter.key] as string[], []),
|
||||
val.value,
|
||||
),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
interface BottomSheetProps {
|
||||
filtersValues: SourceFilter[] | undefined;
|
||||
filterSheetRef: LegacyRef<Bottomsheet> | null;
|
||||
setFilters: React.Dispatch<React.SetStateAction<SelectedFilter | undefined>>;
|
||||
clearFilters: () => void;
|
||||
}
|
||||
|
||||
const BottomSheet: React.FC<BottomSheetProps> = ({
|
||||
filterSheetRef,
|
||||
filtersValues,
|
||||
clearFilters,
|
||||
setFilters,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
const [selectedFilters, setSelectedFilters] = useState<
|
||||
SelectedFilter | undefined
|
||||
>();
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={filterSheetRef}
|
||||
draggableRange={{ top: 600, bottom: 0 }}
|
||||
snappingPoints={[0, 400, 600]}
|
||||
showBackdrop={true}
|
||||
backdropOpacity={0.3}
|
||||
height={600}
|
||||
>
|
||||
<View style={[styles.container, { backgroundColor: theme.colorPrimary }]}>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
title={'Reset'}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
onPress={() => {
|
||||
setSelectedFilters(undefined);
|
||||
clearFilters();
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
<Button
|
||||
title={'Filter'}
|
||||
textColor={theme.colorButtonText}
|
||||
onPress={() => {
|
||||
setFilters(selectedFilters);
|
||||
filterSheetRef?.current?.hide();
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<FlatList
|
||||
contentContainerStyle={styles.filterContainer}
|
||||
data={filtersValues}
|
||||
keyExtractor={item => item.key}
|
||||
renderItem={({ item }: { item: SourceFilter }) => (
|
||||
<FilterItem
|
||||
filter={item}
|
||||
selectedFilters={selectedFilters}
|
||||
setSelectedFilters={setSelectedFilters}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomSheet;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
borderTopRightRadius: 8,
|
||||
borderTopLeftRadius: 8,
|
||||
},
|
||||
buttonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomColor: coverPlaceholderColor,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
filterContainer: {
|
||||
paddingVertical: 16,
|
||||
},
|
||||
filterLabel: {
|
||||
paddingHorizontal: 16,
|
||||
marginVertical: 16,
|
||||
},
|
||||
pickerLabel: {
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
pickerContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
picker: {
|
||||
width: 200,
|
||||
},
|
||||
checkboxHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingRight: 16,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { SelectedFilter, SourceFilter } from 'src/sources/types/filterTypes';
|
||||
import { sourceManager } from '../../sources/sourceManager';
|
||||
import { SourceNovelItem } from '../../sources/types';
|
||||
|
||||
export const useBrowseSource = (
|
||||
sourceId: number,
|
||||
showLatestNovels?: boolean,
|
||||
) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [novels, setNovels] = useState<SourceNovelItem[]>([]);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filterValues, setFilterValues] = useState<SourceFilter[]>();
|
||||
const [selectedFilters, setSelectedFilters] = useState<
|
||||
SelectedFilter | undefined
|
||||
>();
|
||||
|
||||
const isScreenMounted = useRef(true);
|
||||
|
||||
const hasNextPage = currentPage <= totalPages;
|
||||
|
||||
const fetchNovels = useCallback(
|
||||
async (page: number, filters?: SelectedFilter) => {
|
||||
if (isScreenMounted.current === true) {
|
||||
try {
|
||||
const source = sourceManager(sourceId);
|
||||
const res = await source.popularNovels(page, {
|
||||
showLatestNovels,
|
||||
filters,
|
||||
});
|
||||
setNovels(prevState =>
|
||||
page === 1 ? res.novels : [...prevState, ...res.novels],
|
||||
);
|
||||
setTotalPages(res.totalPages);
|
||||
setFilterValues(source.filters);
|
||||
} catch (err: unknown) {
|
||||
setError(`${err}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[sourceId],
|
||||
);
|
||||
|
||||
const fetchNextPage = () => {
|
||||
hasNextPage && setCurrentPage(prevState => prevState + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchNovels(currentPage, selectedFilters);
|
||||
}, [fetchNovels, currentPage]);
|
||||
|
||||
/**
|
||||
* On screen unmount
|
||||
*/
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isScreenMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const clearFilters = useCallback(() => setSelectedFilters(undefined), []);
|
||||
|
||||
const setFilters = (filters: SelectedFilter) => {
|
||||
setIsLoading(true);
|
||||
setCurrentPage(1);
|
||||
fetchNovels(1, filters);
|
||||
setSelectedFilters(filters);
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
novels,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
error,
|
||||
filterValues,
|
||||
setFilters,
|
||||
clearFilters,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSearchSource = (sourceId: number) => {
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<SourceNovelItem[]>([]);
|
||||
const [searchError, setSearchError] = useState<string>();
|
||||
|
||||
const searchSource = useCallback(
|
||||
async (searchTerm: string) => {
|
||||
try {
|
||||
setIsSearching(true);
|
||||
const res = await sourceManager(sourceId).searchNovels(searchTerm);
|
||||
setSearchResults(res);
|
||||
} catch (err) {
|
||||
setSearchError(`${err}`);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
},
|
||||
[sourceId],
|
||||
);
|
||||
|
||||
const clearSearchResults = useCallback(() => setSearchResults([]), []);
|
||||
|
||||
return {
|
||||
isSearching,
|
||||
searchResults,
|
||||
searchSource,
|
||||
clearSearchResults,
|
||||
searchError,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ProgressBar } from 'react-native-paper';
|
||||
|
||||
import { EmptyView, SearchbarV2 } from '@components/index';
|
||||
import GlobalSearchResultsList from './components/GlobalSearchResultsList';
|
||||
|
||||
import useSearch from '@hooks/useSearch';
|
||||
import { useTheme } from '@redux/hooks';
|
||||
import { getString } from '@strings/translations';
|
||||
import { useGlobalSearch } from './hooks/useGlobalSearch';
|
||||
|
||||
interface Props {
|
||||
route?: {
|
||||
params?: {
|
||||
searchText?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const GlobalSearchScreen = (props: Props) => {
|
||||
const theme = useTheme();
|
||||
const { searchText, setSearchText, clearSearchbar } = useSearch(
|
||||
props?.route?.params?.searchText,
|
||||
);
|
||||
const onChangeText = (text: string) => setSearchText(text);
|
||||
const onSubmitEditing = () => globalSearch(searchText);
|
||||
|
||||
const { searchResults, globalSearch, searchAllSources, progress } =
|
||||
useGlobalSearch({
|
||||
defaultSearchText: searchText,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchbarV2
|
||||
searchText={searchText}
|
||||
placeholder={getString('browseScreen.globalSearch')}
|
||||
leftIcon="magnify"
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
clearSearchbar={clearSearchbar}
|
||||
theme={theme}
|
||||
/>
|
||||
{progress ? (
|
||||
<ProgressBar color={theme.colorAccent} progress={progress} />
|
||||
) : null}
|
||||
<GlobalSearchResultsList
|
||||
searchResults={searchResults}
|
||||
ListEmptyComponent={
|
||||
<EmptyView
|
||||
icon="__φ(..)"
|
||||
description={`${getString('globalSearch.searchIn')} ${getString(
|
||||
searchAllSources
|
||||
? 'globalSearch.allSources'
|
||||
: 'globalSearch.pinnedSources',
|
||||
)}`}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchScreen;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Dimensions, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import { coverPlaceholderColor } from '../../../theme/colors';
|
||||
|
||||
import { SourceNovelItem } from '../../../sources/types';
|
||||
import { ThemeType } from '../../../theme/types';
|
||||
|
||||
interface Props {
|
||||
novel: SourceNovelItem;
|
||||
navigateToNovel: (novel: SourceNovelItem) => void;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const GlobalSearchNovelItem: React.FC<Props> = ({
|
||||
novel,
|
||||
navigateToNovel,
|
||||
theme,
|
||||
}) => {
|
||||
const novelItemDimensions = useMemo(
|
||||
() => ({
|
||||
width: Dimensions.get('window').width / 3 - 16,
|
||||
height: ((Dimensions.get('window').width / 3 - 16) * 4) / 3,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.novelItem}>
|
||||
<Pressable
|
||||
style={styles.pressable}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() => navigateToNovel(novel)}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri: novel.novelCover }}
|
||||
style={[styles.novelCover, { ...novelItemDimensions }]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary, width: novelItemDimensions.width },
|
||||
styles.novelName,
|
||||
]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{novel.novelName}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchNovelItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
novelItem: {
|
||||
flex: 1,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pressable: {
|
||||
padding: 4,
|
||||
flex: 1,
|
||||
},
|
||||
novelName: {
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 8,
|
||||
flexWrap: 'wrap',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
novelCover: {
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
import { useTheme } from '@redux/hooks';
|
||||
import { GlobalSearchResult } from '../hooks/useGlobalSearch';
|
||||
import GlobalSearchNovelItem from './GlobalSearchNovelItem';
|
||||
|
||||
interface GlobalSearchResultsListProps {
|
||||
searchResults: GlobalSearchResult[];
|
||||
ListEmptyComponent?: JSX.Element;
|
||||
}
|
||||
|
||||
const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
|
||||
searchResults,
|
||||
ListEmptyComponent,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||
const keyExtractor = useCallback(item => item.source.sourceId.toString(), []);
|
||||
|
||||
const errorColor = useMemo(
|
||||
() => (theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5'),
|
||||
[],
|
||||
);
|
||||
|
||||
const navigateToNovel = useCallback(
|
||||
item => navigation.push('Novel', item),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<FlatList<GlobalSearchResult>
|
||||
keyExtractor={keyExtractor}
|
||||
data={searchResults}
|
||||
contentContainerStyle={styles.resultList}
|
||||
renderItem={({ item }) => (
|
||||
<>
|
||||
<View>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.sourceHeader}
|
||||
onPress={() =>
|
||||
navigation.navigate('SourceScreen', {
|
||||
sourceId: item.source.sourceId,
|
||||
sourceName: item.source.sourceName,
|
||||
url: item.source.url,
|
||||
})
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
style={[styles.sourceName, { color: theme.textColorPrimary }]}
|
||||
>
|
||||
{item.source.sourceName}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.language, { color: theme.textColorSecondary }]}
|
||||
>
|
||||
{item.source.lang}
|
||||
</Text>
|
||||
</View>
|
||||
<MaterialCommunityIcons
|
||||
name="arrow-right"
|
||||
size={24}
|
||||
color={theme.textColorPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
{item.isLoading ? (
|
||||
<ActivityIndicator
|
||||
color={theme.colorAccent}
|
||||
style={styles.loading}
|
||||
/>
|
||||
) : item.error ? (
|
||||
<Text style={[styles.error, { color: errorColor }]}>
|
||||
{item.error}
|
||||
</Text>
|
||||
) : (
|
||||
<FlatList
|
||||
horizontal
|
||||
contentContainerStyle={styles.novelsContainer}
|
||||
keyExtractor={novelItem =>
|
||||
novelItem.novelUrl + novelItem.sourceId
|
||||
}
|
||||
data={item.novels}
|
||||
ListEmptyComponent={
|
||||
<Text
|
||||
style={[
|
||||
styles.listEmpty,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
{getString('sourceScreen.noResultsFound')}
|
||||
</Text>
|
||||
}
|
||||
renderItem={({ item: novelItem }) => (
|
||||
<GlobalSearchNovelItem
|
||||
novel={novelItem}
|
||||
navigateToNovel={navigateToNovel}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchResultsList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
resultList: {
|
||||
flexGrow: 1,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 60,
|
||||
},
|
||||
loading: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 16,
|
||||
},
|
||||
sourceHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingRight: 8,
|
||||
},
|
||||
sourceName: {
|
||||
marginTop: 8,
|
||||
marginBottom: 4,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
language: {
|
||||
fontSize: 12,
|
||||
marginBottom: 8,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
error: {
|
||||
padding: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
novelsContainer: {
|
||||
padding: 8,
|
||||
},
|
||||
listEmpty: {
|
||||
paddingHorizontal: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useBrowseSettings, useSourcesReducer } from '@redux/hooks';
|
||||
import { Source, SourceNovelItem } from '../../../sources/types';
|
||||
import { sourceManager } from '../../../sources/sourceManager';
|
||||
|
||||
interface Props {
|
||||
defaultSearchText?: string;
|
||||
}
|
||||
|
||||
export interface GlobalSearchResult {
|
||||
isLoading: boolean;
|
||||
source: Source;
|
||||
novels: SourceNovelItem[];
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export const useGlobalSearch = ({ defaultSearchText }: Props) => {
|
||||
const isMounted = useRef(true);
|
||||
const { searchAllSources = false } = useBrowseSettings();
|
||||
|
||||
const { allSources, pinnedSourceIds = [] } = useSourcesReducer();
|
||||
|
||||
const isPinned = useCallback(
|
||||
(sourceId: number) => pinnedSourceIds.indexOf(sourceId) > -1,
|
||||
[pinnedSourceIds],
|
||||
);
|
||||
|
||||
const pinnedSources = useMemo(
|
||||
() => allSources.filter(source => isPinned(source.sourceId)),
|
||||
[allSources, isPinned],
|
||||
);
|
||||
|
||||
const [searchResults, setSearchResults] = useState<GlobalSearchResult[]>([]);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const globalSearch = (searchText: string) => {
|
||||
const sources = searchAllSources ? allSources : pinnedSources;
|
||||
|
||||
const defaultResult: GlobalSearchResult[] = sources.map(source => ({
|
||||
isLoading: true,
|
||||
source,
|
||||
novels: [],
|
||||
error: null,
|
||||
}));
|
||||
|
||||
setSearchResults(defaultResult);
|
||||
|
||||
sources.forEach(async source => {
|
||||
if (isMounted.current) {
|
||||
try {
|
||||
const res = await sourceManager(source.sourceId).searchNovels(
|
||||
searchText,
|
||||
);
|
||||
|
||||
setSearchResults(prevState =>
|
||||
prevState.map(prevResult =>
|
||||
prevResult.source.sourceId === source.sourceId
|
||||
? { ...prevResult, novels: res, isLoading: false }
|
||||
: { ...prevResult },
|
||||
),
|
||||
);
|
||||
|
||||
setSearchResults(prevState =>
|
||||
prevState.sort(
|
||||
(
|
||||
{ novels: a, source: { sourceName: aName } },
|
||||
{ novels: b, source: { sourceName: bName } },
|
||||
) => {
|
||||
if (!a.length) {
|
||||
return 1;
|
||||
}
|
||||
if (!b.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return aName.localeCompare(bName);
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
setSearchResults(prevState =>
|
||||
prevState.map(prevResult =>
|
||||
prevResult.source.sourceId === source.sourceId
|
||||
? {
|
||||
...prevResult,
|
||||
novels: [],
|
||||
isLoading: false,
|
||||
error: error.message,
|
||||
}
|
||||
: { ...prevResult },
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setProgress(prevState => prevState + 1 / sources.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultSearchText) {
|
||||
globalSearch(defaultSearchText);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { searchResults, globalSearch, searchAllSources, progress };
|
||||
};
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useAppDispatch,
|
||||
useBrowseSettings,
|
||||
useSourcesReducer,
|
||||
useThemeV1,
|
||||
useTheme,
|
||||
} from '../../redux/hooks';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useSearch } from '../../hooks';
|
||||
@@ -23,7 +23,7 @@ import MalCard from './discover/MalCard/MalCard';
|
||||
|
||||
const BrowseScreen = () => {
|
||||
const { navigate } = useNavigation();
|
||||
const theme = useThemeV1();
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { searchText, setSearchText, clearSearchbar } = useSearch();
|
||||
@@ -59,23 +59,27 @@ const BrowseScreen = () => {
|
||||
? allSources.filter(source => source.sourceId === lastUsed)
|
||||
: [];
|
||||
|
||||
const navigateToSource = useCallback((source: Source) => {
|
||||
navigate(
|
||||
'SourceScreen' as never,
|
||||
{
|
||||
sourceId: source.sourceId,
|
||||
sourceName: source.sourceName,
|
||||
url: source.url,
|
||||
} as never,
|
||||
);
|
||||
dispatch(setLastUsedSource({ sourceId: source.sourceId }));
|
||||
}, []);
|
||||
const navigateToSource = useCallback(
|
||||
(source: Source, showLatestNovels?: boolean) => {
|
||||
navigate(
|
||||
'SourceScreen' as never,
|
||||
{
|
||||
sourceId: source.sourceId,
|
||||
sourceName: source.sourceName,
|
||||
url: source.url,
|
||||
showLatestNovels,
|
||||
} as never,
|
||||
);
|
||||
dispatch(setLastUsedSource({ sourceId: source.sourceId }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const searchbarActions = useMemo(
|
||||
() => [
|
||||
{
|
||||
iconName: 'book-search',
|
||||
onPress: () => navigate('GlobalSearch' as never),
|
||||
onPress: () => navigate('GlobalSearchScreen' as never),
|
||||
},
|
||||
{
|
||||
iconName: 'swap-vertical-variant',
|
||||
@@ -108,19 +112,6 @@ const BrowseScreen = () => {
|
||||
/>
|
||||
) : allSources.length === 0 ? null : (
|
||||
<>
|
||||
{showMyAnimeList && (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionHeader,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
{getString('browseScreen.discover')}
|
||||
</Text>
|
||||
{showMyAnimeList && <MalCard theme={theme} />}
|
||||
</>
|
||||
)}
|
||||
<SectionList
|
||||
sections={[
|
||||
{
|
||||
@@ -133,6 +124,21 @@ const BrowseScreen = () => {
|
||||
data: onlyShowPinnedSources ? [] : allSources,
|
||||
},
|
||||
]}
|
||||
ListHeaderComponent={
|
||||
showMyAnimeList ? (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionHeader,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
{getString('browseScreen.discover')}
|
||||
</Text>
|
||||
{showMyAnimeList && <MalCard theme={theme} />}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
renderSectionHeader={({ section: { header, data } }) =>
|
||||
data.length > 0 ? (
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useAppDispatch,
|
||||
useBrowseSettings,
|
||||
useSourcesReducer,
|
||||
useThemeV1,
|
||||
useTheme,
|
||||
} from '../../redux/hooks';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { getString } from '../../../strings/translations';
|
||||
@@ -15,7 +15,7 @@ import { toggleLanguageFilter } from '../../redux/source/sourcesSlice';
|
||||
import { setBrowseSettings } from '../../redux/settings/settingsSlice';
|
||||
|
||||
const BrowseSettings = () => {
|
||||
const theme = useThemeV1();
|
||||
const theme = useTheme();
|
||||
const { goBack } = useNavigation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ import { ButtonVariation } from '../../../../components/Button/Button';
|
||||
|
||||
import { Source } from '../../../../sources/types';
|
||||
import { coverPlaceholderColor } from '../../../../theme/colors';
|
||||
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../../../theme/types';
|
||||
|
||||
interface Props {
|
||||
source: Source;
|
||||
isPinned: boolean;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
onTogglePinSource: (sourceId: number) => void;
|
||||
navigateToSource: (source: Source) => void;
|
||||
navigateToSource: (source: Source, showLatestNovels?: boolean) => void;
|
||||
}
|
||||
|
||||
const SourceCard: React.FC<Props> = ({
|
||||
@@ -41,12 +41,11 @@ const SourceCard: React.FC<Props> = ({
|
||||
</View>
|
||||
<View style={styles.flexRow}>
|
||||
<Button
|
||||
title={getString('browse')}
|
||||
title={getString('browseScreen.latest')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
textColor={theme.colorAccent}
|
||||
onPress={() => navigateToSource(source)}
|
||||
onPress={() => navigateToSource(source, true)}
|
||||
theme={theme}
|
||||
margin={0}
|
||||
/>
|
||||
<IconButtonV2
|
||||
name={isPinned ? 'pin' : 'pin-outline'}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { getString } from '../../../../../strings/translations';
|
||||
import { Button } from '../../../../components';
|
||||
import { ButtonVariation } from '../../../../components/Button/Button';
|
||||
|
||||
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../../../theme/types';
|
||||
|
||||
interface Props {
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const MalCard: React.FC<Props> = ({ theme }) => {
|
||||
@@ -36,7 +36,6 @@ const MalCard: React.FC<Props> = ({ theme }) => {
|
||||
textColor={theme.colorAccent}
|
||||
onPress={() => navigate('BrowseMal' as never)}
|
||||
theme={theme}
|
||||
margin={0}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
@@ -2,12 +2,12 @@ import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../../../theme/types';
|
||||
|
||||
interface Props {
|
||||
novel: { novelName: string; novelCover: string; score: string; info: string };
|
||||
onPress: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const MalNovelCard: React.FC<Props> = ({ novel, onPress, theme }) => {
|
||||
@@ -34,18 +34,22 @@ const MalNovelCard: React.FC<Props> = ({ novel, onPress, theme }) => {
|
||||
{novel.score}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text style={[styles.small, { color: theme.textColorPrimary }]}>
|
||||
Type:{' '}
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
{novel.info[1]}
|
||||
{novel?.info?.[1] ? (
|
||||
<Text style={[styles.small, { color: theme.textColorPrimary }]}>
|
||||
Type:{' '}
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
{novel.info[1]}
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
<Text style={[styles.small, { color: theme.textColorPrimary }]}>
|
||||
Published:{' '}
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
{novel.info[2]}
|
||||
) : null}
|
||||
{novel?.info?.[2] ? (
|
||||
<Text style={[styles.small, { color: theme.textColorPrimary }]}>
|
||||
Published:{' '}
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
{novel.info[2]}
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -73,7 +77,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
infoContainer: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
marginBottom: 4,
|
||||
|
||||
@@ -69,7 +69,7 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
novel={item}
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
navigation.navigate('GlobalSearch', {
|
||||
navigation.navigate('GlobalSearchScreen', {
|
||||
novelName: item.novelName,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { FlatList } from 'react-native';
|
||||
|
||||
import { ProgressBar } from 'react-native-paper';
|
||||
|
||||
import { Searchbar } from '../../../components/Searchbar/Searchbar';
|
||||
import EmptyView from '../../../components/EmptyView';
|
||||
|
||||
import { ScreenContainer } from '../../../components/Common';
|
||||
import GlobalSearchSourceItem from './GlobalSearchSourceItem';
|
||||
|
||||
import { useLibrary, useTheme } from '../../../hooks/reduxHooks';
|
||||
import { sourceManager } from '../../../sources/sourceManager';
|
||||
import { useBrowseSettings, useSourcesReducer } from '../../../redux/hooks';
|
||||
|
||||
const GlobalSearch = ({ route, navigation }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
let novelName = '';
|
||||
if (route.params) {
|
||||
novelName = route.params.novelName;
|
||||
}
|
||||
const { allSources, pinnedSourceIds = [] } = useSourcesReducer();
|
||||
|
||||
const isPinned = sourceId => pinnedSourceIds.indexOf(sourceId) > -1;
|
||||
const pinnedSources = allSources.filter(source => isPinned(source.sourceId));
|
||||
|
||||
const { searchAllSources = false } = useBrowseSettings();
|
||||
|
||||
const [searchText, setSearchText] = useState(novelName);
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const library = useLibrary();
|
||||
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (novelName) {
|
||||
onSubmitEditing();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const clearSearchbar = () => setSearchText('');
|
||||
const onChangeText = text => setSearchText(text);
|
||||
|
||||
const onSubmitEditing = async () => {
|
||||
setProgress(0);
|
||||
|
||||
let globalSearchSources = searchAllSources ? allSources : pinnedSources;
|
||||
|
||||
setSearchResults(
|
||||
globalSearchSources.map(item => ({
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
lang: item.lang,
|
||||
loading: true,
|
||||
novels: [],
|
||||
error: null,
|
||||
})),
|
||||
);
|
||||
|
||||
globalSearchSources.map(async item => {
|
||||
if (isMounted.current === true) {
|
||||
try {
|
||||
const source = sourceManager(item.sourceId);
|
||||
const data = await source.searchNovels(encodeURI(searchText));
|
||||
|
||||
setSearchResults(prevState =>
|
||||
prevState.map(sourceItem =>
|
||||
sourceItem.sourceId === item.sourceId
|
||||
? { ...sourceItem, novels: data, loading: false }
|
||||
: { ...sourceItem },
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
setSearchResults(prevState =>
|
||||
prevState.map(sourceItem =>
|
||||
sourceItem.sourceId === item.sourceId
|
||||
? {
|
||||
...sourceItem,
|
||||
loading: false,
|
||||
error: e.message,
|
||||
}
|
||||
: sourceItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setProgress(before => before + 1 / globalSearchSources.length);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<GlobalSearchSourceItem
|
||||
source={item}
|
||||
library={library}
|
||||
theme={theme}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Searchbar
|
||||
theme={theme}
|
||||
placeholder="Global Search"
|
||||
backAction="arrow-left"
|
||||
onBackAction={navigation.goBack}
|
||||
searchText={searchText}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
clearSearchbar={clearSearchbar}
|
||||
/>
|
||||
{progress > 0 && (
|
||||
<ProgressBar color={theme.colorAccent} progress={progress} />
|
||||
)}
|
||||
<FlatList
|
||||
contentContainerStyle={{ flexGrow: 1, padding: 4 }}
|
||||
data={searchResults}
|
||||
keyExtractor={item => item.sourceId.toString()}
|
||||
renderItem={renderItem}
|
||||
extraData={pinnedSources}
|
||||
ListEmptyComponent={
|
||||
<EmptyView
|
||||
icon="__φ(..)"
|
||||
description={`Search a novel in ${
|
||||
searchAllSources
|
||||
? 'all sources'
|
||||
: pinnedSources.length === 0
|
||||
? 'pinned sources\n(No sources pinned)'
|
||||
: 'pinned sources'
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearch;
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, FlatList, Text } from 'react-native';
|
||||
|
||||
import GlobalSearchNovelCover from './GlobalSearchNovelCover';
|
||||
|
||||
const GlobalSearchNovelList = ({ data, theme, library, navigation }) => {
|
||||
const inLibrary = (sourceId, novelUrl) =>
|
||||
library.some(obj => obj.novelUrl === novelUrl && obj.sourceId === sourceId);
|
||||
|
||||
const navigateToNovel = item => navigation.push('Novel', item);
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<GlobalSearchNovelCover
|
||||
novel={item}
|
||||
theme={theme}
|
||||
onPress={() => navigateToNovel(item)}
|
||||
inLibrary={inLibrary(item.sourceId, item.novelUrl)}
|
||||
/>
|
||||
);
|
||||
|
||||
const ListEmptyComponent = (
|
||||
<Text style={[{ color: theme.textColorSecondary }, styles.emptyList]}>
|
||||
No results found
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.listContainer}
|
||||
horizontal={true}
|
||||
data={data}
|
||||
keyExtractor={item => item.sourceId + item.novelUrl}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchNovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listContainer: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
emptyList: {
|
||||
padding: 8,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text, ActivityIndicator } from 'react-native';
|
||||
|
||||
import GlobalSearchNovelList from './GlobalSearchNovelList';
|
||||
|
||||
const GlobalSearchSourceItem = ({ source, library, theme, navigation }) => {
|
||||
const { sourceName, lang, loading, novels, error } = source;
|
||||
|
||||
const colorError = {
|
||||
color: theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.sourceContainer}>
|
||||
<Text style={{ color: theme.textColorPrimary }}>{sourceName}</Text>
|
||||
<Text style={{ color: theme.textColorSecondary, fontSize: 12 }}>
|
||||
{lang}
|
||||
</Text>
|
||||
</View>
|
||||
{error ? (
|
||||
<Text style={[styles.error, colorError]}>{error}</Text>
|
||||
) : loading ? (
|
||||
<ActivityIndicator
|
||||
color={theme.colorAccent}
|
||||
style={{ marginVertical: 16 }}
|
||||
/>
|
||||
) : (
|
||||
<GlobalSearchNovelList
|
||||
data={novels}
|
||||
theme={theme}
|
||||
library={library}
|
||||
navigation={navigation}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchSourceItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
sourceContainer: {
|
||||
padding: 8,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
error: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
});
|
||||
@@ -2,26 +2,28 @@ import React, { useState } from 'react';
|
||||
import { StyleSheet, SectionList, Text } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import moment from 'moment';
|
||||
import { Portal } from 'react-native-paper';
|
||||
|
||||
import {
|
||||
EmptyView,
|
||||
ErrorScreenV2,
|
||||
LoadingScreenV2,
|
||||
SearchbarV2,
|
||||
} from '../../components';
|
||||
} from '../../components/index';
|
||||
import HistoryCard from './components/HistoryCard/HistoryCard';
|
||||
|
||||
import { useSearch, useHistory } from '../../hooks';
|
||||
import { useThemeV1 } from '../../redux/hooks';
|
||||
import { useSearch, useHistory, useBoolean } from '@hooks/index';
|
||||
import { useTheme } from '../../redux/hooks';
|
||||
|
||||
import { dateFormats } from '../../utils/constants/dateFormats';
|
||||
import { convertDateToISOString } from '../../database/utils/convertDateToISOString';
|
||||
|
||||
import { History } from '../../database/types';
|
||||
import { getString } from '../../../strings/translations';
|
||||
import { getString } from '@strings/translations';
|
||||
import ClearHistoryDialog from './components/ClearHistoryDialog';
|
||||
|
||||
const HistoryScreen = () => {
|
||||
const theme = useThemeV1();
|
||||
const theme = useTheme();
|
||||
const { navigate } = useNavigation();
|
||||
|
||||
const {
|
||||
@@ -74,8 +76,10 @@ const HistoryScreen = () => {
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
novelName: string,
|
||||
novelUrl: string,
|
||||
chapterId: number,
|
||||
chapterUrl: string,
|
||||
chapterName: string,
|
||||
bookmark: number,
|
||||
) =>
|
||||
navigate(
|
||||
@@ -84,9 +88,11 @@ const HistoryScreen = () => {
|
||||
sourceId,
|
||||
novelId,
|
||||
novelName,
|
||||
novelUrl,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
bookmark,
|
||||
chapterName,
|
||||
} as never,
|
||||
);
|
||||
|
||||
@@ -108,6 +114,12 @@ const HistoryScreen = () => {
|
||||
} as never,
|
||||
);
|
||||
|
||||
const {
|
||||
value: clearHistoryDialogVisible,
|
||||
setTrue: openClearHistoryDialog,
|
||||
setFalse: closeClearHistoryDialog,
|
||||
} = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchbarV2
|
||||
@@ -119,7 +131,7 @@ const HistoryScreen = () => {
|
||||
rightIcons={[
|
||||
{
|
||||
iconName: 'delete-sweep-outline',
|
||||
onPress: clearAllHistory,
|
||||
onPress: openClearHistoryDialog,
|
||||
},
|
||||
]}
|
||||
theme={theme}
|
||||
@@ -158,6 +170,14 @@ const HistoryScreen = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Portal>
|
||||
<ClearHistoryDialog
|
||||
visible={clearHistoryDialogVisible}
|
||||
onSubmit={clearAllHistory}
|
||||
onDismiss={closeClearHistoryDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { Dialog, Portal } from 'react-native-paper';
|
||||
import { ThemeType } from '../../../theme/types';
|
||||
import { Button } from '@components/index';
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
|
||||
interface ClearHistoryDialogProps {
|
||||
visible: boolean;
|
||||
theme: ThemeType;
|
||||
onSubmit: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
const ClearHistoryDialog: React.FC<ClearHistoryDialogProps> = ({
|
||||
visible,
|
||||
onDismiss,
|
||||
theme,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const handleOnSubmit = () => {
|
||||
onSubmit();
|
||||
onDismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
style={[styles.container, { backgroundColor: theme.colorPrimary }]}
|
||||
>
|
||||
<Dialog.Title style={[styles.title, { color: theme.textColorPrimary }]}>
|
||||
{getString('historyScreen.clearHistorWarning')}
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<Button
|
||||
theme={theme}
|
||||
onPress={onDismiss}
|
||||
title={getString('common.cancel')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
/>
|
||||
<Button
|
||||
theme={theme}
|
||||
onPress={handleOnSubmit}
|
||||
title={getString('common.ok')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
style={styles.button}
|
||||
/>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearHistoryDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
title: {
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
marginLeft: 4,
|
||||
},
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import { IconButtonV2 } from '../../../../components';
|
||||
import { parseChapterNumber } from '../../../../utils/parseChapterNumber';
|
||||
|
||||
import { History } from '../../../../database/types';
|
||||
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../../../theme/types';
|
||||
import { coverPlaceholderColor } from '../../../../theme/colors';
|
||||
|
||||
interface HistoryCardProps {
|
||||
@@ -18,8 +18,10 @@ interface HistoryCardProps {
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
novelName: string,
|
||||
novelUrl: string,
|
||||
chapterId: number,
|
||||
chapterUrl: string,
|
||||
chapterName: string,
|
||||
isBookmarked: number,
|
||||
) => void;
|
||||
handleRemoveFromHistory: (historyId: number) => void;
|
||||
@@ -30,7 +32,7 @@ interface HistoryCardProps {
|
||||
novelName: string,
|
||||
novelCover: string,
|
||||
) => void;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const HistoryCard: React.FC<HistoryCardProps> = ({
|
||||
@@ -57,7 +59,7 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
|
||||
const chapterNoAndTime = useMemo(
|
||||
() =>
|
||||
`Chapter ${parseChapterNumber(chapterName)} • ${moment(historyTimeRead)
|
||||
.format('h:mm a')
|
||||
.format('LT')
|
||||
.toUpperCase()}`,
|
||||
[chapterName, historyTimeRead],
|
||||
);
|
||||
@@ -103,8 +105,10 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
|
||||
sourceId,
|
||||
novelId,
|
||||
novelName,
|
||||
novelUrl,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
bookmark,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import moment from 'moment';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import { parseChapterNumber } from '../../../services/utils/helpers';
|
||||
import { setNovel } from '../../../redux/novel/novel.actions';
|
||||
import { defaultCoverUri } from '../../../sources/helpers/constants';
|
||||
import { IconButton } from '../../../components/IconButton/IconButton';
|
||||
|
||||
const HistoryItem = ({
|
||||
history,
|
||||
theme,
|
||||
dispatch,
|
||||
navigation,
|
||||
deleteHistory,
|
||||
}) => {
|
||||
const {
|
||||
historyId,
|
||||
historyTimeRead,
|
||||
historyChapterId,
|
||||
sourceId,
|
||||
novelId,
|
||||
novelUrl,
|
||||
novelName,
|
||||
novelCover,
|
||||
chapterName,
|
||||
chapterUrl,
|
||||
bookmark,
|
||||
} = history;
|
||||
|
||||
const getChapterNumber = useCallback(
|
||||
() =>
|
||||
`Chapter ${parseChapterNumber(chapterName)} • ${moment(historyTimeRead)
|
||||
.format('h:mm a')
|
||||
.toUpperCase()}`,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const navigateToNovel = () => {
|
||||
navigation.navigate('Novel', history);
|
||||
dispatch(setNovel(history));
|
||||
};
|
||||
|
||||
const navigateToChapter = () =>
|
||||
navigation.navigate('Chapter', {
|
||||
chapterId: historyChapterId,
|
||||
chapterUrl,
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelId,
|
||||
chapterName,
|
||||
novelName,
|
||||
bookmark,
|
||||
});
|
||||
|
||||
const uri =
|
||||
novelCover && !novelCover.startsWith('/') ? novelCover : defaultCoverUri;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={navigateToNovel}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.container}>
|
||||
<View>
|
||||
<FastImage source={{ uri }} style={styles.novelCover} />
|
||||
</View>
|
||||
<View style={styles.textContainer}>
|
||||
<Text
|
||||
style={[{ color: theme.textColorPrimary }, styles.chapterTitle]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{novelName}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorSecondary },
|
||||
styles.chapterNumber,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{getChapterNumber()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.buttons}>
|
||||
<IconButton
|
||||
icon="delete-outline"
|
||||
size={24}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={() => deleteHistory(historyId)}
|
||||
containerStyle={{ marginLeft: 12 }}
|
||||
theme={theme}
|
||||
/>
|
||||
<IconButton
|
||||
icon="play"
|
||||
size={24}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={navigateToChapter}
|
||||
containerStyle={{ marginLeft: 12 }}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
novelCover: {
|
||||
height: 80,
|
||||
width: 56,
|
||||
borderRadius: 4,
|
||||
},
|
||||
textContainer: {
|
||||
flexShrink: 1,
|
||||
marginLeft: 16,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
chapterTitle: {
|
||||
fontSize: 15,
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
buttons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
chapterNumber: {
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Dialog, Portal } from 'react-native-paper';
|
||||
|
||||
const RemoveHistoryDialog = ({ dialogVisible, hideDialog, theme, onPress }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog
|
||||
visible={dialogVisible}
|
||||
onDismiss={hideDialog}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
}}
|
||||
>
|
||||
<Dialog.Title
|
||||
style={{
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
Are you sure? All history will be lost.
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<Button
|
||||
uppercase={false}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={hideDialog}
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
uppercase={false}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={onPress}
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveHistoryDialog;
|
||||
@@ -44,6 +44,7 @@ const LibraryScreen = ({ navigation }) => {
|
||||
incognitoMode = false,
|
||||
updateLibraryOnLaunch = false,
|
||||
downloadedOnlyMode = false,
|
||||
disableHapticFeedback = false,
|
||||
} = useSettings();
|
||||
|
||||
const bottomSheetRef = useRef(null);
|
||||
@@ -94,7 +95,9 @@ const LibraryScreen = ({ navigation }) => {
|
||||
|
||||
const selectNovel = novelId => {
|
||||
if (selectedNovels.indexOf(novelId) === -1) {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
if (!disableHapticFeedback) {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
}
|
||||
setSelectedNovels(prevState => [...prevState, novelId]);
|
||||
} else {
|
||||
setSelectedNovels(prevState => prevState.filter(id => id !== novelId));
|
||||
@@ -185,8 +188,8 @@ const LibraryScreen = ({ navigation }) => {
|
||||
<View style={styles.globalSearch}>
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
navigation.navigate('GlobalSearch', {
|
||||
novelName: searchText,
|
||||
navigation.navigate('GlobalSearchScreen', {
|
||||
searchText,
|
||||
})
|
||||
}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
@@ -259,11 +262,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
globalSearch: {
|
||||
marginHorizontal: 16,
|
||||
borderRadius: 4,
|
||||
marginTop: 8,
|
||||
marginBottom: 16,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
globalSearchBtn: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
@@ -68,33 +68,41 @@ const LibrarySheet = ({
|
||||
</View>
|
||||
);
|
||||
|
||||
const sortOrders = [
|
||||
{
|
||||
label: 'Date Added',
|
||||
ASC: 'novels.novelId ASC',
|
||||
DESC: 'novels.novelId DESC',
|
||||
},
|
||||
{
|
||||
label: 'Alphabetically',
|
||||
ASC: 'novels.novelName ASC',
|
||||
DESC: 'novels.novelName DESC',
|
||||
},
|
||||
{
|
||||
label: 'Unread',
|
||||
ASC: 'novels.unread ASC',
|
||||
DESC: 'novels.unread DESC',
|
||||
},
|
||||
{
|
||||
label: 'Downloaded',
|
||||
ASC: 'chaptersDownloaded ASC',
|
||||
DESC: 'chaptersDownloaded DESC',
|
||||
},
|
||||
{
|
||||
label: 'Total Chapters',
|
||||
ASC: 'chaptersUnread ASC',
|
||||
DESC: 'chaptersUnread DESC',
|
||||
},
|
||||
];
|
||||
const sortOrders = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Last Read',
|
||||
ASC: 'lastReadAt ASC',
|
||||
DESC: 'lastReadAt DESC',
|
||||
},
|
||||
{
|
||||
label: 'Alphabetically',
|
||||
ASC: 'novels.novelName ASC',
|
||||
DESC: 'novels.novelName DESC',
|
||||
},
|
||||
{
|
||||
label: 'Unread',
|
||||
ASC: 'novels.unread ASC',
|
||||
DESC: 'novels.unread DESC',
|
||||
},
|
||||
{
|
||||
label: 'Downloaded',
|
||||
ASC: 'chaptersDownloaded ASC',
|
||||
DESC: 'chaptersDownloaded DESC',
|
||||
},
|
||||
{
|
||||
label: 'Total Chapters',
|
||||
ASC: 'chaptersUnread ASC',
|
||||
DESC: 'chaptersUnread DESC',
|
||||
},
|
||||
{
|
||||
label: 'Date Added',
|
||||
ASC: 'novels.novelId ASC',
|
||||
DESC: 'novels.novelId DESC',
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const SecondRoute = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
|
||||
@@ -84,6 +84,7 @@ const Downloads = ({ navigation }) => {
|
||||
const downloadChapter = (
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterId,
|
||||
@@ -92,6 +93,7 @@ const Downloads = ({ navigation }) => {
|
||||
downloadChapterAction(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterId,
|
||||
@@ -99,7 +101,7 @@ const Downloads = ({ navigation }) => {
|
||||
);
|
||||
|
||||
const deleteChapter = (chapterId, chapterName) => {
|
||||
dispatch(deleteChapterAction(chapterId, chapterName));
|
||||
dispatch(deleteChapterAction(sourceId, novelId, chapterId, chapterName));
|
||||
setChapters(chaps => chaps.filter(item => item.chapterId !== chapterId));
|
||||
};
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ import { pickCustomNovelCover } from '../../database/queries/NovelQueries';
|
||||
import FadeView from '../../components/Common/CrossFadeView';
|
||||
import { useModal } from '../../hooks/useModal';
|
||||
import DownloadCustomChapterModal from './components/DownloadCustomChapterModal';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
const Novel = ({ route, navigation }) => {
|
||||
const item = route.params;
|
||||
@@ -64,6 +65,9 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { top: topInset } = useSafeAreaInsets();
|
||||
const progressViewOffset = topInset + 32;
|
||||
|
||||
const { novel, chapters, loading, updating } = useNovel();
|
||||
const { downloadQueue } = useSelector(state => state.downloadsReducer);
|
||||
|
||||
@@ -80,6 +84,7 @@ const Novel = ({ route, navigation }) => {
|
||||
const {
|
||||
useFabForContinueReading = false,
|
||||
defaultChapterSort = 'ORDER BY chapterId ASC',
|
||||
disableHapticFeedback = false,
|
||||
} = useSettings();
|
||||
|
||||
const {
|
||||
@@ -107,6 +112,7 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const refreshControl = () => (
|
||||
<RefreshControl
|
||||
progressViewOffset={progressViewOffset}
|
||||
onRefresh={onRefresh}
|
||||
refreshing={updating}
|
||||
colors={[theme.textColorPrimary]}
|
||||
@@ -123,6 +129,7 @@ const Novel = ({ route, navigation }) => {
|
||||
downloadChapterAction(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterId,
|
||||
@@ -130,7 +137,7 @@ const Novel = ({ route, navigation }) => {
|
||||
);
|
||||
|
||||
const deleteChapter = (chapterId, chapterName) =>
|
||||
dispatch(deleteChapterAction(chapterId, chapterName));
|
||||
dispatch(deleteChapterAction(sourceId, novelId, chapterId, chapterName));
|
||||
|
||||
const isSelected = chapterId => {
|
||||
return selected.some(obj => obj.chapterId === chapterId);
|
||||
@@ -152,7 +159,9 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const onSelectLongPress = chapter => {
|
||||
if (selected.length === 0) {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
if (!disableHapticFeedback) {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
}
|
||||
setSelected(sel => [...sel, chapter]);
|
||||
} else {
|
||||
/**
|
||||
@@ -472,7 +481,9 @@ const Novel = ({ route, navigation }) => {
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
onPress={() => dispatch(deleteAllChaptersAction(chapters))}
|
||||
onPress={() =>
|
||||
dispatch(deleteAllChaptersAction(sourceId, chapters))
|
||||
}
|
||||
/>
|
||||
</Menu>
|
||||
|
||||
@@ -594,7 +605,7 @@ const Novel = ({ route, navigation }) => {
|
||||
selected.some(obj => obj.downloaded === 1) && {
|
||||
icon: 'trash-can-outline',
|
||||
onPress: () => {
|
||||
dispatch(deleteAllChaptersAction(selected));
|
||||
dispatch(deleteAllChaptersAction(sourceId, selected));
|
||||
setSelected([]);
|
||||
},
|
||||
},
|
||||
@@ -641,7 +652,7 @@ const Novel = ({ route, navigation }) => {
|
||||
action={{
|
||||
label: 'Delete',
|
||||
onPress: () => {
|
||||
dispatch(deleteAllChaptersAction(chapters));
|
||||
dispatch(deleteAllChaptersAction(sourceId, chapters));
|
||||
},
|
||||
}}
|
||||
theme={{ colors: { accent: theme.colorAccent } }}
|
||||
|
||||
@@ -108,7 +108,10 @@ const EditInfoModal = ({ theme, hideModal, modalVisible, novel, dispatch }) => {
|
||||
dense
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={`Description: ${info.novelSummary.substring(0, 16)}...`}
|
||||
placeholder={`Description: ${info?.novelSummary?.substring(
|
||||
0,
|
||||
16,
|
||||
)}...`}
|
||||
style={{ fontSize: 14 }}
|
||||
numberOfLines={1}
|
||||
mode="outlined"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { View, Text, StyleSheet, Pressable } from 'react-native';
|
||||
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
|
||||
import { IconButton } from 'react-native-paper';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
@@ -68,7 +70,7 @@ const NovelInfoHeader = ({
|
||||
<NovelTitle
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
navigation.navigate('GlobalSearch', {
|
||||
navigation.navigate('GlobalSearchScreen', {
|
||||
novelName: item.novelName,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
|
||||
? 'Start reading'
|
||||
: getString('novelScreen.continueReading')
|
||||
} ${lastRead.chapterName}`}
|
||||
margin={16}
|
||||
style={{ margin: 16 }}
|
||||
textColor={theme.colorButtonText}
|
||||
theme={theme}
|
||||
onPress={navigateToLastReadChapter}
|
||||
/>
|
||||
@@ -42,8 +43,9 @@ const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
|
||||
? 'All chapters read'
|
||||
: getString('novelScreen.continueReading')
|
||||
} ${lastRead.chapterName}`}
|
||||
margin={16}
|
||||
style={{ margin: 16 }}
|
||||
color={theme.colorDisabled}
|
||||
textColor={theme.colorButtonText}
|
||||
theme={theme}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -6,12 +6,12 @@ import * as WebBrowser from 'expo-web-browser';
|
||||
import { NovelInfo } from '../../../../database/types';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useNovelTrackerInfo } from '../../../../hooks';
|
||||
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../../../theme/types';
|
||||
import { getString } from '../../../../../strings/translations';
|
||||
|
||||
interface NovelScreenButtonGroupProps {
|
||||
novel: NovelInfo;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
handleTrackerSheet: () => void;
|
||||
handleFollowNovel: () => void;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import React, { useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
|
||||
import { ThemeType } from '../../../../theme/types';
|
||||
|
||||
interface NovelSummaryProps {
|
||||
summary: string;
|
||||
isExpanded: boolean;
|
||||
theme: ThemeTypeV1;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const NovelSummary: React.FC<NovelSummaryProps> = ({
|
||||
|
||||
+148
-139
@@ -6,8 +6,12 @@ import {
|
||||
ScrollView,
|
||||
TouchableWithoutFeedback,
|
||||
Dimensions,
|
||||
NativeModules,
|
||||
NativeEventEmitter,
|
||||
} from 'react-native';
|
||||
|
||||
import VolumeButtonListener from './../../utils/volumeButtonListener';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconButton, Portal } from 'react-native-paper';
|
||||
import { useKeepAwake } from 'expo-keep-awake';
|
||||
@@ -20,19 +24,16 @@ import { fetchChapter } from '../../services/Source/source';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import {
|
||||
usePosition,
|
||||
useReaderSettings,
|
||||
useSettings,
|
||||
useTheme,
|
||||
useTrackingStatus,
|
||||
} from '../../hooks/reduxHooks';
|
||||
import { updateChaptersRead } from '../../redux/tracker/tracker.actions';
|
||||
import { readerBackground, readerTextColor } from './utils/readerStyles';
|
||||
import { markChapterReadAction } from '../../redux/novel/novel.actions';
|
||||
import { saveScrollPosition } from '../../redux/preferences/preference.actions';
|
||||
import { parseChapterNumber } from '../../utils/parseChapterNumber';
|
||||
|
||||
import ReaderAppbar from './components/ReaderAppbar';
|
||||
import ReaderBottomSheet from './components/ReaderBottomSheet/ReaderBottomSheet';
|
||||
import ReaderFooter from './components/ReaderFooter';
|
||||
import ReaderSeekBar from './components/ReaderSeekBar';
|
||||
|
||||
@@ -43,14 +44,16 @@ import { LoadingScreen } from '../../components/LoadingScreen/LoadingScreen';
|
||||
import { insertHistory } from '../../database/queries/HistoryQueries';
|
||||
import { SET_LAST_READ } from '../../redux/preferences/preference.types';
|
||||
import { setAppSettings } from '../../redux/settings/settings.actions';
|
||||
import { useBatteryLevel } from 'react-native-device-info';
|
||||
import moment from 'moment';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import TextReader from './components/TextReader';
|
||||
import WebViewReader from './components/WebViewReader';
|
||||
import { useTextToSpeech } from '../../hooks/useTextToSpeech';
|
||||
import { useFullscreenMode } from '../../hooks';
|
||||
import { getChapterFromDb } from '../../database/queries/DownloadQueries';
|
||||
import ReaderBottomSheetV2 from './components/ReaderBottomSheet/ReaderBottomSheet';
|
||||
import { useReaderSettings } from '../../redux/hooks';
|
||||
import { defaultTo } from 'lodash';
|
||||
import BottomInfoBar from './components/BottomInfoBar/BottomInfoBar';
|
||||
import { sanitizeChapterText } from './utils/sanitizeChapterText';
|
||||
|
||||
const Chapter = ({ route, navigation }) => {
|
||||
useKeepAwake();
|
||||
@@ -72,25 +75,23 @@ const Chapter = ({ route, navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const readerSettings = useReaderSettings();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const {
|
||||
showScrollPercentage = true,
|
||||
fullScreenMode = true,
|
||||
swipeGestures = true,
|
||||
swipeGestures = false,
|
||||
incognitoMode = false,
|
||||
textSelectable = false,
|
||||
useWebViewForChapter = false,
|
||||
showBatteryAndTime = false,
|
||||
wvUseNewSwipes = false,
|
||||
wvShowSwipeMargins = true,
|
||||
wvUseVolumeButtons = false,
|
||||
autoScroll = false,
|
||||
autoScrollInterval = 10,
|
||||
autoScrollOffset = null,
|
||||
verticalSeekbar = true,
|
||||
removeExtraParagraphSpacing = false,
|
||||
} = useSettings();
|
||||
|
||||
const { setImmersiveMode, showStatusAndNavBar } = useFullscreenMode();
|
||||
|
||||
const batteryLevel = useBatteryLevel();
|
||||
|
||||
const [hidden, setHidden] = useState(true);
|
||||
|
||||
const { tracker, trackedNovels } = useTrackingStatus();
|
||||
@@ -107,6 +108,40 @@ const Chapter = ({ route, navigation }) => {
|
||||
|
||||
const [contentSize, setContentSize] = useState(0);
|
||||
|
||||
const [scrollPage, setScrollPage] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
VolumeButtonListener.disconnect();
|
||||
if (useWebViewForChapter && wvUseVolumeButtons) {
|
||||
VolumeButtonListener.connect();
|
||||
VolumeButtonListener.preventDefault();
|
||||
const emmiter = new NativeEventEmitter(
|
||||
NativeModules.VolumeButtonListener,
|
||||
);
|
||||
emmiter.removeAllListeners('VolumeUp');
|
||||
emmiter.removeAllListeners('VolumeDown');
|
||||
const upSub = emmiter.addListener('VolumeUp', e => {
|
||||
setScrollPage('up');
|
||||
});
|
||||
const downSub = emmiter.addListener('VolumeDown', e => {
|
||||
setScrollPage('down');
|
||||
});
|
||||
return () => {
|
||||
VolumeButtonListener.disconnect();
|
||||
upSub?.remove();
|
||||
downSub?.remove();
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (wvUseVolumeButtons) {
|
||||
VolumeButtonListener.connect();
|
||||
} else {
|
||||
VolumeButtonListener.disconnect();
|
||||
}
|
||||
}, [wvUseVolumeButtons]);
|
||||
|
||||
const getChapter = async id => {
|
||||
try {
|
||||
if (id) {
|
||||
@@ -163,30 +198,33 @@ const Chapter = ({ route, navigation }) => {
|
||||
setPrevAndNextChap();
|
||||
}, [chapter]);
|
||||
|
||||
const [currentTime, setCurrentTime] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
const timeInterval = setInterval(() => {
|
||||
setCurrentTime(new Date().toISOString());
|
||||
}, 60000);
|
||||
|
||||
return () => clearInterval(timeInterval);
|
||||
}, []);
|
||||
|
||||
const [currentOffset, setCurrentOffset] = useState(position?.position || 0);
|
||||
|
||||
let scrollTimeout;
|
||||
|
||||
useEffect(() => {
|
||||
if (!useWebViewForChapter && scrollPercentage !== 100 && autoScroll) {
|
||||
if (scrollPercentage !== 100 && autoScroll) {
|
||||
scrollTimeout = setTimeout(() => {
|
||||
scrollViewRef.current.scrollTo({
|
||||
x: 0,
|
||||
y: currentOffset + Dimensions.get('window').height,
|
||||
animated: true,
|
||||
});
|
||||
if (useWebViewForChapter) {
|
||||
setWebViewScroll({
|
||||
percentage:
|
||||
currentOffset +
|
||||
defaultTo(autoScrollOffset, Dimensions.get('window').height),
|
||||
type: 'exact',
|
||||
});
|
||||
} else {
|
||||
scrollViewRef.current.scrollTo({
|
||||
x: 0,
|
||||
y:
|
||||
currentOffset +
|
||||
defaultTo(autoScrollOffset, Dimensions.get('window').height),
|
||||
animated: true,
|
||||
});
|
||||
}
|
||||
setCurrentOffset(
|
||||
prevState => prevState + Dimensions.get('window').height,
|
||||
prevState =>
|
||||
prevState +
|
||||
defaultTo(autoScrollOffset, Dimensions.get('window').height),
|
||||
);
|
||||
}, autoScrollInterval * 1000);
|
||||
}
|
||||
@@ -235,22 +273,40 @@ const Chapter = ({ route, navigation }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const scrollToSavedProgress = useCallback(event => {
|
||||
if (position && firstLayout) {
|
||||
position.percentage < 100 &&
|
||||
scrollViewRef.current.scrollTo({
|
||||
x: 0,
|
||||
y: position.position,
|
||||
animated: false,
|
||||
});
|
||||
setFirstLayout(false);
|
||||
}
|
||||
}, []);
|
||||
const [webViewScroll, setWebViewScroll] = useState(scrollPercentage);
|
||||
|
||||
const scrollToSavedProgress = useCallback(
|
||||
event => {
|
||||
if (position && firstLayout) {
|
||||
if (position.percentage < 100) {
|
||||
scrollViewRef.current.scrollTo({
|
||||
x: 0,
|
||||
y: position.position,
|
||||
animated: false,
|
||||
});
|
||||
if (useWebViewForChapter) {
|
||||
setWebViewScroll({
|
||||
percentage: position.percentage,
|
||||
type: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
setFirstLayout(false);
|
||||
}
|
||||
},
|
||||
[nextChapter],
|
||||
);
|
||||
|
||||
const hideHeader = () => {
|
||||
if (!hidden) {
|
||||
if (useWebViewForChapter && wvUseVolumeButtons) {
|
||||
VolumeButtonListener.connect();
|
||||
}
|
||||
setImmersiveMode();
|
||||
} else {
|
||||
if (useWebViewForChapter && wvUseVolumeButtons) {
|
||||
VolumeButtonListener.disconnect();
|
||||
}
|
||||
showStatusAndNavBar();
|
||||
}
|
||||
setHidden(!hidden);
|
||||
@@ -289,13 +345,6 @@ const Chapter = ({ route, navigation }) => {
|
||||
})
|
||||
: showToast("There's no next chapter");
|
||||
|
||||
const enableSwipeGestures = () => {
|
||||
dispatch(setAppSettings('swipeGestures', !swipeGestures));
|
||||
showToast(
|
||||
swipeGestures ? 'Swipe gestures disabled' : 'Swipe gestured enabled',
|
||||
);
|
||||
};
|
||||
|
||||
const enableAutoScroll = () =>
|
||||
dispatch(setAppSettings('autoScroll', !autoScroll));
|
||||
|
||||
@@ -311,7 +360,11 @@ const Chapter = ({ route, navigation }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const backgroundColor = readerBackground(readerSettings.theme);
|
||||
const backgroundColor = readerSettings.theme;
|
||||
|
||||
const chapterText = sanitizeChapterText(chapter.chapterText, {
|
||||
removeExtraParagraphSpacing,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -332,8 +385,16 @@ const Chapter = ({ route, navigation }) => {
|
||||
pauseTts={pauseTts}
|
||||
/>
|
||||
<GestureRecognizer
|
||||
onSwipeRight={swipeGestures && navigateToPrevChapter}
|
||||
onSwipeLeft={swipeGestures && navigateToNextChapter}
|
||||
onSwipeRight={
|
||||
swipeGestures &&
|
||||
(!useWebViewForChapter || !wvUseNewSwipes) &&
|
||||
navigateToPrevChapter
|
||||
}
|
||||
onSwipeLeft={
|
||||
swipeGestures &&
|
||||
(!useWebViewForChapter || !wvUseNewSwipes) &&
|
||||
navigateToNextChapter
|
||||
}
|
||||
config={config}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
@@ -352,35 +413,20 @@ const Chapter = ({ route, navigation }) => {
|
||||
<EmptyView
|
||||
icon="Σ(ಠ_ಠ)"
|
||||
description={error}
|
||||
style={{
|
||||
color:
|
||||
readerSettings.textColor ||
|
||||
readerTextColor(readerSettings.theme),
|
||||
}}
|
||||
style={{ color: readerSettings.textColor }}
|
||||
>
|
||||
<IconButton
|
||||
icon="reload"
|
||||
size={25}
|
||||
style={{ margin: 0, marginTop: 16 }}
|
||||
color={
|
||||
readerSettings.textColor ||
|
||||
readerTextColor(readerSettings.theme)
|
||||
}
|
||||
color={readerSettings.textColor}
|
||||
onPress={() => {
|
||||
getChapter(chapterId);
|
||||
setLoading(true);
|
||||
setError();
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color:
|
||||
readerSettings.textColor ||
|
||||
readerTextColor(readerSettings.theme),
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
<Text style={{ color: readerSettings.textColor }}>Retry</Text>
|
||||
</EmptyView>
|
||||
</View>
|
||||
) : loading ? (
|
||||
@@ -388,28 +434,40 @@ const Chapter = ({ route, navigation }) => {
|
||||
) : (
|
||||
<TouchableWithoutFeedback
|
||||
style={{ flex: 1 }}
|
||||
onPress={hideHeader}
|
||||
onLayout={scrollToSavedProgress}
|
||||
>
|
||||
{useWebViewForChapter ? (
|
||||
<View style={{ flex: 1 }}>
|
||||
<WebViewReader
|
||||
layoutHeight={Dimensions.get('window').height}
|
||||
webViewScroll={webViewScroll}
|
||||
setScrollPercentage={setScrollPercentage}
|
||||
scrollPercentage={scrollPercentage}
|
||||
reader={readerSettings}
|
||||
html={chapter.chapterText}
|
||||
html={chapterText}
|
||||
chapterName={chapter.chapterName || chapterName}
|
||||
nextChapter={nextChapter}
|
||||
navigateToNextChapter={() => navigateToNextChapter()}
|
||||
navigateToPrevChapter={() => navigateToPrevChapter()}
|
||||
onScroll={onScroll}
|
||||
onPress={hideHeader}
|
||||
onWebViewNavigationStateChange={
|
||||
onWebViewNavigationStateChange
|
||||
}
|
||||
scrollPage={scrollPage}
|
||||
setScrollPage={setScrollPage}
|
||||
swipeGestures={swipeGestures && wvUseNewSwipes}
|
||||
wvShowSwipeMargins={wvShowSwipeMargins}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<TextReader
|
||||
text={chapter.chapterText}
|
||||
onPress={hideHeader}
|
||||
text={chapterText}
|
||||
reader={readerSettings}
|
||||
chapterName={chapterName || chapter.chapterName}
|
||||
textSelectable={textSelectable}
|
||||
chapterName={chapter.chapterName || chapterName}
|
||||
theme={theme}
|
||||
nextChapter={nextChapter}
|
||||
navigateToNextChapter={navigateToNextChapter}
|
||||
@@ -420,42 +478,26 @@ const Chapter = ({ route, navigation }) => {
|
||||
)}
|
||||
</ScrollView>
|
||||
</GestureRecognizer>
|
||||
|
||||
<BottomInfoBar scrollPercentage={scrollPercentage} />
|
||||
<Portal>
|
||||
<ReaderBottomSheet
|
||||
theme={theme}
|
||||
reader={readerSettings}
|
||||
dispatch={dispatch}
|
||||
navigation={navigation}
|
||||
bottomSheetRef={readerSheetRef}
|
||||
verticalSeekbar={verticalSeekbar}
|
||||
selectText={textSelectable}
|
||||
autoScroll={autoScroll}
|
||||
useWebViewForChapter={useWebViewForChapter}
|
||||
showScrollPercentage={showScrollPercentage}
|
||||
showBatteryAndTime={showBatteryAndTime}
|
||||
fullScreenMode={fullScreenMode}
|
||||
swipeGestures={swipeGestures}
|
||||
enableSwipeGestures={enableSwipeGestures}
|
||||
/>
|
||||
<ReaderBottomSheetV2 bottomSheetRef={readerSheetRef} />
|
||||
</Portal>
|
||||
{!useWebViewForChapter && (
|
||||
<ReaderSeekBar
|
||||
theme={theme}
|
||||
hide={hidden}
|
||||
setLoading={setLoading}
|
||||
contentSize={contentSize}
|
||||
scrollViewRef={scrollViewRef}
|
||||
scrollPercentage={scrollPercentage}
|
||||
setScrollPercentage={setScrollPercentage}
|
||||
verticalSeekbar={verticalSeekbar}
|
||||
/>
|
||||
)}
|
||||
<ReaderSeekBar
|
||||
theme={theme}
|
||||
hide={hidden}
|
||||
setLoading={setLoading}
|
||||
contentSize={contentSize}
|
||||
scrollViewRef={scrollViewRef}
|
||||
scrollPercentage={scrollPercentage}
|
||||
setScrollPercentage={setScrollPercentage}
|
||||
setWebViewScroll={setWebViewScroll}
|
||||
useWebViewForChapter={useWebViewForChapter}
|
||||
verticalSeekbar={verticalSeekbar}
|
||||
/>
|
||||
<ReaderFooter
|
||||
theme={theme}
|
||||
novelUrl={novelUrl}
|
||||
swipeGestures={swipeGestures}
|
||||
enableSwipeGestures={enableSwipeGestures}
|
||||
chapterUrl={chapterUrl}
|
||||
enableWebView={enableWebView}
|
||||
dispatch={dispatch}
|
||||
nextChapter={nextChapter}
|
||||
@@ -469,39 +511,6 @@ const Chapter = ({ route, navigation }) => {
|
||||
scrollViewRef={scrollViewRef}
|
||||
enableAutoScroll={enableAutoScroll}
|
||||
/>
|
||||
{(showScrollPercentage || showBatteryAndTime) && (
|
||||
<View
|
||||
style={[
|
||||
styles.scrollPercentageContainer,
|
||||
{ backgroundColor },
|
||||
!fullScreenMode && {
|
||||
paddingBottom: insets.bottom,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{showBatteryAndTime && (
|
||||
<Text style={{ color: readerSettings.textColor }}>
|
||||
{Math.ceil(batteryLevel * 100) + '%'}
|
||||
</Text>
|
||||
)}
|
||||
{showScrollPercentage && (
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
color: readerSettings.textColor,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{scrollPercentage + '%'}
|
||||
</Text>
|
||||
)}
|
||||
{showBatteryAndTime && (
|
||||
<Text style={{ color: readerSettings.textColor }}>
|
||||
{moment(currentTime).format('h:mm')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useBatteryLevel } from 'react-native-device-info';
|
||||
import moment from 'moment';
|
||||
|
||||
import { useReaderSettings, useSettingsV1 } from '@redux/hooks';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
interface BottomInfoBarProps {
|
||||
scrollPercentage: number;
|
||||
}
|
||||
|
||||
const BottomInfoBar: React.FC<BottomInfoBarProps> = ({ scrollPercentage }) => {
|
||||
const { bottom: bottomInset } = useSafeAreaInsets();
|
||||
const {
|
||||
showScrollPercentage = true,
|
||||
showBatteryAndTime = false,
|
||||
fullScreenMode = true,
|
||||
} = useSettingsV1();
|
||||
|
||||
const { textColor, theme: backgroundColor } = useReaderSettings();
|
||||
|
||||
const batteryLevel = useBatteryLevel();
|
||||
|
||||
const [currentTime, setCurrentTime] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const timeInterval = setInterval(() => {
|
||||
setCurrentTime(new Date().toISOString());
|
||||
}, 60000);
|
||||
|
||||
return () => clearInterval(timeInterval);
|
||||
}, []);
|
||||
|
||||
if (showScrollPercentage || showBatteryAndTime) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor },
|
||||
!fullScreenMode && {
|
||||
paddingBottom: bottomInset,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{showBatteryAndTime && batteryLevel ? (
|
||||
<Text style={{ color: textColor }}>
|
||||
{Math.ceil(batteryLevel * 100) + '%'}
|
||||
</Text>
|
||||
) : null}
|
||||
{showScrollPercentage ? (
|
||||
<Text style={[styles.scrollPercentage, { color: textColor }]}>
|
||||
{scrollPercentage + '%'}
|
||||
</Text>
|
||||
) : null}
|
||||
{showBatteryAndTime ? (
|
||||
<Text style={{ color: textColor }}>
|
||||
{moment(currentTime).format('LT')}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default BottomInfoBar;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
paddingVertical: 10,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 1,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
scrollPercentage: {
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
@@ -42,7 +42,7 @@ const ReaderAppbar = ({
|
||||
<Appbar.Content
|
||||
title={novelName}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
subtitle={chapterName}
|
||||
subtitle={chapterName?.trim()}
|
||||
subtitleStyle={{ color: theme.textColorSecondary }}
|
||||
/>
|
||||
<Appbar.Action
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import { StyleSheet, View, Text, ScrollView, Animated } from 'react-native';
|
||||
|
||||
import Slider from '@react-native-community/slider';
|
||||
import { IconButton } from 'react-native-paper';
|
||||
import Bottomsheet from 'rn-sliding-up-panel';
|
||||
|
||||
import BottomSheetHandle from '../../../../components/BottomSheetHandle';
|
||||
import { Row } from '../../../../components/Common';
|
||||
|
||||
import {
|
||||
setAppSettings,
|
||||
setReaderSettings,
|
||||
} from '../../../../redux/settings/settings.actions';
|
||||
import {
|
||||
ToggleButton,
|
||||
ToggleColorButton,
|
||||
} from '../../../../components/Common/ToggleButton';
|
||||
import { ReaderBottomSheetSwitch } from './components/ReaderBottomSheetSwitch';
|
||||
import { ReaderBottomSheetFontPicker } from './components/ReaderBottomSheetFontPicker';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
const ReaderSheet = ({
|
||||
theme,
|
||||
reader,
|
||||
dispatch,
|
||||
navigation,
|
||||
bottomSheetRef,
|
||||
showBatteryAndTime,
|
||||
selectText,
|
||||
autoScroll,
|
||||
useWebViewForChapter,
|
||||
showScrollPercentage,
|
||||
swipeGestures,
|
||||
enableSwipeGestures,
|
||||
fullScreenMode,
|
||||
verticalSeekbar,
|
||||
}) => {
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
|
||||
const presetThemes = [
|
||||
{ value: 2, backgroundColor: '#f5f5fa', textColor: '#111111' },
|
||||
{ value: 3, backgroundColor: '#F7DFC6', textColor: '#593100' },
|
||||
{ value: 6, backgroundColor: '#dce5e2', textColor: '#000000' },
|
||||
{ value: 4, backgroundColor: '#292832', textColor: '#CCCCCC' },
|
||||
{
|
||||
value: 1,
|
||||
backgroundColor: '#000000',
|
||||
textColor: 'rgba(255,255,255,0.7)',
|
||||
},
|
||||
];
|
||||
|
||||
const textAlignments = [
|
||||
{ value: 'left', icon: 'format-align-left' },
|
||||
{ value: 'center', icon: 'format-align-center' },
|
||||
{ value: 'justify', icon: 'format-align-justify' },
|
||||
{ value: 'right', icon: 'format-align-right' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 390, bottom: 0 }}
|
||||
snappingPoints={[0, 390]}
|
||||
showBackdrop={true}
|
||||
backdropOpacity={0}
|
||||
height={390}
|
||||
>
|
||||
<ScrollView
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<BottomSheetHandle theme={theme} />
|
||||
<View style={{ flex: 1, paddingTop: 24, paddingBottom: bottom }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Text size
|
||||
</Text>
|
||||
<Slider
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 40,
|
||||
}}
|
||||
value={reader.textSize}
|
||||
minimumValue={12}
|
||||
maximumValue={20}
|
||||
step={1}
|
||||
minimumTrackTintColor={theme.colorAccent}
|
||||
maximumTrackTintColor="#000000"
|
||||
thumbTintColor={theme.colorAccent}
|
||||
onSlidingComplete={value =>
|
||||
dispatch(setReaderSettings('textSize', value))
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ paddingLeft: 16, paddingRight: 8 }}>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>Color</Text>
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<ScrollView horizontal={true}>
|
||||
{presetThemes.map((item, index) => (
|
||||
<ToggleColorButton
|
||||
key={index}
|
||||
selected={reader.theme === item.value}
|
||||
backgroundColor={item.backgroundColor}
|
||||
textColor={item.textColor}
|
||||
theme={theme}
|
||||
onPress={() => {
|
||||
dispatch(setReaderSettings('theme', item.value));
|
||||
dispatch(
|
||||
setReaderSettings('textColor', item.textColor),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Text align
|
||||
</Text>
|
||||
<Row>
|
||||
{textAlignments.map(item => (
|
||||
<ToggleButton
|
||||
key={item.value}
|
||||
selected={item.value === reader.textAlign}
|
||||
icon={item.icon}
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
dispatch(setReaderSettings('textAlign', item.value))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>Padding</Text>
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.padding <= 0 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(setReaderSettings('padding', reader.padding - 1))
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{`${reader.padding}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.padding >= 10 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(setReaderSettings('padding', reader.padding + 1))
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
marginVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
Line height
|
||||
</Text>
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.lineHeight <= 1.3 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings('lineHeight', reader.lineHeight - 0.1),
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{`${Math.round(reader.lineHeight * 10) / 10}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.lineHeight >= 2 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings('lineHeight', reader.lineHeight + 0.1),
|
||||
)
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<ReaderBottomSheetFontPicker reader={reader} theme={theme} />
|
||||
</View>
|
||||
<ReaderBottomSheetSwitch
|
||||
label="Render HTML"
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings('useWebViewForChapter', !useWebViewForChapter),
|
||||
)
|
||||
}
|
||||
value={useWebViewForChapter}
|
||||
theme={theme}
|
||||
/>
|
||||
<ReaderBottomSheetSwitch
|
||||
label="Fullscreen"
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings('fullScreenMode', !fullScreenMode))
|
||||
}
|
||||
value={fullScreenMode}
|
||||
theme={theme}
|
||||
/>
|
||||
{!useWebViewForChapter && (
|
||||
<>
|
||||
<ReaderBottomSheetSwitch
|
||||
label="AutoScroll"
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings('autoScroll', !autoScroll))
|
||||
}
|
||||
value={autoScroll}
|
||||
theme={theme}
|
||||
/>
|
||||
<ReaderBottomSheetSwitch
|
||||
label="Vertical seekbar"
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings('verticalSeekbar', !verticalSeekbar))
|
||||
}
|
||||
value={verticalSeekbar}
|
||||
theme={theme}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ReaderBottomSheetSwitch
|
||||
label="Show battery and time"
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings('showBatteryAndTime', !showBatteryAndTime),
|
||||
)
|
||||
}
|
||||
value={showBatteryAndTime}
|
||||
theme={theme}
|
||||
/>
|
||||
<ReaderBottomSheetSwitch
|
||||
label="Show progress percentage"
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings('showScrollPercentage', !showScrollPercentage),
|
||||
)
|
||||
}
|
||||
value={showScrollPercentage}
|
||||
theme={theme}
|
||||
/>
|
||||
<ReaderBottomSheetSwitch
|
||||
label="Swipe gestures"
|
||||
onPress={enableSwipeGestures}
|
||||
value={swipeGestures}
|
||||
theme={theme}
|
||||
/>
|
||||
{!useWebViewForChapter && (
|
||||
<ReaderBottomSheetSwitch
|
||||
label="Select text"
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings('textSelectable', !selectText))
|
||||
}
|
||||
value={selectText}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Bottomsheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ReaderSheet);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
borderTopLeftRadius: 8,
|
||||
borderTopRightRadius: 8,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
},
|
||||
readerSettingsContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
paddingTop: 30,
|
||||
},
|
||||
title: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
switchStyle: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user